Spaces:
Running
Running
| /* | |
| * 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<string, unknown> = {}) { | |
| 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<string, unknown> = {} | |
| ) { | |
| 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<ImageProps, 'x' | 'y' | 'w' | 'h' | 'sizing'> { | |
| 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<ImageProps> = {} | |
| ): Promise<ImageProps | null> { | |
| 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<string>((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<ImageProps> = {} | |
| ) { | |
| 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` }); | |
| } | |