/* * editable-pptx-export.ts * Purpose: Builds an editable PowerPoint file from the current slide/editor state. * Used by: hooks/useExport. * Depends on: pptxgenjs, editor types, and template/theme helpers. */ import PptxGenJS from 'pptxgenjs'; import { getTemplateById, type SlideSpec } from '@/data/templates'; import type { SlideModel, TextElement, ImageElement, ShapeElement } from '@/lib/editor-types'; import { themes as editorThemes, TEMPLATE_THEMES } from '@/lib/editor-themes'; const PPT_WIDTH = 10; const PPT_HEIGHT = 5.625; const CANVAS_WIDTH = 800; const PX_TO_IN = PPT_WIDTH / CANVAS_WIDTH; type Rect = { x: number; y: number; w: number; h: number }; type PptxSlide = PptxGenJS.Slide; type ImageProps = PptxGenJS.ImageProps; interface EditablePptxExportParams { slides: SlideModel[]; slideSpecs: SlideSpec[]; currentTheme: keyof typeof editorThemes; presentationTitle: string; } function pxToIn(value: number) { return value * PX_TO_IN; } function clamp(value: number, min: number, max: number) { return Math.max(min, Math.min(value, max)); } function cleanText(value: string | undefined | null) { return (value || '').replace(/\r\n/g, '\n').trim(); } function upperText(value: string | undefined | null) { return cleanText(value).toUpperCase(); } function normalizeColor(color: string | undefined, fallback = '000000') { if (!color) return fallback; const hex = color.trim().replace(/^#/, ''); if (/^[0-9a-fA-F]{3}$/.test(hex)) { return hex .split('') .map((char) => `${char}${char}`) .join('') .toUpperCase(); } if (/^[0-9a-fA-F]{6}$/.test(hex)) return hex.toUpperCase(); return fallback; } function normalizeOpacity(opacity: number | undefined) { if (typeof opacity !== 'number' || Number.isNaN(opacity)) return 0; return clamp(Math.round((1 - opacity) * 100), 0, 100); } function sanitizeFileName(title: string) { return title.replace(/[^a-zA-Z0-9 -]/g, '').trim() || 'presentation'; } function normalizeFontFace(font: string | undefined, fallback = 'Arial') { if (!font) return fallback; const raw = font .split(',') .map((item) => item.trim().replace(/^['"]|['"]$/g, '')) .find(Boolean); if (!raw) return fallback; const lower = raw.toLowerCase(); if (lower.includes('newsreader')) return 'Georgia'; if (lower.includes('inter')) return 'Arial'; if (lower.includes('manrope')) return 'Arial'; if (lower.includes('anton')) return 'Arial Black'; if (lower.includes('oswald')) return 'Arial Narrow'; if (lower.includes('roboto mono') || lower.includes('courier') || lower.includes('mono')) return 'Courier New'; if (lower.includes('serif')) return 'Georgia'; if (lower.includes('sans')) return 'Arial'; return raw; } function bodyToParagraph(body: SlideSpec['body'] | string | undefined) { if (typeof body === 'string') { return cleanText(body); } return (body || []) .map((item) => { const heading = cleanText(item.heading); const text = cleanText(item.text); if (heading && text) return `${heading}\n${text}`; return heading || text; }) .filter(Boolean) .join('\n\n'); } function getCompactTextLength(value: string | undefined | null) { return cleanText(value).replace(/\s+/g, ' ').length; } function getResponsiveFontSize( value: string | undefined | null, steps: Array<{ maxLength: number; size: number }>, fallback: number ) { const length = getCompactTextLength(value); if (!length) return fallback; for (const step of steps) { if (length <= step.maxLength) { return step.size; } } return steps[steps.length - 1]?.size ?? fallback; } function splitAgendaItem(text: string) { const [heading, description] = text.split('||').map((part) => cleanText(part)); return { heading: heading || cleanText(text), description }; } function parseReferenceItem(text: string, index: number) { const parts = text .split('||') .map((part) => cleanText(part)) .filter(Boolean); if (parts.length >= 2) { return { label: parts[0], value: parts[1], url: parts[2], }; } return { label: `REF_${String(index + 1).padStart(2, '0')}`, value: cleanText(text), url: '', }; } function formattingOffset(spec: SlideSpec, key: string) { const source = spec.formatting?.[key]; const x = typeof source?.x === 'number' && Number.isFinite(source.x) ? source.x : 0; const y = typeof source?.y === 'number' && Number.isFinite(source.y) ? source.y : 0; const width = typeof source?.width === 'number' && Number.isFinite(source.width) && source.width > 0 ? pxToIn(source.width) : undefined; const height = typeof source?.height === 'number' && Number.isFinite(source.height) && source.height > 0 ? pxToIn(source.height) : undefined; return { x: pxToIn(x), y: pxToIn(y), width, height }; } function withOffset(spec: SlideSpec, key: string, rect: Rect): Rect { const offset = formattingOffset(spec, key); return { x: rect.x + offset.x, y: rect.y + offset.y, w: offset.width ?? rect.w, h: offset.height ?? rect.h, }; } function addTextBox(slide: PptxSlide, text: string, rect: Rect, options: Record = {}) { slide.addText(text || ' ', { x: rect.x, y: rect.y, w: rect.w, h: rect.h, margin: 0, fit: 'shrink', valign: 'middle', ...options, }); } function addRect( slide: PptxSlide, rect: Rect, fillColor: string, lineColor = fillColor, extra: Record = {} ) { slide.addShape('rect', { x: rect.x, y: rect.y, w: rect.w, h: rect.h, fill: { color: normalizeColor(fillColor) }, line: { color: normalizeColor(lineColor), width: 1 }, ...extra, }); } function getImageSizing(rect: Rect): Pick { return { x: rect.x, y: rect.y, w: rect.w, h: rect.h, sizing: { type: 'cover', w: rect.w, h: rect.h }, }; } async function sourceToImageProps( src: string | undefined, rect: Rect, extra: Partial = {} ): Promise { const cleanSrc = cleanText(src); if (!cleanSrc) return null; if (cleanSrc.startsWith('data:')) { return { data: cleanSrc, ...getImageSizing(rect), ...extra }; } try { const response = await fetch(cleanSrc); if (!response.ok) throw new Error(`Image fetch failed: ${response.status}`); const blob = await response.blob(); const dataUrl = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(typeof reader.result === 'string' ? reader.result : ''); reader.onerror = () => reject(reader.error || new Error('Failed to read image')); reader.readAsDataURL(blob); }); if (dataUrl) return { data: dataUrl, ...getImageSizing(rect), ...extra }; } catch { if (/^https?:\/\//i.test(cleanSrc)) { return { path: cleanSrc, ...getImageSizing(rect), ...extra }; } } return null; } async function addImage( slide: PptxSlide, src: string | undefined, rect: Rect, extra: Partial = {} ) { const imageProps = await sourceToImageProps(src, rect, extra); if (imageProps) { slide.addImage(imageProps); return; } addRect(slide, rect, 'E5E7EB', 'CBD5E1'); addTextBox(slide, 'Image unavailable', rect, { fontSize: 16, color: '475569', fontFace: 'Arial', align: 'center', valign: 'middle', }); } function addEditorText(slide: PptxSlide, element: TextElement) { addTextBox( slide, cleanText(element.text), { x: pxToIn(element.x), y: pxToIn(element.y), w: pxToIn(element.width), h: pxToIn(element.height), }, { fontFace: normalizeFontFace(element.fontFamily), fontSize: Math.max(8, Math.round(element.fontSize * 0.72)), color: normalizeColor(element.color), bold: element.fontWeight === 'bold', italic: element.fontStyle === 'italic', underline: element.textDecoration === 'underline', align: element.align, rotate: element.rotation || 0, valign: 'top', margin: 0, } ); } async function addEditorImage(slide: PptxSlide, element: ImageElement) { await addImage(slide, element.src, { x: pxToIn(element.x), y: pxToIn(element.y), w: pxToIn(element.width), h: pxToIn(element.height), }); } function addEditorShape(slide: PptxSlide, element: ShapeElement) { const rect = { x: pxToIn(element.x), y: pxToIn(element.y), w: pxToIn(element.width), h: pxToIn(element.height), }; const line = { color: normalizeColor(element.borderColor), width: Math.max(0.75, element.borderWidth), transparency: normalizeOpacity(element.opacity), }; const fill = { color: normalizeColor(element.backgroundColor, 'FFFFFF'), transparency: normalizeOpacity(element.opacity), }; const common = { ...rect, rotate: element.rotation || 0, line, fill }; switch (element.shapeType) { case 'rectangle': slide.addShape('rect', common); return; case 'circle': slide.addShape('ellipse', common); return; case 'triangle': slide.addShape('triangle', common); return; case 'arrow': slide.addShape('chevron', common); return; case 'star': slide.addShape('star5', common); return; case 'diamond': slide.addShape('diamond', common); return; case 'hexagon': slide.addShape('hexagon', common); return; case 'line': slide.addShape('line', { ...rect, rotate: element.rotation || 0, line: { color: normalizeColor(element.borderColor), width: Math.max(1, element.borderWidth), transparency: normalizeOpacity(element.opacity), }, }); return; } } async function addLegacySlide( slide: PptxSlide, slideModel: SlideModel, currentTheme: keyof typeof editorThemes ) { const theme = editorThemes[currentTheme] as { solidBackground?: string; background?: string; backgroundImage?: string; }; slide.background = { color: normalizeColor(theme.solidBackground || theme.background, 'FFFFFF') }; const backgroundImage = theme.backgroundImage?.match(/url\((['"]?)(.*?)\1\)/)?.[2]; if (backgroundImage) { await addImage(slide, backgroundImage, { x: 0, y: 0, w: PPT_WIDTH, h: PPT_HEIGHT }); slide.addShape('rect', { x: 0, y: 0, w: PPT_WIDTH, h: PPT_HEIGHT, fill: { color: normalizeColor(theme.solidBackground || '000000'), transparency: 30 }, line: { color: normalizeColor(theme.solidBackground || '000000'), transparency: 100 }, }); } for (const element of slideModel.elements) { if (element.type === 'text') { addEditorText(slide, element); continue; } if (element.type === 'image') { await addEditorImage(slide, element); continue; } addEditorShape(slide, element); } } function addNeoBackground(slide: PptxSlide) { slide.background = { color: 'F5F5F0' }; } function addGalerynBackground(slide: PptxSlide) { slide.background = { color: 'FBF9F4' }; } function addNoisyBackground(slide: PptxSlide, color = '547BEE') { slide.background = { color }; } function addNeoDotPattern(slide: PptxSlide) { const spacing = 0.42; const dotSize = 0.022; for (let y = 0.18; y < PPT_HEIGHT - 0.08; y += spacing) { for (let x = 0.18; x < PPT_WIDTH - 0.08; x += spacing) { slide.addShape('ellipse', { x, y, w: dotSize, h: dotSize, fill: { color: '000000', transparency: 55 }, line: { color: '000000', transparency: 100 }, }); } } } function addNeoShadowCard( slide: PptxSlide, rect: Rect, fillColor: string, options: { rotate?: number; shadowX?: number; shadowY?: number; shadowColor?: string; lineWidth?: number; } = {} ) { const { rotate = 0, shadowX = 0.12, shadowY = 0.12, shadowColor = '000000', lineWidth = 2.25, } = options; slide.addShape('rect', { x: rect.x + shadowX, y: rect.y + shadowY, w: rect.w, h: rect.h, rotate, fill: { color: normalizeColor(shadowColor) }, line: { color: normalizeColor(shadowColor), transparency: 100 }, }); slide.addShape('rect', { x: rect.x, y: rect.y, w: rect.w, h: rect.h, rotate, fill: { color: normalizeColor(fillColor) }, line: { color: '000000', width: lineWidth }, }); } function addNeoTitleSubtitle(slide: PptxSlide, spec: SlideSpec) { addNeoBackground(slide); addNeoDotPattern(slide); const titleCard = withOffset(spec, 'title', { x: 0.72, y: 0.78, w: 8.15, h: 2.02 }); addNeoShadowCard(slide, titleCard, 'D9FF00', { rotate: -1.2, shadowX: 0.12, shadowY: 0.12 }); addTextBox(slide, upperText(spec.title), { x: titleCard.x + 0.36, y: titleCard.y + 0.3, w: titleCard.w - 0.72, h: titleCard.h - 0.6 }, { fontFace: 'Arial Black', fontSize: 31, bold: true, color: '000000', align: 'center', valign: 'middle', rotate: -1.2, }); const badgeRect = { x: titleCard.x + titleCard.w - 0.48, y: titleCard.y - 0.16, w: 0.48, h: 0.48 }; addNeoShadowCard(slide, badgeRect, 'D9FF00', { shadowX: 0.05, shadowY: 0.05 }); slide.addShape('rect', { x: badgeRect.x + 0.08, y: badgeRect.y + 0.08, w: badgeRect.w - 0.16, h: badgeRect.h - 0.16, fill: { color: '000000' }, line: { color: '000000', transparency: 100 }, }); addTextBox(slide, '★', { x: badgeRect.x + 0.115, y: badgeRect.y + 0.085, w: 0.25, h: 0.24 }, { fontFace: 'Arial Black', fontSize: 16, bold: true, color: 'D9FF00', align: 'center', valign: 'middle', }); const subtitleCard = withOffset(spec, 'subtitle', { x: 1.52, y: 3.35, w: 7.0, h: 1.02 }); addNeoShadowCard(slide, subtitleCard, 'FFFFFF', { shadowX: 0.1, shadowY: 0.1 }); addTextBox(slide, upperText(spec.subtitle), { x: subtitleCard.x + 0.28, y: subtitleCard.y + 0.22, w: subtitleCard.w - 0.56, h: subtitleCard.h - 0.44 }, { fontFace: 'Arial', fontSize: 15, bold: true, color: '000000', align: 'center', valign: 'middle', }); } function addNeoAgenda(slide: PptxSlide, spec: SlideSpec) { addNeoBackground(slide); addNeoDotPattern(slide); addTextBox(slide, upperText(spec.title), withOffset(spec, 'agenda-title', { x: 0.55, y: 0.45, w: 4.2, h: 0.65 }), { fontFace: 'Arial Black', fontSize: 25, bold: true, color: '000000', }); const cards = [ { x: 0.55, y: 1.45 }, { x: 3.45, y: 1.45 }, { x: 6.35, y: 1.45 }, { x: 0.55, y: 3.28 }, { x: 3.45, y: 3.28 }, ]; (spec.items || []).slice(0, 5).forEach((item, index) => { const rect = withOffset(spec, `agenda-card-${index}`, { x: cards[index]?.x || 0.55, y: cards[index]?.y || 3.28, w: 2.45, h: 1.45 }); addNeoShadowCard(slide, rect, 'FFFFFF', { shadowX: 0.1, shadowY: 0.1, shadowColor: 'A000A0', }); addTextBox(slide, `${String(index + 1).padStart(2, '0')}/`, { x: rect.x + 0.16, y: rect.y + 0.12, w: 0.52, h: 0.2 }, { fontFace: 'Arial Black', fontSize: 11, bold: true, color: 'A000A0', }); addTextBox(slide, cleanText(item.text), { x: rect.x + 0.16, y: rect.y + 0.42, w: rect.w - 0.32, h: rect.h - 0.55 }, { fontFace: 'Arial Black', fontSize: 18, bold: true, color: '000000', valign: 'top', }); }); } function addNeoTitleAndText(slide: PptxSlide, spec: SlideSpec) { addNeoBackground(slide); addNeoDotPattern(slide); const titleRect = withOffset(spec, 'title-and-text-title', { x: 2.2, y: 1.0, w: 5.6, h: 0.85 }); addNeoShadowCard(slide, titleRect, '00FFFF', { shadowX: 0.1, shadowY: 0.1 }); addTextBox(slide, upperText(spec.title), titleRect, { fontFace: 'Arial Black', fontSize: 26, bold: true, color: '000000', align: 'center', }); const bodyRect = withOffset(spec, 'title-and-text-body', { x: 1.25, y: 2.2, w: 7.5, h: 1.7 }); addNeoShadowCard(slide, bodyRect, 'FFFFFF', { shadowX: 0.1, shadowY: 0.1 }); addTextBox(slide, bodyToParagraph(spec.body), { x: bodyRect.x + 0.18, y: bodyRect.y + 0.18, w: bodyRect.w - 0.36, h: bodyRect.h - 0.36 }, { fontFace: 'Arial', fontSize: 18, color: '000000', align: 'center', valign: 'middle', }); } function addNeoThreeColumns(slide: PptxSlide, spec: SlideSpec) { addNeoBackground(slide); addNeoDotPattern(slide); const titleRect = withOffset(spec, 'columns-title', { x: 0.55, y: 0.45, w: 3.8, h: 0.75 }); addNeoShadowCard(slide, titleRect, 'D9FF00', { shadowX: 0.1, shadowY: 0.1 }); addTextBox(slide, upperText(spec.title), titleRect, { fontFace: 'Arial Black', fontSize: 22, bold: true, color: '000000', }); const decorRect = withOffset(spec, 'columns-decor', { x: 8.45, y: 0.42, w: 0.8, h: 0.8 }); slide.addShape('rect', { x: decorRect.x, y: decorRect.y, w: decorRect.w, h: decorRect.h, fill: { color: 'FFFFFF', transparency: 75 }, line: { color: '000000', width: 2.25, transparency: 75 }, }); const columnX = [0.55, 3.35, 6.15]; (spec.columns || []).slice(0, 3).forEach((column, index) => { const rect = withOffset(spec, `column-card-${index}`, { x: columnX[index], y: 1.65, w: 2.55, h: 3.25 }); addNeoShadowCard(slide, rect, 'FFFFFF', { shadowX: 0.1, shadowY: 0.1 }); addRect(slide, { x: rect.x, y: rect.y, w: rect.w, h: 0.65 }, 'D9FF00', '000000', { line: { color: '000000', width: 0.75 }, }); addTextBox(slide, String(index + 1).padStart(2, '0'), { x: rect.x + 0.15, y: rect.y + 0.12, w: 0.5, h: 0.2 }, { fontFace: 'Arial Black', fontSize: 14, bold: true, color: '000000', }); addTextBox(slide, upperText(column.heading), { x: rect.x + 0.18, y: rect.y + 0.85, w: rect.w - 0.36, h: 0.65 }, { fontFace: 'Arial Black', fontSize: 16, bold: true, color: '000000', valign: 'top', }); addTextBox(slide, cleanText(column.text), { x: rect.x + 0.18, y: rect.y + 1.65, w: rect.w - 0.36, h: 1.25 }, { fontFace: 'Arial', fontSize: 11, color: '000000', valign: 'top', }); }); } async function addNeoImageAndText(slide: PptxSlide, spec: SlideSpec) { addNeoBackground(slide); addNeoDotPattern(slide); const imageCard = withOffset(spec, 'image-card', { x: 0.6, y: 0.7, w: 4.15, h: 3.45 }); addNeoShadowCard(slide, imageCard, 'FFD700', { rotate: 2, shadowX: 0.1, shadowY: 0.1 }); const imageFrame = { x: imageCard.x + 0.18, y: imageCard.y + 0.18, w: imageCard.w - 0.36, h: Math.max(2.35, imageCard.h - 0.95), }; addRect(slide, imageFrame, 'FFFFFF', '000000', { line: { color: '000000', width: 2.25 }, rotate: 2 }); await addImage(slide, spec.imageUrl, { x: imageFrame.x + 0.04, y: imageFrame.y + 0.04, w: imageFrame.w - 0.08, h: imageFrame.h - 0.08, }, { rotate: 2 }); addTextBox(slide, 'FIG 1. STRUCTURAL HONESTY', { x: imageCard.x + 0.18, y: imageCard.y + imageCard.h - 0.42, w: imageCard.w - 0.36, h: 0.22 }, { fontFace: 'Arial Black', fontSize: 11, bold: true, color: '000000', align: 'center', rotate: 2, }); const textCard = withOffset(spec, 'image-text', { x: 5.3, y: 1.15, w: 4.05, h: 2.65 }); addNeoShadowCard(slide, textCard, 'FFFFFF', { rotate: -1, shadowX: 0.1, shadowY: 0.1 }); addTextBox(slide, upperText(spec.title), { x: textCard.x + 0.2, y: textCard.y + 0.2, w: textCard.w - 0.4, h: 0.8 }, { fontFace: 'Arial Black', fontSize: 24, bold: true, color: '000000', rotate: -1, }); addTextBox(slide, bodyToParagraph(spec.body), { x: textCard.x + 0.2, y: textCard.y + 1.1, w: textCard.w - 0.4, h: 1.2 }, { fontFace: 'Arial', fontSize: 12, color: '000000', valign: 'top', rotate: -1, }); } function addNeoReferences(slide: PptxSlide, spec: SlideSpec) { addNeoBackground(slide); addNeoDotPattern(slide); const titleRect = withOffset(spec, 'references-title', { x: 0.55, y: 0.45, w: 4.0, h: 0.65 }); addNeoShadowCard(slide, titleRect, '000000', { shadowX: 0.1, shadowY: 0.1 }); addTextBox(slide, upperText(spec.title), titleRect, { fontFace: 'Arial Black', fontSize: 18, bold: true, color: 'FFFFFF', }); (spec.items || []).slice(0, 6).forEach((item, index) => { const row = withOffset(spec, `reference-item-${index}`, { x: 0.7, y: 1.45 + index * 0.62, w: 5.1, h: 0.45 }); const parsed = parseReferenceItem(item.text, index); slide.addShape('rect', { x: row.x, y: row.y, w: row.w, h: row.h, fill: { color: 'FFFFFF', transparency: 30 }, line: { color: 'FFFFFF', transparency: 100 }, }); slide.addShape('line', { x: row.x, y: row.y + row.h, w: row.w, h: 0, line: { color: '000000', width: 2.25 }, }); addTextBox(slide, upperText(parsed.label), { x: row.x + 0.08, y: row.y + 0.05, w: 1.15, h: 0.14 }, { fontFace: 'Arial Black', fontSize: 8.5, color: 'A000A0', bold: true, }); addTextBox(slide, parsed.value, { x: row.x + 0.08, y: row.y + 0.17, w: row.w - 0.16, h: row.h - 0.16 }, { fontFace: 'Arial', fontSize: 12.5, color: '000000', bold: true, valign: 'top', }); }); const noteRect = withOffset(spec, 'references-note', { x: 6.4, y: 1.55, w: 2.85, h: 2.0 }); addNeoShadowCard(slide, noteRect, 'FFD700', { shadowX: 0.1, shadowY: 0.1 }); const noteLabelRect = { x: noteRect.x + 0.24, y: noteRect.y + 0.22, w: 1.55, h: 0.34 }; slide.addShape('rect', { x: noteLabelRect.x, y: noteLabelRect.y, w: noteLabelRect.w, h: noteLabelRect.h, rotate: -2, fill: { color: '000000' }, line: { color: '000000', width: 2.25 }, }); addTextBox(slide, 'RAW SOURCES', noteLabelRect, { fontFace: 'Arial Black', fontSize: 14, color: 'FFFFFF', bold: true, align: 'center', rotate: -2, }); addTextBox(slide, 'Reference entries stay editable inline. Use label || title || url if you want a tagged source with a clickable link.', { x: noteRect.x + 0.22, y: noteRect.y + 0.72, w: noteRect.w - 0.44, h: noteRect.h - 0.92, }, { fontFace: 'Arial', fontSize: 10.5, color: '000000', bold: true, valign: 'top', }); } function addNeoThankYou(slide: PptxSlide, spec: SlideSpec) { addNeoBackground(slide); addNeoDotPattern(slide); const titleRect = withOffset(spec, 'thank-you-title', { x: 2.0, y: 1.05, w: 5.9, h: 2.2 }); addNeoShadowCard(slide, titleRect, 'D9FF00', { shadowX: 0.1, shadowY: 0.1 }); const labelRect = { x: titleRect.x - 0.42, y: titleRect.y - 0.34, w: 1.25, h: 0.42 }; slide.addShape('rect', { x: labelRect.x, y: labelRect.y, w: labelRect.w, h: labelRect.h, rotate: 12, fill: { color: 'A000A0' }, line: { color: '000000', width: 2.25 }, }); addTextBox(slide, 'THE_END', labelRect, { fontFace: 'Arial Black', fontSize: 12, color: 'FFFFFF', bold: true, align: 'center', rotate: 12, }); addTextBox(slide, upperText(spec.title), titleRect, { fontFace: 'Arial Black', fontSize: 30, bold: true, color: '000000', align: 'center', valign: 'middle', }); const subtitleRect = withOffset(spec, 'thank-you-subtitle', { x: 2.2, y: 4.05, w: 5.6, h: 0.42 }); slide.addShape('rect', { x: subtitleRect.x, y: subtitleRect.y, w: subtitleRect.w, h: subtitleRect.h, fill: { color: 'FFFFFF', transparency: 30 }, line: { color: 'FFFFFF', transparency: 100 }, }); addTextBox(slide, upperText(spec.subtitle), subtitleRect, { fontFace: 'Arial', fontSize: 13, bold: true, color: '000000', align: 'center', }); } function addGalerynTitleSubtitle(slide: PptxSlide, spec: SlideSpec) { addGalerynBackground(slide); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title', { x: 1.0, y: 1.1, w: 8.0, h: 1.55 }), { fontFace: 'Georgia', fontSize: 42, bold: true, color: '021D30', align: 'center', valign: 'middle', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'subtitle', { x: 2.0, y: 3.65, w: 6.0, h: 0.85 }), { fontFace: 'Arial', fontSize: 12, color: '021D30', align: 'center', valign: 'middle', }); } async function addGalerynAgenda(slide: PptxSlide, spec: SlideSpec) { addGalerynBackground(slide); const imageRect = withOffset(spec, 'agenda-image', { x: 0, y: 0, w: 1.8, h: PPT_HEIGHT }); await addImage(slide, spec.imageUrl || 'https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&q=80&w=1000', imageRect); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'agenda-title', { x: 2.45, y: 0.72, w: 5.1, h: 0.75 }), { fontFace: 'Georgia', fontSize: 26, bold: true, color: '021D30', align: 'center', }); (spec.items || []).slice(0, 5).forEach((item, index) => { const { heading, description } = splitAgendaItem(item.text); const row = withOffset(spec, `agenda-item-${index}`, { x: 2.2, y: 1.6 + index * 0.68, w: 5.7, h: 0.55 }); slide.addShape('line', { x: row.x, y: row.y + row.h, w: row.w, h: 0, line: { color: 'CBD5D1', width: 1 }, }); addTextBox(slide, heading, { x: row.x, y: row.y, w: 3.8, h: 0.24 }, { fontFace: 'Arial', fontSize: 15, bold: true, color: '021D30', }); addTextBox(slide, description, { x: row.x, y: row.y + 0.24, w: 3.8, h: 0.22 }, { fontFace: 'Arial', fontSize: 9, color: '021D30', transparency: 35, }); addTextBox(slide, String(index + 1).padStart(2, '0'), { x: row.x + 4.55, y: row.y + 0.05, w: 0.4, h: 0.2 }, { fontFace: 'Arial', fontSize: 14, bold: true, color: '021D30', align: 'right', }); }); } function addGalerynThreeColumns(slide: PptxSlide, spec: SlideSpec) { addGalerynBackground(slide); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'columns-title', { x: 0.85, y: 0.7, w: 4.1, h: 0.55 }), { fontFace: 'Georgia', fontSize: 24, bold: true, color: '021D30', }); addTextBox(slide, '001 // Design', withOffset(spec, 'columns-tag', { x: 7.2, y: 0.78, w: 1.75, h: 0.22 }), { fontFace: 'Arial', fontSize: 10, color: '021D30', transparency: 35, align: 'right', }); const columnX = [0.85, 3.45, 6.05]; (spec.columns || []).slice(0, 3).forEach((column, index) => { const rect = withOffset(spec, `column-${index}`, { x: columnX[index], y: 1.85, w: 2.0, h: 2.3 }); addTextBox(slide, `0${index + 1}`, { x: rect.x, y: rect.y, w: 0.35, h: 0.18 }, { fontFace: 'Arial', fontSize: 10, color: '021D30', transparency: 35, }); addTextBox(slide, cleanText(column.heading), { x: rect.x, y: rect.y + 0.35, w: rect.w, h: 0.5 }, { fontFace: 'Arial', fontSize: 16, bold: true, color: '021D30', }); addTextBox(slide, cleanText(column.text), { x: rect.x, y: rect.y + 0.95, w: rect.w, h: 0.9 }, { fontFace: 'Arial', fontSize: 10, color: '021D30', transparency: 25, valign: 'top', }); }); } function addGalerynTitleAndText(slide: PptxSlide, spec: SlideSpec) { addGalerynBackground(slide); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title-and-text-title', { x: 0.8, y: 1.4, w: 4.5, h: 1.2 }), { fontFace: 'Georgia', fontSize: 26, bold: true, color: '021D30', valign: 'middle', }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'title-and-text-body-0', { x: 5.7, y: 2.2, w: 3.2, h: 1.2 }), { fontFace: 'Arial', fontSize: 12, color: '021D30', transparency: 20, valign: 'middle', }); } async function addGalerynImageAndText(slide: PptxSlide, spec: SlideSpec) { addGalerynBackground(slide); await addImage(slide, spec.imageUrl, withOffset(spec, 'image-card', { x: 0, y: 0, w: 5, h: PPT_HEIGHT })); addTextBox(slide, '001 Stories', { x: 0.72, y: 4.58, w: 1.45, h: 0.22 }, { fontFace: 'Arial', fontSize: 10, color: 'FFFFFF', fill: { color: '021D30', transparency: 20 }, margin: [3, 4, 3, 4], }); addRect(slide, { x: 5, y: 0, w: 5, h: PPT_HEIGHT }, 'F5F3EE', 'F5F3EE'); const textRect = withOffset(spec, 'image-text-content', { x: 5.7, y: 1.55, w: 3.4, h: 2.0 }); addTextBox(slide, cleanText(spec.title), { x: textRect.x, y: textRect.y, w: textRect.w, h: 0.65 }, { fontFace: 'Georgia', fontSize: 24, bold: true, color: '021D30', }); addTextBox(slide, bodyToParagraph(spec.body), { x: textRect.x, y: textRect.y + 0.9, w: textRect.w, h: 1.1 }, { fontFace: 'Arial', fontSize: 12, color: '021D30', transparency: 20, valign: 'top', }); } function addGalerynReferences(slide: PptxSlide, spec: SlideSpec) { addGalerynBackground(slide); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'references-title', { x: 0.95, y: 0.8, w: 3, h: 0.55 }), { fontFace: 'Georgia', fontSize: 24, bold: true, color: '021D30', }); (spec.items || []).slice(0, 6).forEach((item, index) => { const row = withOffset(spec, `reference-item-${index}`, { x: 1.0, y: 1.7 + index * 0.62, w: 7.1, h: 0.4 }); addTextBox(slide, `[${index + 1}]`, { x: row.x, y: row.y, w: 0.35, h: row.h }, { fontFace: 'Arial', fontSize: 10, color: '021D30', transparency: 50, }); addTextBox(slide, cleanText(item.text), { x: row.x + 0.45, y: row.y, w: row.w - 0.45, h: row.h }, { fontFace: 'Arial', fontSize: 12, color: '021D30', }); slide.addShape('line', { x: row.x + 0.45, y: row.y + row.h, w: row.w - 0.45, h: 0, line: { color: 'D6D3D1', width: 0.75 }, }); }); } function addGalerynThankYou(slide: PptxSlide, spec: SlideSpec) { slide.background = { color: '021D30' }; addTextBox(slide, cleanText(spec.title), withOffset(spec, 'thank-you-title', { x: 2.0, y: 1.95, w: 6, h: 0.9 }), { fontFace: 'Georgia', fontSize: 30, bold: true, color: 'FBF9F4', align: 'center', valign: 'middle', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'thank-you-subtitle', { x: 2.5, y: 2.95, w: 5, h: 0.3 }), { fontFace: 'Arial', fontSize: 12, color: 'FBF9F4', transparency: 35, align: 'center', }); addTextBox(slide, 'GALERYN CO. // 2026', { x: 3.15, y: 4.15, w: 3.7, h: 0.22 }, { fontFace: 'Arial', fontSize: 9, color: 'FBF9F4', charSpacing: 2, align: 'center', }); } function addNoisyTitleSubtitle(slide: PptxSlide, spec: SlideSpec) { addNoisyBackground(slide); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title', { x: 0.68, y: 0.52, w: 5.0, h: 1.45 }), { fontFace: 'Courier New', fontSize: 46, bold: true, color: 'FFFFFF', underline: true, valign: 'top', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'subtitle', { x: 0.88, y: 3.18, w: 7.35, h: 1.05 }), { fontFace: 'Courier New', fontSize: 19, color: 'FFFFFF', valign: 'top', }); } function addNoisyAgenda(slide: PptxSlide, spec: SlideSpec) { addNoisyBackground(slide); const agendaItems = (spec.items || []).slice(0, 6); const compactAgenda = agendaItems.length > 3; const titleFontSize = getResponsiveFontSize( spec.title, [ { maxLength: 18, size: 28 }, { maxLength: 28, size: 26 }, { maxLength: 42, size: 24 }, { maxLength: 60, size: 21 }, ], 28 ); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'agenda-title', { x: 0.72, y: 0.56, w: 2.8, h: 0.52 }), { fontFace: 'Courier New', fontSize: titleFontSize, bold: true, color: 'FFFFFF', underline: true, }); agendaItems.forEach((item, index) => { const column = index % 3; const row = Math.floor(index / 3); const itemFontSize = getResponsiveFontSize( item.text, [ { maxLength: 18, size: compactAgenda ? 14 : 17 }, { maxLength: 32, size: compactAgenda ? 13 : 16 }, { maxLength: 48, size: compactAgenda ? 12 : 15 }, { maxLength: 72, size: compactAgenda ? 11 : 13 }, ], compactAgenda ? 14 : 17 ); const rect = withOffset(spec, `agenda-item-${index}`, { x: 0.72 + column * 2.9, y: 1.42 + row * (compactAgenda ? 1.82 : 2.16), w: 2.12, h: compactAgenda ? 1.46 : 1.76, }); addTextBox(slide, String(index + 1).padStart(2, '0'), { x: rect.x, y: rect.y, w: rect.w, h: compactAgenda ? 0.7 : 0.82 }, { fontFace: 'Courier New', fontSize: compactAgenda ? 58 : 70, bold: true, color: 'FFFFFF', align: 'center', valign: 'middle', }); addRect( slide, { x: rect.x + 0.34, y: rect.y + (compactAgenda ? 0.8 : 0.95), w: rect.w - 0.68, h: compactAgenda ? 0.07 : 0.08, }, 'FF7A59', 'FF7A59' ); addTextBox(slide, cleanText(item.text), { x: rect.x, y: rect.y + (compactAgenda ? 0.98 : 1.14), w: rect.w, h: compactAgenda ? 0.44 : 0.54 }, { fontFace: 'Courier New', fontSize: itemFontSize, color: 'FFFFFF', align: 'center', valign: 'top', }); }); } function addNoisyThreeColumns(slide: PptxSlide, spec: SlideSpec) { slide.background = { color: 'FFFFFF' }; const titleFontSize = getResponsiveFontSize( spec.title, [ { maxLength: 18, size: 20 }, { maxLength: 28, size: 18 }, { maxLength: 42, size: 17 }, { maxLength: 60, size: 15 }, ], 20 ); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'columns-title', { x: 0.78, y: 0.68, w: 4.8, h: 0.48 }), { fontFace: 'Courier New', fontSize: titleFontSize, bold: true, color: '547BEE', underline: true, }); const columnX = [0.82, 3.48, 6.14]; (spec.columns || []).slice(0, 3).forEach((column, index) => { const headingFontSize = getResponsiveFontSize( column.heading, [ { maxLength: 14, size: 14 }, { maxLength: 22, size: 13 }, { maxLength: 30, size: 11.5 }, { maxLength: 42, size: 10.5 }, ], 14 ); const bodyFontSize = getResponsiveFontSize( column.text, [ { maxLength: 110, size: 10.5 }, { maxLength: 170, size: 9.8 }, { maxLength: 240, size: 9.2 }, { maxLength: 340, size: 8.6 }, ], 10.5 ); const rect = withOffset(spec, `column-${index}`, { x: columnX[index], y: 1.62, w: 2.05, h: 2.68 }); addTextBox(slide, cleanText(column.heading), { x: rect.x, y: rect.y, w: rect.w, h: 0.42 }, { fontFace: 'Courier New', fontSize: headingFontSize, bold: true, color: '547BEE', valign: 'top', }); addTextBox(slide, cleanText(column.text), { x: rect.x, y: rect.y + 0.54, w: rect.w, h: 1.7 }, { fontFace: 'Courier New', fontSize: bodyFontSize, color: '1F2937', valign: 'top', }); }); } function addNoisyTitleAndText(slide: PptxSlide, spec: SlideSpec) { slide.background = { color: 'FFFFFF' }; const titleFontSize = getResponsiveFontSize( spec.title, [ { maxLength: 18, size: 32 }, { maxLength: 28, size: 29 }, { maxLength: 42, size: 26 }, { maxLength: 60, size: 23 }, ], 32 ); const bodyFontSize = getResponsiveFontSize( bodyToParagraph(spec.body), [ { maxLength: 260, size: 17 }, { maxLength: 420, size: 15.5 }, { maxLength: 620, size: 14 }, { maxLength: 820, size: 12.5 }, { maxLength: 1100, size: 11.5 }, ], 17 ); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title-and-text-title', { x: 0.78, y: 0.68, w: 5.0, h: 0.56 }), { fontFace: 'Courier New', fontSize: titleFontSize, bold: true, color: '547BEE', underline: true, }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'title-and-text-body-0', { x: 0.92, y: 1.56, w: 6.9, h: 2.7 }), { fontFace: 'Courier New', fontSize: bodyFontSize, color: '1F2937', valign: 'top', }); } async function addNoisyImageAndText(slide: PptxSlide, spec: SlideSpec) { addNoisyBackground(slide, 'F2725C'); const titleFontSize = getResponsiveFontSize( spec.title, [ { maxLength: 18, size: 31 }, { maxLength: 28, size: 28 }, { maxLength: 40, size: 24 }, { maxLength: 56, size: 22 }, { maxLength: 76, size: 19 }, ], 31 ); const bodyFontSize = getResponsiveFontSize( bodyToParagraph(spec.body), [ { maxLength: 170, size: 16 }, { maxLength: 280, size: 15 }, { maxLength: 400, size: 13.5 }, { maxLength: 560, size: 12 }, { maxLength: 760, size: 10.8 }, ], 16 ); await addImage(slide, spec.imageUrl, withOffset(spec, 'image-card', { x: 0.72, y: 0.62, w: 4.25, h: 4.38 })); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'image-body', { x: 5.48, y: 0.9, w: 3.35, h: 0.82 }), { fontFace: 'Courier New', fontSize: titleFontSize, bold: true, color: 'FFFFFF', underline: true, valign: 'top', }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'image-body', { x: 5.48, y: 2.0, w: 3.2, h: 1.85 }), { fontFace: 'Courier New', fontSize: bodyFontSize, color: 'FFFFFF', valign: 'top', }); } function addNoisyReferences(slide: PptxSlide, spec: SlideSpec) { slide.background = { color: 'FFFFFF' }; addTextBox(slide, cleanText(spec.title), withOffset(spec, 'references-title', { x: 0.8, y: 0.75, w: 3.5, h: 0.55 }), { fontFace: 'Courier New', fontSize: 22, bold: true, color: '547BEE', underline: true, }); (spec.items || []).slice(0, 8).forEach((item, index) => { addTextBox(slide, `• ${cleanText(item.text)}`, withOffset(spec, `reference-item-${index}`, { x: 0.95, y: 1.65 + index * 0.45, w: 8.0, h: 0.3 }), { fontFace: 'Courier New', fontSize: 12, color: '1F2937', valign: 'middle', }); }); } function addNoisyThankYou(slide: PptxSlide, spec: SlideSpec) { addNoisyBackground(slide); addTextBox(slide, cleanText(spec.title), withOffset(spec, 'thank-you-title', { x: 1.8, y: 1.72, w: 6.4, h: 0.95 }), { fontFace: 'Courier New', fontSize: 63, bold: true, color: 'FFFFFF', align: 'center', valign: 'middle', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'thank-you-subtitle', { x: 3.0, y: 3.02, w: 4.0, h: 0.42 }), { fontFace: 'Courier New', fontSize: 27, color: 'FFFFFF', align: 'center', }); } async function addTemplateBackground(slide: PptxSlide, backgroundImage: string) { const cleanSrc = cleanText(backgroundImage); if (!cleanSrc) return; if (cleanSrc.startsWith('data:')) { slide.addImage({ data: cleanSrc, x: 0, y: 0, w: PPT_WIDTH, h: PPT_HEIGHT }); return; } slide.addImage({ path: cleanSrc, x: 0, y: 0, w: PPT_WIDTH, h: PPT_HEIGHT }); } function addNeoTextOverlay(slide: PptxSlide, spec: SlideSpec) { switch (spec.layout) { case 'title_subtitle': addTextBox(slide, upperText(spec.title), withOffset(spec, 'title', { x: 1.08, y: 1.08, w: 7.15, h: 1.35 }), { fontFace: 'Arial Black', fontSize: 31, bold: true, color: '000000', align: 'center', valign: 'middle', rotate: -1.2, }); addTextBox(slide, upperText(spec.subtitle), withOffset(spec, 'subtitle', { x: 1.8, y: 3.58, w: 6.45, h: 0.56 }), { fontFace: 'Arial', fontSize: 14, bold: true, color: '000000', align: 'center', valign: 'middle', }); return; case 'agenda': addTextBox(slide, upperText(spec.title), withOffset(spec, 'agenda-title', { x: 0.55, y: 0.45, w: 4.2, h: 0.65 }), { fontFace: 'Arial Black', fontSize: 25, bold: true, color: '000000', }); { const cards = [ { x: 0.72, y: 1.98, w: 2.05, h: 0.62 }, { x: 3.62, y: 1.98, w: 2.05, h: 0.62 }, { x: 6.52, y: 1.98, w: 2.05, h: 0.62 }, { x: 0.72, y: 3.82, w: 2.05, h: 0.62 }, { x: 3.62, y: 3.82, w: 2.05, h: 0.62 }, ]; (spec.items || []).slice(0, 5).forEach((item, index) => { addTextBox(slide, upperText(item.text), withOffset(spec, `agenda-card-${index}`, cards[index]), { fontFace: 'Arial Black', fontSize: 17, bold: true, color: '000000', align: 'left', valign: 'top', }); }); } return; case 'title_and_text': addTextBox(slide, upperText(spec.title), withOffset(spec, 'title-and-text-title', { x: 2.45, y: 1.18, w: 5.1, h: 0.5 }), { fontFace: 'Arial Black', fontSize: 26, bold: true, color: '000000', align: 'center', }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'title-and-text-body', { x: 1.62, y: 2.78, w: 6.75, h: 0.78 }), { fontFace: 'Arial', fontSize: 16, color: '000000', align: 'center', valign: 'middle', }); return; case 'three_columns': addTextBox(slide, upperText(spec.title), withOffset(spec, 'columns-title', { x: 0.75, y: 0.65, w: 3.3, h: 0.45 }), { fontFace: 'Arial Black', fontSize: 22, bold: true, color: '000000', }); { const columnX = [0.76, 3.56, 6.36]; (spec.columns || []).slice(0, 3).forEach((column, index) => { const key = `column-card-${index}`; addTextBox(slide, upperText(column.heading), withOffset(spec, key, { x: columnX[index] + 0.18, y: 2.5, w: 2.0, h: 0.55 }), { fontFace: 'Arial Black', fontSize: 15, bold: true, color: '000000', valign: 'top', }); addTextBox(slide, cleanText(column.text), withOffset(spec, key, { x: columnX[index] + 0.18, y: 3.42, w: 2.0, h: 1.0 }), { fontFace: 'Arial', fontSize: 10.5, color: '000000', valign: 'top', }); }); } return; case 'image_and_text': addTextBox(slide, upperText(spec.title), withOffset(spec, 'image-text', { x: 5.5, y: 1.4, w: 3.55, h: 0.7 }), { fontFace: 'Arial Black', fontSize: 24, bold: true, color: '000000', }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'image-text', { x: 5.5, y: 2.35, w: 3.5, h: 1.15 }), { fontFace: 'Arial', fontSize: 11, color: '000000', valign: 'top', }); return; case 'references': addTextBox(slide, upperText(spec.title), withOffset(spec, 'references-title', { x: 0.74, y: 0.56, w: 3.6, h: 0.3 }), { fontFace: 'Arial Black', fontSize: 18, bold: true, color: 'FFFFFF', }); (spec.items || []).slice(0, 6).forEach((item, index) => { addTextBox(slide, cleanText(item.text), withOffset(spec, `reference-item-${index}`, { x: 1.15, y: 1.5 + index * 0.62, w: 4.5, h: 0.24 }), { fontFace: 'Arial', fontSize: 12, color: '000000', }); }); return; case 'thank_you': addTextBox(slide, upperText(spec.title), withOffset(spec, 'thank-you-title', { x: 2.35, y: 1.6, w: 5.15, h: 1.35 }), { fontFace: 'Arial Black', fontSize: 30, bold: true, color: '000000', align: 'center', valign: 'middle', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'thank-you-subtitle', { x: 2.35, y: 4.08, w: 5.5, h: 0.22 }), { fontFace: 'Arial', fontSize: 13, bold: true, color: '000000', align: 'center', }); return; } } function addGalerynTextOverlay(slide: PptxSlide, spec: SlideSpec) { switch (spec.layout) { case 'title_subtitle': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title', { x: 1.15, y: 1.68, w: 7.7, h: 0.86 }), { fontFace: 'Georgia', fontSize: 42, bold: true, color: '021D30', align: 'center', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'subtitle', { x: 2.1, y: 3.72, w: 5.8, h: 0.5 }), { fontFace: 'Arial', fontSize: 11, color: '021D30', align: 'center', valign: 'middle', }); return; case 'agenda': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'agenda-title', { x: 2.45, y: 0.9, w: 5.0, h: 0.48 }), { fontFace: 'Georgia', fontSize: 25, bold: true, color: '021D30', align: 'center', }); (spec.items || []).slice(0, 5).forEach((item, index) => { const parsed = splitAgendaItem(item.text); addTextBox(slide, parsed.heading, withOffset(spec, `agenda-item-${index}`, { x: 2.25, y: 1.78 + index * 0.68, w: 3.6, h: 0.2 }), { fontFace: 'Arial', fontSize: 15, bold: true, color: '021D30', }); addTextBox(slide, parsed.description, withOffset(spec, `agenda-item-${index}`, { x: 2.25, y: 2.02 + index * 0.68, w: 3.6, h: 0.18 }), { fontFace: 'Arial', fontSize: 8.5, color: '021D30', transparency: 35, }); }); return; case 'three_columns': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'columns-title', { x: 0.85, y: 0.7, w: 4.1, h: 0.34 }), { fontFace: 'Georgia', fontSize: 24, bold: true, color: '021D30', }); { const columnX = [0.85, 3.45, 6.05]; (spec.columns || []).slice(0, 3).forEach((column, index) => { addTextBox(slide, cleanText(column.heading), withOffset(spec, `column-${index}`, { x: columnX[index], y: 2.2, w: 2.0, h: 0.32 }), { fontFace: 'Arial', fontSize: 16, bold: true, color: '021D30', }); addTextBox(slide, cleanText(column.text), withOffset(spec, `column-${index}`, { x: columnX[index], y: 2.82, w: 2.0, h: 0.62 }), { fontFace: 'Arial', fontSize: 10, color: '021D30', transparency: 25, valign: 'top', }); }); } return; case 'title_and_text': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title-and-text-title', { x: 0.86, y: 1.76, w: 4.1, h: 0.84 }), { fontFace: 'Georgia', fontSize: 26, bold: true, color: '021D30', valign: 'middle', }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'title-and-text-body-0', { x: 5.78, y: 2.58, w: 3.05, h: 0.66 }), { fontFace: 'Arial', fontSize: 12, color: '021D30', transparency: 20, valign: 'middle', }); return; case 'image_and_text': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'image-text-content', { x: 5.72, y: 1.68, w: 3.3, h: 0.44 }), { fontFace: 'Georgia', fontSize: 24, bold: true, color: '021D30', }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'image-text-content', { x: 5.72, y: 2.55, w: 3.2, h: 0.85 }), { fontFace: 'Arial', fontSize: 12, color: '021D30', transparency: 20, valign: 'top', }); return; case 'references': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'references-title', { x: 0.95, y: 0.82, w: 3.0, h: 0.34 }), { fontFace: 'Georgia', fontSize: 24, bold: true, color: '021D30', }); (spec.items || []).slice(0, 6).forEach((item, index) => { addTextBox(slide, cleanText(item.text), withOffset(spec, `reference-item-${index}`, { x: 1.42, y: 1.7 + index * 0.62, w: 6.55, h: 0.2 }), { fontFace: 'Arial', fontSize: 12, color: '021D30', }); }); return; case 'thank_you': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'thank-you-title', { x: 2.0, y: 2.08, w: 6.0, h: 0.45 }), { fontFace: 'Georgia', fontSize: 30, bold: true, color: 'FBF9F4', align: 'center', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'thank-you-subtitle', { x: 2.5, y: 3.05, w: 5.0, h: 0.16 }), { fontFace: 'Arial', fontSize: 12, color: 'FBF9F4', transparency: 35, align: 'center', }); return; } } function addNoisyTextOverlay(slide: PptxSlide, spec: SlideSpec) { switch (spec.layout) { case 'title_subtitle': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title', { x: 0.8, y: 1.2, w: 4.4, h: 0.38 }), { fontFace: 'Courier New', fontSize: 28, bold: true, color: 'FFFFFF', underline: true, }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'subtitle', { x: 0.82, y: 2.0, w: 5.8, h: 1.5 }), { fontFace: 'Courier New', fontSize: 16, color: 'FFFFFF', valign: 'top', }); return; case 'agenda': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'agenda-title', { x: 0.82, y: 0.82, w: 2.0, h: 0.34 }), { fontFace: 'Courier New', fontSize: 24, bold: true, color: 'FFFFFF', underline: true, }); (spec.items || []).slice(0, 6).forEach((item, index) => { const column = index % 3; const row = Math.floor(index / 3); addTextBox(slide, cleanText(item.text), withOffset(spec, `agenda-item-${index}`, { x: 0.98 + column * 2.8, y: 2.68 + row * 2.0, w: 2.02, h: 0.26, }), { fontFace: 'Courier New', fontSize: 13, color: 'FFFFFF', align: 'center', valign: 'middle', }); }); return; case 'three_columns': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'columns-title', { x: 0.8, y: 0.82, w: 4.6, h: 0.34 }), { fontFace: 'Courier New', fontSize: 22, bold: true, color: '547BEE', underline: true, }); { const columnX = [0.85, 3.55, 6.25]; (spec.columns || []).slice(0, 3).forEach((column, index) => { addTextBox(slide, cleanText(column.heading), withOffset(spec, `column-${index}`, { x: columnX[index], y: 1.72, w: 2.1, h: 0.28 }), { fontFace: 'Courier New', fontSize: 16, bold: true, color: '547BEE', }); addTextBox(slide, cleanText(column.text), withOffset(spec, `column-${index}`, { x: columnX[index], y: 2.34, w: 2.1, h: 0.95 }), { fontFace: 'Courier New', fontSize: 12, color: '1F2937', valign: 'top', }); }); } return; case 'title_and_text': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'title-and-text-title', { x: 0.82, y: 0.82, w: 4.5, h: 0.34 }), { fontFace: 'Courier New', fontSize: 22, bold: true, color: '547BEE', underline: true, }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'title-and-text-body-0', { x: 0.92, y: 1.76, w: 8.0, h: 1.9 }), { fontFace: 'Courier New', fontSize: 16, color: '1F2937', valign: 'top', }); return; case 'image_and_text': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'image-title', { x: 0.82, y: 0.82, w: 4.0, h: 0.34 }), { fontFace: 'Courier New', fontSize: 22, bold: true, color: 'FFFFFF', underline: true, }); addTextBox(slide, bodyToParagraph(spec.body), withOffset(spec, 'image-body', { x: 5.48, y: 1.72, w: 3.3, h: 1.9 }), { fontFace: 'Courier New', fontSize: 14, color: 'FFFFFF', valign: 'top', }); return; case 'references': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'references-title', { x: 0.82, y: 0.82, w: 3.4, h: 0.34 }), { fontFace: 'Courier New', fontSize: 22, bold: true, color: '547BEE', underline: true, }); (spec.items || []).slice(0, 8).forEach((item, index) => { addTextBox(slide, cleanText(item.text), withOffset(spec, `reference-item-${index}`, { x: 1.12, y: 1.68 + index * 0.45, w: 7.65, h: 0.2 }), { fontFace: 'Courier New', fontSize: 12, color: '1F2937', valign: 'middle', }); }); return; case 'thank_you': addTextBox(slide, cleanText(spec.title), withOffset(spec, 'thank-you-title', { x: 2.1, y: 2.18, w: 5.8, h: 0.45 }), { fontFace: 'Courier New', fontSize: 30, bold: true, color: 'FFFFFF', align: 'center', }); addTextBox(slide, cleanText(spec.subtitle), withOffset(spec, 'thank-you-subtitle', { x: 2.8, y: 3.06, w: 4.4, h: 0.22 }), { fontFace: 'Courier New', fontSize: 15, color: 'FFFFFF', align: 'center', }); return; } } async function addTemplateSlide(slide: PptxSlide, spec: SlideSpec) { const template = getTemplateById(spec.templateId); if (!template) { slide.background = { color: 'FFFFFF' }; addTextBox(slide, cleanText(spec.title) || 'Slide', { x: 1, y: 1, w: 8, h: 1 }, { fontFace: 'Arial', fontSize: 28, color: '000000', }); return; } if (spec.templateId === 'neobrutalism') { switch (spec.layout) { case 'title_subtitle': addNeoTitleSubtitle(slide, spec); return; case 'agenda': addNeoAgenda(slide, spec); return; case 'title_and_text': addNeoTitleAndText(slide, spec); return; case 'three_columns': addNeoThreeColumns(slide, spec); return; case 'image_and_text': await addNeoImageAndText(slide, spec); return; case 'references': addNeoReferences(slide, spec); return; case 'thank_you': addNeoThankYou(slide, spec); return; } } if (spec.templateId === 'galeryn') { switch (spec.layout) { case 'title_subtitle': addGalerynTitleSubtitle(slide, spec); return; case 'agenda': await addGalerynAgenda(slide, spec); return; case 'title_and_text': addGalerynTitleAndText(slide, spec); return; case 'three_columns': addGalerynThreeColumns(slide, spec); return; case 'image_and_text': await addGalerynImageAndText(slide, spec); return; case 'references': addGalerynReferences(slide, spec); return; case 'thank_you': addGalerynThankYou(slide, spec); return; } } switch (spec.layout) { case 'title_subtitle': addNoisyTitleSubtitle(slide, spec); return; case 'agenda': addNoisyAgenda(slide, spec); return; case 'title_and_text': addNoisyTitleAndText(slide, spec); return; case 'three_columns': addNoisyThreeColumns(slide, spec); return; case 'image_and_text': await addNoisyImageAndText(slide, spec); return; case 'references': addNoisyReferences(slide, spec); return; case 'thank_you': addNoisyThankYou(slide, spec); return; } } function addTemplateTextOverlay(slide: PptxSlide, spec: SlideSpec) { if (spec.templateId === 'neobrutalism') { addNeoTextOverlay(slide, spec); return; } if (spec.templateId === 'galeryn') { addGalerynTextOverlay(slide, spec); return; } addNoisyTextOverlay(slide, spec); } export async function exportEditablePptx({ slides, slideSpecs, currentTheme, presentationTitle, }: EditablePptxExportParams) { const pptx = new PptxGenJS(); pptx.defineLayout({ name: 'LAYOUT_16x9', width: PPT_WIDTH, height: PPT_HEIGHT }); pptx.layout = 'LAYOUT_16x9'; pptx.author = 'AI PowerPoint Generator'; pptx.company = 'AI Generated'; pptx.subject = 'Editable presentation export'; pptx.title = presentationTitle || 'Presentation'; if (TEMPLATE_THEMES.has(currentTheme) && slideSpecs.length > 0) { for (const spec of slideSpecs) { const slide = pptx.addSlide(); await addTemplateSlide(slide, spec); } } else { for (const slideModel of slides) { const slide = pptx.addSlide(); await addLegacySlide(slide, slideModel, currentTheme); } } await pptx.writeFile({ fileName: `${sanitizeFileName(presentationTitle)}.pptx` }); }