File size: 2,490 Bytes
0eaa85f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import * as PptxModule from "pptxgenjs";
import { cleanHex, normalizeTheme } from "./themes.js";

export type SlideSpec = {
  layout: "title" | "section" | "bullets" | "chart_and_insight" | "image_reference";
  title: string;
  subtitle?: string;
  bullets?: string[];
  insights?: string[];
  notes?: string;
};

export type PresentationSpec = {
  title: string;
  theme: Record<string, unknown>;
  slides: SlideSpec[];
  provenance?: Record<string, unknown>[];
};

export async function renderPresentation(spec: PresentationSpec): Promise<Buffer> {
  const PptxGenJS = ((PptxModule as unknown as { default?: unknown }).default ?? PptxModule) as new () => any;
  const pptx = new PptxGenJS();
  pptx.layout = "LAYOUT_WIDE";
  pptx.author = "GLM Visual Variable Runtime";
  pptx.subject = "Generated from visual evidence";
  pptx.title = spec.title;

  const theme = normalizeTheme(spec.theme);
  pptx.defineSlideMaster({
    title: "VVR_MASTER",
    background: { color: cleanHex(theme.background) },
    objects: [
      { rect: { x: 0, y: 0, w: 13.333, h: 0.12, fill: { color: cleanHex(theme.accent) }, line: { color: cleanHex(theme.accent) } } }
    ],
    slideNumber: { x: 12.3, y: 7.15, color: cleanHex(theme.secondary) }
  });

  const slides = spec.slides.length ? spec.slides : [{ layout: "title" as const, title: spec.title }];
  for (const item of slides) {
    const slide = pptx.addSlide("VVR_MASTER");
    slide.addText(item.title, {
      x: 0.7,
      y: 0.65,
      w: 11.9,
      h: 0.6,
      fontFace: "Aptos Display",
      fontSize: item.layout === "title" ? 34 : 26,
      bold: true,
      color: cleanHex(theme.primary),
      margin: 0
    });
    if (item.subtitle) {
      slide.addText(item.subtitle, { x: 0.75, y: 1.35, w: 11.7, h: 0.4, fontSize: 16, color: cleanHex(theme.secondary) });
    }
    const bullets = [...(item.bullets ?? []), ...(item.insights ?? [])];
    if (bullets.length) {
      slide.addText(bullets.map((text) => ({ text, options: { bullet: { indent: 14 }, hanging: 4 } })), {
        x: 0.9,
        y: 2.0,
        w: 11.4,
        h: 4.6,
        fontSize: 18,
        color: cleanHex(theme.text_dark),
        breakLine: false,
        fit: "shrink"
      });
    }
    if (item.notes || spec.provenance?.length) {
      slide.addNotes(`Provenance: ${JSON.stringify(spec.provenance ?? [])}\n${item.notes ?? ""}`);
    }
  }

  const data = await pptx.write({ outputType: "nodebuffer" });
  return Buffer.from(data as ArrayBuffer);
}