Powerpoint_AI / lib /editable-pptx-export.ts
Reubencf's picture
some PPT slides fix
063525e
/*
* 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` });
}