@bendyline/squisq-formats 2.7.6 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -62,11 +62,12 @@ import {
62
62
  fontAwesomeGlyph
63
63
  } from "./chunk-LBXBRFBZ.js";
64
64
  import {
65
- extToMime
65
+ extToMime,
66
+ readImageDimensions
66
67
  } from "./chunk-JZR32FON.js";
67
68
 
68
69
  // src/pptx/export.ts
69
- import { resolveFontFamily, VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
70
+ import { VIEWPORT_PRESETS } from "@bendyline/squisq/schemas";
70
71
  import {
71
72
  buildPreviewDoc,
72
73
  createTemplateContext,
@@ -74,13 +75,14 @@ import {
74
75
  expandCoverBlock,
75
76
  expandDocBlocks,
76
77
  flattenRenderableBlocks,
78
+ isGeometryBoundTemplate,
77
79
  isTemplateBlock,
78
80
  resolvePersistentLayers,
79
81
  resolveThemeForDoc
80
82
  } from "@bendyline/squisq/doc";
81
- import { splitIconMarkers } from "@bendyline/squisq/icon-marker";
83
+ import { splitIconMarkers as splitIconMarkers3 } from "@bendyline/squisq/icon-marker";
82
84
  import {
83
- isSvgImageSource,
85
+ isSvgImageSource as isSvgImageSource2,
84
86
  parseVideoEmbedUrl,
85
87
  readFrontmatterThemeId
86
88
  } from "@bendyline/squisq/markdown";
@@ -179,6 +181,1095 @@ var DEFAULT_TITLE_SIZE = 4400;
179
181
  var DEFAULT_CODE_SIZE = 1400;
180
182
  var HYPERLINK_COLOR = "0563C1";
181
183
 
184
+ // src/pptx/geometry.ts
185
+ var EMU_PER_CANVAS_PX = SLIDE_WIDTH / 1920;
186
+ function resolvePositionValue(value, dimension) {
187
+ if (typeof value === "number") return value;
188
+ if (typeof value === "string") {
189
+ const trimmed = value.trim();
190
+ if (trimmed.endsWith("%")) {
191
+ const percent = Number.parseFloat(trimmed);
192
+ if (Number.isFinite(percent)) return percent / 100 * dimension;
193
+ }
194
+ const numeric = Number.parseFloat(trimmed);
195
+ if (Number.isFinite(numeric)) return numeric;
196
+ }
197
+ return 0;
198
+ }
199
+ function resolveLayerRect(position, defaultWidth, defaultHeight) {
200
+ const width = Math.max(
201
+ 1,
202
+ position.width != null ? resolvePositionValue(position.width, 1920) : defaultWidth
203
+ );
204
+ const height = Math.max(
205
+ 1,
206
+ position.height != null ? resolvePositionValue(position.height, 1080) : defaultHeight
207
+ );
208
+ let x = resolvePositionValue(position.x, 1920);
209
+ let y = resolvePositionValue(position.y, 1080);
210
+ const anchor = position.anchor ?? "top-left";
211
+ if (anchor === "center") {
212
+ x -= width / 2;
213
+ y -= height / 2;
214
+ } else {
215
+ if (anchor.endsWith("right")) x -= width;
216
+ if (anchor.startsWith("bottom")) y -= height;
217
+ }
218
+ const clippedX = Math.min(1919, Math.max(0, x));
219
+ const clippedY = Math.min(1079, Math.max(0, y));
220
+ const clippedWidth = Math.max(1, Math.min(width - (clippedX - x), 1920 - clippedX));
221
+ const clippedHeight = Math.max(1, Math.min(height - (clippedY - y), 1080 - clippedY));
222
+ return { x: clippedX, y: clippedY, width: clippedWidth, height: clippedHeight };
223
+ }
224
+ function toEmuRect(rect) {
225
+ return {
226
+ x: Math.round(rect.x / 1920 * SLIDE_WIDTH),
227
+ y: Math.round(rect.y / 1080 * SLIDE_HEIGHT),
228
+ width: Math.max(1, Math.round(rect.width / 1920 * SLIDE_WIDTH)),
229
+ height: Math.max(1, Math.round(rect.height / 1080 * SLIDE_HEIGHT))
230
+ };
231
+ }
232
+
233
+ // src/pptx/drawingXml.ts
234
+ import { resolveFontFamily } from "@bendyline/squisq/schemas";
235
+
236
+ // src/pptx/cssPaint.ts
237
+ function parseCssColor(color, fallback) {
238
+ if (!color) return { hex: toOoxmlHex(fallback), alpha: 1e5 };
239
+ const trimmed = color.trim();
240
+ if (trimmed === "transparent" || trimmed === "none") return { hex: "000000", alpha: 0 };
241
+ const hexMatch = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(trimmed);
242
+ if (hexMatch) {
243
+ const raw = hexMatch[1];
244
+ const expanded = raw.length === 3 ? raw.split("").map((char) => char + char).join("") : raw;
245
+ const alpha = expanded.length === 8 ? Math.round(Number.parseInt(expanded.slice(6, 8), 16) / 255 * 1e5) : 1e5;
246
+ return { hex: expanded.slice(0, 6).toUpperCase(), alpha };
247
+ }
248
+ const rgbMatch = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(trimmed);
249
+ if (rgbMatch) {
250
+ const channel = (value) => Math.max(0, Math.min(255, Math.round(Number(value)))).toString(16).padStart(2, "0");
251
+ const alpha = rgbMatch[4] ? Math.round(Math.max(0, Math.min(1, Number(rgbMatch[4]))) * 1e5) : 1e5;
252
+ return {
253
+ hex: `${channel(rgbMatch[1])}${channel(rgbMatch[2])}${channel(rgbMatch[3])}`.toUpperCase(),
254
+ alpha
255
+ };
256
+ }
257
+ const named = {
258
+ black: "000000",
259
+ white: "FFFFFF",
260
+ red: "FF0000",
261
+ blue: "0000FF",
262
+ green: "008000",
263
+ gray: "808080",
264
+ grey: "808080"
265
+ };
266
+ return { hex: named[trimmed.toLowerCase()] ?? toOoxmlHex(fallback), alpha: 1e5 };
267
+ }
268
+ function splitTopLevel(value) {
269
+ const parts = [];
270
+ let depth = 0;
271
+ let current = "";
272
+ for (const char of value) {
273
+ if (char === "(") depth++;
274
+ if (char === ")") depth--;
275
+ if (char === "," && depth === 0) {
276
+ parts.push(current.trim());
277
+ current = "";
278
+ continue;
279
+ }
280
+ current += char;
281
+ }
282
+ if (current.trim()) parts.push(current.trim());
283
+ return parts;
284
+ }
285
+ var SIDE_ANGLES = {
286
+ top: 0,
287
+ right: 90,
288
+ bottom: 180,
289
+ left: 270,
290
+ "top right": 45,
291
+ "right top": 45,
292
+ "bottom right": 135,
293
+ "right bottom": 135,
294
+ "bottom left": 225,
295
+ "left bottom": 225,
296
+ "top left": 315,
297
+ "left top": 315
298
+ };
299
+ function parseAngle(token) {
300
+ const side = /^to\s+(.+)$/i.exec(token);
301
+ if (side) return SIDE_ANGLES[side[1].trim().toLowerCase().replace(/\s+/g, " ")] ?? null;
302
+ const match = /^(-?[\d.]+)(deg|turn|rad|grad)$/i.exec(token);
303
+ if (!match) return null;
304
+ const value = Number(match[1]);
305
+ const unit = match[2].toLowerCase();
306
+ if (!Number.isFinite(value)) return null;
307
+ if (unit === "turn") return value * 360;
308
+ if (unit === "rad") return value * 180 / Math.PI;
309
+ if (unit === "grad") return value * 0.9;
310
+ return value;
311
+ }
312
+ var POSITION_KEYWORDS = {
313
+ left: 0,
314
+ top: 0,
315
+ center: 0.5,
316
+ right: 1,
317
+ bottom: 1
318
+ };
319
+ function parseRadialCenter(token) {
320
+ const at = /\bat\s+(.+)$/i.exec(token);
321
+ if (!at) return { x: 0.5, y: 0.5 };
322
+ const parts = at[1].trim().toLowerCase().split(/\s+/);
323
+ const value = (part) => {
324
+ if (part === void 0) return void 0;
325
+ if (part in POSITION_KEYWORDS) return POSITION_KEYWORDS[part];
326
+ const percent = /^(-?[\d.]+)%$/.exec(part);
327
+ return percent ? Number(percent[1]) / 100 : void 0;
328
+ };
329
+ if (parts.length === 1) {
330
+ const only = parts[0];
331
+ if (only === "top" || only === "bottom") return { x: 0.5, y: value(only) ?? 0.5 };
332
+ return { x: value(only) ?? 0.5, y: 0.5 };
333
+ }
334
+ const [first, second] = parts;
335
+ if (first === "top" || first === "bottom" || second === "left" || second === "right") {
336
+ return { x: value(second) ?? 0.5, y: value(first) ?? 0.5 };
337
+ }
338
+ return { x: value(first) ?? 0.5, y: value(second) ?? 0.5 };
339
+ }
340
+ var CSS_COLOR = /^(#[0-9a-f]{3}|#[0-9a-f]{6}|#[0-9a-f]{8}|rgba?\([^)]*\)|transparent|black|white|red|blue|green|gr[ae]y)$/i;
341
+ function parseStop(token) {
342
+ const positions = [];
343
+ let rest = token.trim();
344
+ for (; ; ) {
345
+ const match = /\s+(-?[\d.]+)%$/.exec(rest);
346
+ if (!match) break;
347
+ positions.unshift(Number(match[1]) / 100);
348
+ rest = rest.slice(0, match.index).trim();
349
+ }
350
+ if (!CSS_COLOR.test(rest)) return null;
351
+ const color = parseCssColor(rest, "000000");
352
+ if (positions.length === 0) return [{ color }];
353
+ return positions.slice(0, 2).map((position) => ({ color: { ...color }, position }));
354
+ }
355
+ function resolveStopPositions(raw) {
356
+ const positions = raw.map((stop) => stop.position);
357
+ if (positions[0] === void 0) positions[0] = 0;
358
+ if (positions[positions.length - 1] === void 0) positions[positions.length - 1] = 1;
359
+ let previous = 0;
360
+ for (let index = 0; index < positions.length; index++) {
361
+ const value = positions[index];
362
+ if (value === void 0) continue;
363
+ positions[index] = Math.max(previous, value);
364
+ previous = positions[index];
365
+ }
366
+ for (let index = 1; index < positions.length; index++) {
367
+ if (positions[index] !== void 0) continue;
368
+ const start = index - 1;
369
+ let end = index;
370
+ while (positions[end] === void 0) end++;
371
+ const from = positions[start];
372
+ const to = positions[end];
373
+ for (let gap = index; gap < end; gap++) {
374
+ positions[gap] = from + (to - from) * (gap - start) / (end - start);
375
+ }
376
+ }
377
+ return raw.map((stop, index) => ({
378
+ color: stop.color,
379
+ position: Math.max(0, Math.min(1, positions[index]))
380
+ }));
381
+ }
382
+ function borrowTransparentHues(stops) {
383
+ return stops.map((stop, index) => {
384
+ if (stop.color.alpha > 0) return stop;
385
+ const neighbour = stops.slice(index + 1).find((candidate) => candidate.color.alpha > 0) ?? [...stops.slice(0, index)].reverse().find((candidate) => candidate.color.alpha > 0);
386
+ return neighbour ? { ...stop, color: { hex: neighbour.color.hex, alpha: 0 } } : stop;
387
+ });
388
+ }
389
+ function parseCssGradient(value) {
390
+ const match = /(linear|radial)-gradient\(\s*([\s\S]*)\)\s*$/i.exec(value.trim());
391
+ if (!match) return null;
392
+ const kind = match[1].toLowerCase();
393
+ const args = splitTopLevel(match[2]);
394
+ if (args.length === 0) return null;
395
+ const first = args[0];
396
+ let angle = 180;
397
+ let center = { x: 0.5, y: 0.5 };
398
+ let stopTokens = args;
399
+ if (kind === "linear") {
400
+ const parsed = parseAngle(first);
401
+ if (parsed !== null) {
402
+ angle = parsed;
403
+ stopTokens = args.slice(1);
404
+ }
405
+ } else if (/^(circle|ellipse|closest-|farthest-|at\s)/i.test(first) || /\bat\s/i.test(first)) {
406
+ center = parseRadialCenter(first);
407
+ stopTokens = args.slice(1);
408
+ }
409
+ const raw = stopTokens.flatMap((token) => parseStop(token) ?? []);
410
+ if (raw.length < 2) return null;
411
+ const stops = borrowTransparentHues(resolveStopPositions(raw));
412
+ return kind === "linear" ? { kind, angle, stops } : { kind, centerX: center.x, centerY: center.y, stops };
413
+ }
414
+ function isOpaqueGradient(gradient) {
415
+ return gradient.stops.every((stop) => stop.color.alpha >= 1e5);
416
+ }
417
+ function cssGradientFillXml(gradient, opacity = 1) {
418
+ const factor = Math.max(0, Math.min(1, opacity));
419
+ const stops = gradient.stops.map(({ color, position }) => {
420
+ const alpha = Math.round(color.alpha * factor);
421
+ return `<a:gs pos="${Math.round(position * 1e5)}"><a:srgbClr val="${color.hex}">` + (alpha < 1e5 ? `<a:alpha val="${alpha}"/>` : "") + `</a:srgbClr></a:gs>`;
422
+ }).join("");
423
+ if (gradient.kind === "linear") {
424
+ const angle = Math.round(((gradient.angle - 90) % 360 + 360) % 360 * 6e4);
425
+ return `<a:gradFill rotWithShape="1"><a:gsLst>${stops}</a:gsLst><a:lin ang="${angle}" scaled="0"/></a:gradFill>`;
426
+ }
427
+ const edge = (value) => Math.round(Math.max(0, Math.min(1, value)) * 1e5);
428
+ return `<a:gradFill rotWithShape="1"><a:gsLst>${stops}</a:gsLst><a:path path="circle"><a:fillToRect l="${edge(gradient.centerX)}" t="${edge(gradient.centerY)}" r="${edge(1 - gradient.centerX)}" b="${edge(1 - gradient.centerY)}"/></a:path></a:gradFill>`;
429
+ }
430
+ function blendOver(dst, src) {
431
+ const alpha = Math.max(0, Math.min(1, src.alpha / 1e5));
432
+ const channel = (offset) => {
433
+ const under = Number.parseInt(dst.slice(offset, offset + 2), 16);
434
+ const over = Number.parseInt(src.hex.slice(offset, offset + 2), 16);
435
+ return Math.round(over * alpha + under * (1 - alpha)).toString(16).padStart(2, "0");
436
+ };
437
+ return `${channel(0)}${channel(2)}${channel(4)}`.toUpperCase();
438
+ }
439
+ function cssGradientToString(gradient) {
440
+ const stops = gradient.stops.map(({ color, position }) => {
441
+ const alpha = color.alpha / 1e5;
442
+ const value = alpha >= 1 ? `#${color.hex}` : `rgba(${Number.parseInt(color.hex.slice(0, 2), 16)}, ${Number.parseInt(color.hex.slice(2, 4), 16)}, ${Number.parseInt(color.hex.slice(4, 6), 16)}, ${alpha})`;
443
+ return `${value} ${+(position * 100).toFixed(3)}%`;
444
+ }).join(", ");
445
+ return gradient.kind === "linear" ? `linear-gradient(${gradient.angle}deg, ${stops})` : `radial-gradient(ellipse at ${+(gradient.centerX * 100).toFixed(3)}% ${+(gradient.centerY * 100).toFixed(3)}%, ${stops})`;
446
+ }
447
+
448
+ // src/pptx/drawingXml.ts
449
+ function officeTypeface(family, fallback) {
450
+ const resolved = resolveFontFamily(family, fallback);
451
+ const first = resolved.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
452
+ return first || fallback;
453
+ }
454
+ function solidColorXml(color) {
455
+ const alpha = color.alpha < 1e5 ? `<a:alpha val="${color.alpha}"/>` : "";
456
+ return `<a:solidFill><a:srgbClr val="${color.hex}">${alpha}</a:srgbClr></a:solidFill>`;
457
+ }
458
+ function fillXml(fill, opacity, gradient) {
459
+ if (gradient) return gradientFillXml(gradient.from, gradient.to, gradient.angle ?? 0, opacity);
460
+ if (!fill || fill === "none" || fill === "transparent") return "<a:noFill/>";
461
+ if (fill.includes("gradient(")) {
462
+ const parsed = parseCssGradient(fill);
463
+ if (parsed) return cssGradientFillXml(parsed, opacity ?? 1);
464
+ const colors = fill.match(/#[0-9a-f]{3,8}|rgba?\([^)]*\)/gi) ?? [];
465
+ if (colors.length >= 2) {
466
+ const angle = Number.parseFloat(
467
+ /(?:linear-gradient\()\s*([\d.-]+)deg/i.exec(fill)?.[1] ?? "0"
468
+ );
469
+ return gradientFillXml(colors[0], colors[colors.length - 1], angle, opacity);
470
+ }
471
+ }
472
+ const color = parseCssColor(fill, "000000");
473
+ if (opacity !== void 0)
474
+ color.alpha = Math.round(color.alpha * Math.max(0, Math.min(1, opacity)));
475
+ return solidColorXml(color);
476
+ }
477
+ function gradientFillXml(from, to, angle, opacity) {
478
+ const first = parseCssColor(from, "000000");
479
+ const second = parseCssColor(to, first.hex);
480
+ if (opacity !== void 0) {
481
+ const factor = Math.max(0, Math.min(1, opacity));
482
+ first.alpha = Math.round(first.alpha * factor);
483
+ second.alpha = Math.round(second.alpha * factor);
484
+ }
485
+ const ooxmlAngle = Math.round(((90 - angle) % 360 + 360) % 360 * 6e4);
486
+ return `<a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:srgbClr val="${first.hex}">${first.alpha < 1e5 ? `<a:alpha val="${first.alpha}"/>` : ""}</a:srgbClr></a:gs><a:gs pos="100000"><a:srgbClr val="${second.hex}">${second.alpha < 1e5 ? `<a:alpha val="${second.alpha}"/>` : ""}</a:srgbClr></a:gs></a:gsLst><a:lin ang="${ooxmlAngle}" scaled="1"/></a:gradFill>`;
487
+ }
488
+ function lineXml(color, widthPx, borderStyle) {
489
+ if (!color || !widthPx || widthPx <= 0) return "<a:ln><a:noFill/></a:ln>";
490
+ const width = Math.max(1, Math.round(widthPx * EMU_PER_CANVAS_PX));
491
+ return `<a:ln w="${width}">${solidColorXml(parseCssColor(color, "000000"))}${dashXml(borderStyle)}</a:ln>`;
492
+ }
493
+ function dashXml(style, dasharray) {
494
+ if (style === "dashed" || dasharray && dasharray !== "none") return '<a:prstDash val="dash"/>';
495
+ if (style === "dotted") return '<a:prstDash val="dot"/>';
496
+ return "";
497
+ }
498
+ function patternFillXml(pattern, ground) {
499
+ const size = pattern.size ?? 24;
500
+ const preset = pattern.kind === "dots" ? "pct5" : pattern.kind === "grid" ? size >= 32 ? "lgGrid" : "smGrid" : size >= 32 ? "wdUpDiag" : "ltUpDiag";
501
+ const ink = parseCssColor(pattern.color, "FFFFFF");
502
+ ink.alpha = Math.round(ink.alpha * Math.max(0, Math.min(1, pattern.opacity ?? 1)));
503
+ const alpha = ink.alpha < 1e5 ? `<a:alpha val="${ink.alpha}"/>` : "";
504
+ return `<a:pattFill prst="${preset}"><a:fgClr><a:srgbClr val="${ink.hex}">${alpha}</a:srgbClr></a:fgClr>` + (ground ? `<a:bgClr><a:srgbClr val="${parseCssColor(ground, "000000").hex}"/></a:bgClr>` : `<a:bgClr><a:srgbClr val="${ink.hex}"><a:alpha val="0"/></a:srgbClr></a:bgClr>`) + `</a:pattFill>`;
505
+ }
506
+ function buildLayerShape(layer, shapeId) {
507
+ const rect = toEmuRect(resolveLayerRect(layer.position, 100, 100));
508
+ const geometry = layer.content.shape === "circle" ? "ellipse" : layer.content.shape === "line" ? "line" : layer.content.borderRadius ? "roundRect" : "rect";
509
+ const fill = layer.content.shape === "line" ? "<a:noFill/>" : layer.content.pattern ? patternFillXml(layer.content.pattern) : fillXml(layer.content.fill, layer.content.fillOpacity, layer.content.gradient);
510
+ const stroke = lineXml(
511
+ layer.content.stroke ?? (layer.content.shape === "line" ? "#ffffff" : void 0),
512
+ layer.content.strokeWidth ?? (layer.content.shape === "line" ? 2 : void 0),
513
+ layer.content.borderStyle
514
+ );
515
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${shapeId}" name="${escapeXml(layer.id)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm><a:prstGeom prst="${geometry}"><a:avLst/></a:prstGeom>` + fill + stroke + `</p:spPr></p:sp>`;
516
+ }
517
+
518
+ // src/pptx/backdrop.ts
519
+ import { TEMPLATE_METADATA } from "@bendyline/squisq/doc";
520
+ import { isSvgImageSource } from "@bendyline/squisq/markdown";
521
+ var FULL_BLEED_TOLERANCE_PX = 1;
522
+ function coversCanvas(layer) {
523
+ if (layer.position.width == null || layer.position.height == null) return false;
524
+ const rect = resolveLayerRect(layer.position, 1920, 1080);
525
+ return rect.x <= FULL_BLEED_TOLERANCE_PX && rect.y <= FULL_BLEED_TOLERANCE_PX && rect.x + rect.width >= 1920 - FULL_BLEED_TOLERANCE_PX && rect.y + rect.height >= 1080 - FULL_BLEED_TOLERANCE_PX;
526
+ }
527
+ function isPaintRect(layer) {
528
+ if (layer.type !== "shape" || layer.content.shape !== "rect") return false;
529
+ if (layer.content.borderRadius) return false;
530
+ if (layer.content.stroke && (layer.content.strokeWidth ?? 1) > 0) return false;
531
+ return coversCanvas(layer);
532
+ }
533
+ function isPhotoBackdrop(layer) {
534
+ return layer.type === "image" && (!layer.content.src || layer.content.fit === "cover" || layer.content.fit === "fill") && !(layer.content.src && isSvgImageSource(layer.content.src)) && coversCanvas(layer);
535
+ }
536
+ function isOpaqueColor(color, opacity) {
537
+ if (!color || color === "none" || color === "transparent") return false;
538
+ return parseCssColor(color, "000000").alpha >= 1e5 && (opacity ?? 1) >= 1;
539
+ }
540
+ function isOpaquePaint(layer) {
541
+ if (layer.type === "image") return !!layer.content.src;
542
+ const { fill, fillOpacity, gradient, pattern, filter } = layer.content;
543
+ if (pattern || filter) return false;
544
+ if (gradient) {
545
+ return isOpaqueColor(gradient.from, fillOpacity) && isOpaqueColor(gradient.to, fillOpacity);
546
+ }
547
+ if (fill?.includes("gradient(")) {
548
+ const parsed = parseCssGradient(fill);
549
+ return !!parsed && isOpaqueGradient(parsed) && (fillOpacity ?? 1) >= 1;
550
+ }
551
+ return isOpaqueColor(fill, fillOpacity);
552
+ }
553
+ function isDrawable(layer) {
554
+ return layer.content.filter?.type !== "noise";
555
+ }
556
+ var CLEAR = { hex: "000000", alpha: 0 };
557
+ function layerPaint(layer) {
558
+ const { fill, fillOpacity, gradient, pattern } = layer.content;
559
+ const factor = Math.max(0, Math.min(1, fillOpacity ?? 1));
560
+ const fade = (color) => ({
561
+ hex: color.hex,
562
+ alpha: Math.round(color.alpha * factor)
563
+ });
564
+ if (pattern) return { kind: "pattern", pattern };
565
+ if (gradient) {
566
+ return {
567
+ kind: "gradient",
568
+ gradient: {
569
+ kind: "linear",
570
+ angle: 180 - (gradient.angle ?? 0),
571
+ stops: [
572
+ { position: 0, color: fade(parseCssColor(gradient.from, "000000")) },
573
+ { position: 1, color: fade(parseCssColor(gradient.to, "000000")) }
574
+ ]
575
+ }
576
+ };
577
+ }
578
+ if (!fill || fill === "none" || fill === "transparent") return { kind: "solid", color: CLEAR };
579
+ if (fill.includes("gradient(")) {
580
+ const parsed = parseCssGradient(fill);
581
+ if (!parsed) return null;
582
+ const stops = parsed.stops.map((stop) => ({ ...stop, color: fade(stop.color) }));
583
+ return { kind: "gradient", gradient: { ...parsed, stops } };
584
+ }
585
+ return { kind: "solid", color: fade(parseCssColor(fill, "000000")) };
586
+ }
587
+ function compositeBackdrop(base, overlays) {
588
+ const paints = [base, ...overlays].map(layerPaint);
589
+ if (paints.some((paint) => paint === null)) return null;
590
+ const stack = paints;
591
+ const varying = stack.filter((paint) => paint.kind !== "solid");
592
+ if (varying.length > 1 || stack[0].kind === "pattern") return null;
593
+ const uniform = (paint) => paint.kind === "solid" ? paint.color : CLEAR;
594
+ const fold = (sample) => stack.reduce((under, paint) => blendOver(under, sample(paint)), "000000");
595
+ const only = varying[0];
596
+ let content;
597
+ if (!only) {
598
+ content = { shape: "rect", fill: `#${fold(uniform)}` };
599
+ } else if (only.kind === "gradient") {
600
+ const stops = only.gradient.stops.map((stop) => ({
601
+ position: stop.position,
602
+ color: {
603
+ hex: fold((paint) => paint === only ? stop.color : uniform(paint)),
604
+ alpha: 1e5
605
+ }
606
+ }));
607
+ content = { shape: "rect", fill: cssGradientToString({ ...only.gradient, stops }) };
608
+ } else {
609
+ const ink = parseCssColor(only.pattern.color, "FFFFFF");
610
+ ink.alpha = Math.round(ink.alpha * Math.max(0, Math.min(1, only.pattern.opacity ?? 1)));
611
+ content = {
612
+ shape: "rect",
613
+ // The ground under the motif, and the motif's ink over that ground.
614
+ fill: `#${fold(uniform)}`,
615
+ pattern: {
616
+ ...only.pattern,
617
+ color: `#${fold((paint) => paint === only ? ink : uniform(paint))}`,
618
+ opacity: 1
619
+ }
620
+ };
621
+ }
622
+ return { ...base, content };
623
+ }
624
+ function splitSlideBackdrop(layers) {
625
+ let start = 0;
626
+ while (start < layers.length && (isPaintRect(layers[start]) || isPhotoBackdrop(layers[start]))) {
627
+ start++;
628
+ }
629
+ let end = layers.length;
630
+ while (end > start) {
631
+ const layer = layers[end - 1];
632
+ if (!isPaintRect(layer) || isOpaquePaint(layer)) break;
633
+ end--;
634
+ }
635
+ const under = layers.slice(0, start);
636
+ let baseIndex = -1;
637
+ for (let index = under.length - 1; index >= 0; index--) {
638
+ if (isOpaquePaint(under[index])) {
639
+ baseIndex = index;
640
+ break;
641
+ }
642
+ }
643
+ const overlays = [...under.slice(baseIndex + 1), ...layers.slice(end)].filter(
644
+ (layer) => layer.type === "shape" && isDrawable(layer)
645
+ );
646
+ return {
647
+ base: baseIndex >= 0 ? under[baseIndex] : null,
648
+ overlays,
649
+ content: layers.slice(start, end)
650
+ };
651
+ }
652
+ function compositeAsFarAsPossible(base, overlays) {
653
+ if (base?.type !== "shape") return { base, overlays };
654
+ for (let count = overlays.length; count > 0; count--) {
655
+ const flat = compositeBackdrop(base, overlays.slice(0, count));
656
+ if (flat) return { base: flat, overlays: overlays.slice(count) };
657
+ }
658
+ return { base, overlays };
659
+ }
660
+ function paintSignature(layer) {
661
+ const { id: _id, animation: _animation, ...rest } = layer;
662
+ return JSON.stringify(rest);
663
+ }
664
+ function isSolidPaint(layer) {
665
+ const { fill, gradient, pattern } = layer.content;
666
+ return !gradient && !pattern && !fill?.includes("gradient(");
667
+ }
668
+ function mostCommonPaint(items) {
669
+ const counts = /* @__PURE__ */ new Map();
670
+ for (const item of items) {
671
+ const entry = counts.get(paintSignature(item));
672
+ if (entry) entry.count++;
673
+ else counts.set(paintSignature(item), { item, count: 1 });
674
+ }
675
+ let best = null;
676
+ for (const entry of counts.values()) {
677
+ if (!best || entry.count > best.count || entry.count === best.count && isSolidPaint(entry.item) && !isSolidPaint(best.item)) {
678
+ best = entry;
679
+ }
680
+ }
681
+ return best?.item ?? null;
682
+ }
683
+ function layoutLabel(block) {
684
+ if (block.template) return TEMPLATE_METADATA[block.template]?.label ?? block.template;
685
+ return block.id === "cover-block" ? "Cover" : "Slide";
686
+ }
687
+ function planDeckBackdrops(blocks) {
688
+ const raw = blocks.map((block) => splitSlideBackdrop(block.layers ?? []));
689
+ let masterOverlays = (raw[0]?.overlays ?? []).filter(
690
+ (overlay) => overlay.id.startsWith("persistent-") && raw.every(
691
+ (slide) => slide.overlays.some((other) => paintSignature(other) === paintSignature(overlay))
692
+ )
693
+ );
694
+ const masterSignatures = new Set(masterOverlays.map(paintSignature));
695
+ const split = raw.map((slide) => ({
696
+ ...compositeAsFarAsPossible(
697
+ slide.base,
698
+ slide.overlays.filter((overlay) => !masterSignatures.has(paintSignature(overlay)))
699
+ ),
700
+ content: slide.content
701
+ }));
702
+ const slideOverlays = split.map((slide) => slide.overlays);
703
+ const paintBases = split.map((slide) => slide.base).filter((base) => base?.type === "shape");
704
+ let masterBase = mostCommonPaint(paintBases);
705
+ const masterBaseSignature = masterBase ? paintSignature(masterBase) : null;
706
+ const layouts = [];
707
+ const layoutKeys = /* @__PURE__ */ new Map();
708
+ const groups = [];
709
+ blocks.forEach((block, index) => {
710
+ const key = `${layoutLabel(block)}\0${slideOverlays[index].map(paintSignature).join("\0")}`;
711
+ let layout = layoutKeys.get(key);
712
+ if (layout === void 0) {
713
+ layout = layouts.length;
714
+ layoutKeys.set(key, layout);
715
+ const label = layoutLabel(block);
716
+ const taken = layouts.filter((existing) => existing.name.replace(/ \d+$/, "") === label);
717
+ layouts.push({
718
+ name: taken.length ? `${label} ${taken.length + 1}` : label,
719
+ base: null,
720
+ overlays: slideOverlays[index]
721
+ });
722
+ groups.push([]);
723
+ }
724
+ groups[layout].push(index);
725
+ });
726
+ const slides = blocks.map(() => ({ layout: 0, base: null, content: [] }));
727
+ groups.forEach((members, layoutIndex) => {
728
+ const layout = layouts[layoutIndex];
729
+ const groupBase = mostCommonPaint(
730
+ members.map((index) => split[index].base).filter((base) => base?.type === "shape")
731
+ );
732
+ if (groupBase && paintSignature(groupBase) !== masterBaseSignature) layout.base = groupBase;
733
+ const inherited = layout.base ?? masterBase;
734
+ const inheritedSignature = inherited ? paintSignature(inherited) : null;
735
+ for (const index of members) {
736
+ const { base, content } = split[index];
737
+ slides[index] = {
738
+ layout: layoutIndex,
739
+ base: base && paintSignature(base) !== inheritedSignature ? base : null,
740
+ content
741
+ };
742
+ }
743
+ });
744
+ if (masterBase && masterOverlays.length > 0 && layouts.every((layout) => !layout.base) && slides.every((slide) => !slide.base)) {
745
+ const flat = compositeBackdrop(masterBase, masterOverlays);
746
+ if (flat) {
747
+ masterBase = flat;
748
+ masterOverlays = [];
749
+ }
750
+ }
751
+ return { master: { base: masterBase, overlays: masterOverlays }, layouts, slides };
752
+ }
753
+
754
+ // src/pptx/layerText.ts
755
+ import { splitIconMarkers as splitIconMarkers2 } from "@bendyline/squisq/icon-marker";
756
+
757
+ // src/pptx/textFit.ts
758
+ import { resolveFontFamily as resolveFontFamily2 } from "@bendyline/squisq/schemas";
759
+ import { hasIconMarker, splitIconMarkers } from "@bendyline/squisq/icon-marker";
760
+ var CANVAS_WIDTH = 1920;
761
+ var CANVAS_HEIGHT = 1080;
762
+ var MAX_TEXT_SCALE = 2;
763
+ var FIT_TOLERANCE_PX = 1;
764
+ var PARAGRAPH_GAP_EM = 0.4;
765
+ var LIST_GAP_EM = 0.7;
766
+ var LIST_INDENT_EM = 1.2;
767
+ var MONO_FONT = /\b(mono|monospace|courier|consolas|menlo|monaco)\b/;
768
+ var SERIF_FONT = /\b(serif|georgia|times|garamond|playfair|merriweather|lora|baskerville|cambria|palatino|didot|bodoni)\b/;
769
+ function averageCharEm(style, basis = "office") {
770
+ const first = resolveFontFamily2(style.fontFamily, "sans-serif").split(",")[0].trim().replace(/^['"]|['"]$/g, "").toLowerCase();
771
+ const serif = !first.includes("sans") && SERIF_FONT.test(first);
772
+ const base = MONO_FONT.test(first) ? 0.6 : basis === "browser" ? serif ? 0.5 : 0.44 : serif ? 0.52 : 0.48;
773
+ return style.fontWeight === "bold" ? base * 1.07 : base;
774
+ }
775
+ function wrapByChars(text, charsPerLine) {
776
+ if (!text.trim()) return [""];
777
+ if (charsPerLine <= 0) return [text];
778
+ const lines = [];
779
+ let current = "";
780
+ for (const word of text.split(/\s+/)) {
781
+ const candidate = current ? `${current} ${word}` : word;
782
+ if (candidate.length <= charsPerLine) {
783
+ current = candidate;
784
+ continue;
785
+ }
786
+ if (current) lines.push(current);
787
+ let remaining = word;
788
+ while (remaining.length > charsPerLine) {
789
+ lines.push(remaining.slice(0, charsPerLine));
790
+ remaining = remaining.slice(charsPerLine);
791
+ }
792
+ current = remaining;
793
+ }
794
+ if (current) lines.push(current);
795
+ return lines.length > 0 ? lines : [""];
796
+ }
797
+ function measurableText(text) {
798
+ return splitIconMarkers(text).map((run) => run.type === "text" ? run.text : "M").join("");
799
+ }
800
+ function isHtmlText(layer) {
801
+ return Boolean(layer.content.html?.trim()) || hasIconMarker(layer.content.text ?? "");
802
+ }
803
+ function hasBlockGaps(layer) {
804
+ return Boolean(layer.content.html?.trim());
805
+ }
806
+ function layerParagraphs(layer) {
807
+ return layer.content.paragraphs ?? (layer.content.text ?? "").split("\n").map((text) => ({ text }));
808
+ }
809
+ function blockGapsEm(paragraphs, lineHeight) {
810
+ let pending = 0;
811
+ let previous;
812
+ return paragraphs.map((paragraph) => {
813
+ if (!paragraph.text.trim() && !paragraph.list) {
814
+ pending++;
815
+ return null;
816
+ }
817
+ const gap = previous && pending > 0 ? (previous.list ? LIST_GAP_EM : PARAGRAPH_GAP_EM) + (pending - 1) * lineHeight : 0;
818
+ pending = 0;
819
+ previous = paragraph;
820
+ return gap;
821
+ });
822
+ }
823
+ function estimateFlowHeight(paragraphs, style, widthPx, blockGaps) {
824
+ const fontSize = style.fontSize;
825
+ const lineHeight = style.lineHeight ?? 1.4;
826
+ const advance = averageCharEm(style) * fontSize;
827
+ const gaps = blockGaps ? blockGapsEm(paragraphs, lineHeight) : void 0;
828
+ let height = 0;
829
+ paragraphs.forEach((paragraph, index) => {
830
+ const gap = gaps?.[index];
831
+ if (gap === null) return;
832
+ height += (gap ?? 0) * fontSize + (paragraph.spaceBefore ?? 0);
833
+ const indent = paragraph.list ? LIST_INDENT_EM * fontSize * (paragraph.list.level + 1) : 0;
834
+ const charsPerLine = Math.floor(Math.max(advance, widthPx - indent) / advance);
835
+ const lines = measurableText(paragraph.text).split("\n").reduce((count, line) => count + wrapByChars(line, charsPerLine).length, 0);
836
+ height += lines * lineHeight * fontSize;
837
+ });
838
+ return height;
839
+ }
840
+ function htmlContentWidth(layer) {
841
+ const { position, content } = layer;
842
+ const { style } = content;
843
+ const border = style.borderColor && (style.borderWidth ?? 0) > 0 ? style.borderWidth : 0;
844
+ const boxWidth = position.width != null ? resolvePositionValue(position.width, CANVAS_WIDTH) : CANVAS_WIDTH;
845
+ return Math.max(1, boxWidth - ((style.padding ?? 0) + border) * 2);
846
+ }
847
+ function estimateTextHeight(layer, scale, listIndentPx = 0) {
848
+ const { style } = layer.content;
849
+ if (isHtmlText(layer)) {
850
+ return estimateFlowHeight(
851
+ layerParagraphs(layer),
852
+ { ...style, fontSize: style.fontSize * scale },
853
+ htmlContentWidth(layer),
854
+ hasBlockGaps(layer)
855
+ );
856
+ }
857
+ const boxWidth = layer.position.width != null ? resolvePositionValue(layer.position.width, CANVAS_WIDTH) : void 0;
858
+ const lineHeight = (style.lineHeight ?? 1.4) * style.fontSize * scale;
859
+ let lineCount = 0;
860
+ let spacing = 0;
861
+ for (const paragraph of layerParagraphs(layer)) {
862
+ spacing += paragraph.spaceBefore ?? 0;
863
+ const indent = paragraph.list ? listIndentPx * (paragraph.list.level + 1) : 0;
864
+ const charsPerLine = boxWidth == null ? void 0 : Math.floor((boxWidth - indent) / (style.fontSize * 0.5));
865
+ for (const line of measurableText(paragraph.text).split("\n")) {
866
+ lineCount += charsPerLine == null ? 1 : wrapByChars(line, charsPerLine).length;
867
+ }
868
+ }
869
+ if (style.maxLines) lineCount = Math.min(lineCount, style.maxLines);
870
+ return lineCount * lineHeight + spacing;
871
+ }
872
+ function anchorOffset(anchor, size, axis) {
873
+ if (anchor === "center") return size / 2;
874
+ if (axis === "x") return anchor.includes("right") ? size : 0;
875
+ return anchor.includes("bottom") ? size : 0;
876
+ }
877
+ function svgTarget(layer) {
878
+ const { position, content } = layer;
879
+ const { style } = content;
880
+ const anchor = position.anchor ?? "top-left";
881
+ const rawX = resolvePositionValue(position.x, CANVAS_WIDTH);
882
+ const rawY = resolvePositionValue(position.y, CANVAS_HEIGHT);
883
+ const boxWidth = position.width != null ? resolvePositionValue(position.width, CANVAS_WIDTH) : void 0;
884
+ const boxHeight = position.height != null ? resolvePositionValue(position.height, CANVAS_HEIGHT) : void 0;
885
+ const textAnchor = style.textAlign === "center" ? "middle" : style.textAlign === "right" ? "end" : style.textAlign === "left" ? "start" : anchor.includes("right") ? "end" : anchor === "center" ? "middle" : "start";
886
+ const baseline = style.verticalAlign === "top" ? "before" : style.verticalAlign === "middle" ? "middle" : style.verticalAlign === "bottom" ? "after" : anchor.includes("bottom") ? "after" : anchor === "center" ? "middle" : "before";
887
+ let x = rawX;
888
+ if (boxWidth != null) {
889
+ const left = rawX - anchorOffset(anchor, boxWidth, "x");
890
+ x = textAnchor === "middle" ? left + boxWidth / 2 : textAnchor === "end" ? left + boxWidth : left;
891
+ }
892
+ let y = rawY;
893
+ if (boxHeight != null) {
894
+ const top = rawY - anchorOffset(anchor, boxHeight, "y");
895
+ y = baseline === "middle" ? top + boxHeight / 2 : baseline === "after" ? top + boxHeight : top;
896
+ }
897
+ const fitWidth = boxWidth ?? (textAnchor === "middle" ? Math.max(0, Math.min(x, CANVAS_WIDTH - x) * 2) : textAnchor === "end" ? Math.max(0, x) : Math.max(0, CANVAS_WIDTH - x));
898
+ const fitHeight = boxHeight ?? (baseline === "middle" ? Math.max(0, Math.min(y, CANVAS_HEIGHT - y) * 2) : baseline === "after" ? Math.max(0, y) : Math.max(0, CANVAS_HEIGHT - y));
899
+ let lines = measurableText(content.text ?? "").split("\n");
900
+ if (boxWidth != null) {
901
+ const playerChars = Math.floor(boxWidth / (style.fontSize * 0.5));
902
+ lines = lines.flatMap((line) => wrapByChars(line, playerChars));
903
+ }
904
+ if (style.maxLines && lines.length > style.maxLines) lines = lines.slice(0, style.maxLines);
905
+ const longest = Math.max(...lines.map((line) => line.length));
906
+ if (longest === 0) return null;
907
+ const width = longest * averageCharEm(style, "browser") * style.fontSize;
908
+ const height = estimateTextHeight(layer, 1);
909
+ return {
910
+ fits: (scale) => width * scale <= fitWidth + FIT_TOLERANCE_PX && height * scale <= fitHeight + FIT_TOLERANCE_PX,
911
+ rect: (scale) => {
912
+ const w = width * scale;
913
+ const h = height * scale;
914
+ const left = textAnchor === "middle" ? x - w / 2 : textAnchor === "end" ? x - w : x;
915
+ const top = baseline === "middle" ? y - h / 2 : baseline === "after" ? y - h : y;
916
+ return { left, top, right: left + w, bottom: top + h };
917
+ }
918
+ };
919
+ }
920
+ function htmlTarget(layer) {
921
+ const { position, content } = layer;
922
+ const { style } = content;
923
+ const anchor = position.anchor ?? "top-left";
924
+ const lineHeight = style.lineHeight ?? 1.4;
925
+ const padding = style.padding ?? 0;
926
+ const border = style.borderColor && (style.borderWidth ?? 0) > 0 ? style.borderWidth : 0;
927
+ const rich = hasBlockGaps(layer);
928
+ const boxWidth = position.width != null ? resolvePositionValue(position.width, CANVAS_WIDTH) : CANVAS_WIDTH;
929
+ const plain = measurableText(content.text ?? "");
930
+ const boxHeight = position.height != null ? resolvePositionValue(position.height, CANVAS_HEIGHT) : rich ? style.fontSize * lineHeight * 2 : Math.max(
931
+ 1,
932
+ plain.split("\n").flatMap((line) => wrapByChars(line, Math.floor(boxWidth / (style.fontSize * 0.5)))).length
933
+ ) * style.fontSize * lineHeight + padding * 2;
934
+ const inset = padding + border;
935
+ const left = resolvePositionValue(position.x, CANVAS_WIDTH) - anchorOffset(anchor, boxWidth, "x");
936
+ const top = resolvePositionValue(position.y, CANVAS_HEIGHT) - anchorOffset(anchor, boxHeight, "y");
937
+ const availableWidth = Math.max(1, boxWidth - inset * 2);
938
+ const availableHeight = Math.max(0, boxHeight - inset * 2);
939
+ const paragraphs = layerParagraphs(layer);
940
+ if (!paragraphs.some((paragraph) => paragraph.text.trim())) return null;
941
+ const justify = rich ? style.verticalAlign ?? "top" : style.verticalAlign === "top" || style.verticalAlign === "bottom" ? style.verticalAlign : "middle";
942
+ const heightAt = (scale) => estimateTextHeight(layer, scale);
943
+ return {
944
+ fits: (scale) => heightAt(scale) <= availableHeight + FIT_TOLERANCE_PX,
945
+ rect: (scale) => {
946
+ const h = heightAt(scale);
947
+ const contentTop = justify === "middle" ? top + (boxHeight - h) / 2 : justify === "bottom" ? top + boxHeight - inset - h : top + inset;
948
+ return {
949
+ left: left + inset,
950
+ top: contentTop,
951
+ right: left + inset + availableWidth,
952
+ bottom: contentTop + h
953
+ };
954
+ }
955
+ };
956
+ }
957
+ function overlapPairs(rects) {
958
+ const pairs = /* @__PURE__ */ new Set();
959
+ for (let a = 0; a < rects.length; a++) {
960
+ for (let b = a + 1; b < rects.length; b++) {
961
+ const one = rects[a];
962
+ const two = rects[b];
963
+ if (one.left < two.right - FIT_TOLERANCE_PX && one.right > two.left + FIT_TOLERANCE_PX && one.top < two.bottom - FIT_TOLERANCE_PX && one.bottom > two.top + FIT_TOLERANCE_PX) {
964
+ pairs.add(`${a}:${b}`);
965
+ }
966
+ }
967
+ }
968
+ return pairs;
969
+ }
970
+ function estimateSlideTextScale(layers) {
971
+ const targets = layers.filter((layer) => layer.type === "text").map((layer) => isHtmlText(layer) ? htmlTarget(layer) : svgTarget(layer)).filter((target) => target !== null);
972
+ if (targets.length === 0) return 1;
973
+ const baseline = overlapPairs(targets.map((target) => target.rect(1)));
974
+ const fits = (scale) => {
975
+ if (!targets.every((target) => target.fits(scale))) return false;
976
+ for (const pair of overlapPairs(targets.map((target) => target.rect(scale)))) {
977
+ if (!baseline.has(pair)) return false;
978
+ }
979
+ return true;
980
+ };
981
+ if (!fits(1)) return 1;
982
+ if (fits(MAX_TEXT_SCALE)) return MAX_TEXT_SCALE;
983
+ let low = 1;
984
+ let high = MAX_TEXT_SCALE;
985
+ for (let attempt = 0; attempt < 10; attempt++) {
986
+ const candidate = (low + high) / 2;
987
+ if (fits(candidate)) low = candidate;
988
+ else high = candidate;
989
+ }
990
+ return Math.max(1, Math.floor(low * 100) / 100);
991
+ }
992
+
993
+ // src/pptx/textLists.ts
994
+ function mergeTextFlows(layers, textScale = 1) {
995
+ const flows = /* @__PURE__ */ new Map();
996
+ for (const layer of layers) {
997
+ if (layer.type !== "text" || !layer.content.flow) continue;
998
+ const members = flows.get(layer.content.flow.id) ?? [];
999
+ members.push(layer);
1000
+ flows.set(layer.content.flow.id, members);
1001
+ }
1002
+ const units = [];
1003
+ const emitted = /* @__PURE__ */ new Set();
1004
+ for (const layer of layers) {
1005
+ const flowId = layer.type === "text" ? layer.content.flow?.id : void 0;
1006
+ const members = flowId === void 0 ? void 0 : flows.get(flowId);
1007
+ const bodies = members?.filter((member) => member.content.flow?.role !== "marker") ?? [];
1008
+ if (flowId === void 0 || !members || !bodies.some((body) => body.content.paragraphs)) {
1009
+ units.push({ layer });
1010
+ continue;
1011
+ }
1012
+ if (emitted.has(flowId)) continue;
1013
+ emitted.add(flowId);
1014
+ const rects = members.map((member) => {
1015
+ const { fontSize, lineHeight } = member.content.style;
1016
+ return resolveLayerRect(
1017
+ member.position,
1018
+ Math.max(fontSize, member.content.text.length * fontSize * 0.55),
1019
+ fontSize * (lineHeight ?? 1.4)
1020
+ );
1021
+ });
1022
+ const left = Math.min(...rects.map((rect) => rect.x));
1023
+ const top = Math.min(...rects.map((rect) => rect.y));
1024
+ const right = Math.max(...rects.map((rect) => rect.x + rect.width));
1025
+ const bottom = Math.max(...rects.map((rect) => rect.y + rect.height));
1026
+ const first = bodies[0];
1027
+ const indent = rects[members.indexOf(first)].x - left;
1028
+ const paragraphs = bodies.flatMap((body, index) => {
1029
+ const own = body.content.paragraphs ?? [{ text: body.content.text }];
1030
+ const previous = bodies[index - 1];
1031
+ if (!previous || own.length === 0) return own;
1032
+ const previousRect = rects[members.indexOf(previous)];
1033
+ const gap = rects[members.indexOf(body)].y - (previousRect.y + estimateTextHeight(previous, textScale));
1034
+ const [head, ...rest] = own;
1035
+ return [{ ...head, spaceBefore: Math.max(0, gap) }, ...rest];
1036
+ });
1037
+ units.push({
1038
+ layer: {
1039
+ ...first,
1040
+ id: flowId,
1041
+ position: { x: left, y: top, width: right - left, height: bottom - top },
1042
+ content: {
1043
+ text: bodies.map((body) => body.content.text).join("\n"),
1044
+ paragraphs,
1045
+ style: first.content.style
1046
+ }
1047
+ },
1048
+ ...indent > 0 ? { listIndentPx: indent } : {}
1049
+ });
1050
+ }
1051
+ return units;
1052
+ }
1053
+ function planListParagraphs(paragraphs) {
1054
+ const runs = [];
1055
+ return paragraphs.map(({ list }) => {
1056
+ if (!list) {
1057
+ runs.length = 0;
1058
+ return void 0;
1059
+ }
1060
+ const level = Math.min(8, Math.max(0, Math.floor(list.level)));
1061
+ runs.length = Math.min(runs.length, level + 1);
1062
+ if (!list.ordered) {
1063
+ runs[level] = void 0;
1064
+ return { ordered: false, level };
1065
+ }
1066
+ const number = list.number ?? 1;
1067
+ const run = runs[level];
1068
+ if (run && run.next === number) run.next++;
1069
+ else runs[level] = { start: number, next: number + 1 };
1070
+ return { ordered: true, level, startAt: runs[level].start };
1071
+ });
1072
+ }
1073
+ function listParagraphXml(plan, indentPx) {
1074
+ const hang = Math.round(indentPx * EMU_PER_CANVAS_PX);
1075
+ const startAt = plan.startAt && plan.startAt !== 1 ? ` startAt="${plan.startAt}"` : "";
1076
+ return {
1077
+ attributes: ` marL="${hang * (plan.level + 1)}" lvl="${plan.level}" indent="${-hang}"`,
1078
+ bullet: plan.ordered ? `<a:buAutoNum type="arabicPeriod"${startAt}/>` : `<a:buChar char="\u2022"/>`
1079
+ };
1080
+ }
1081
+
1082
+ // src/pptx/layerText.ts
1083
+ function buildLayerTextShape(layer, shapeId, maxBottom, listIndentPx, textScale = 1) {
1084
+ const sourceText = layer.content.text ?? "";
1085
+ const text = measurableText(sourceText);
1086
+ const baseStyle = layer.content.style;
1087
+ const style = textScale === 1 ? baseStyle : { ...baseStyle, fontSize: baseStyle.fontSize * textScale };
1088
+ const padding = style.padding ?? 0;
1089
+ const rawLines = text.split("\n");
1090
+ const charEm = averageCharEm(style);
1091
+ const estimatedWidth = Math.min(
1092
+ 1920,
1093
+ Math.max(
1094
+ 1,
1095
+ Math.max(...rawLines.map((line) => line.length), 1) * style.fontSize * // A box sized to the text must not wrap it when Office's face for
1096
+ // the CSS stack runs a little wider than the estimate.
1097
+ Math.max(0.55, charEm * 1.15) + padding * 2
1098
+ )
1099
+ );
1100
+ const boxWidth = layer.position.width != null ? resolvePositionValue(layer.position.width, 1920) : estimatedWidth;
1101
+ const fittedFontSize = rawLines.length === 1 && style.fontSize >= 80 && text.length > 0 ? Math.min(style.fontSize, (boxWidth - padding * 2) / (text.length * charEm * 1.05)) : style.fontSize;
1102
+ const blockGaps = hasBlockGaps(layer);
1103
+ const paragraphSource = layerParagraphs(layer);
1104
+ const neededHeight = Math.max(
1105
+ fittedFontSize * (style.lineHeight ?? 1.4),
1106
+ estimateTextHeight(layer, fittedFontSize / baseStyle.fontSize, listIndentPx)
1107
+ );
1108
+ const reserve = fittedFontSize * OFFICE_TEXT_RESERVE_EM;
1109
+ const estimatedHeight = neededHeight + reserve + padding * 2;
1110
+ const resolvedRect = resolveLayerRect(layer.position, boxWidth, estimatedHeight);
1111
+ const safeBottom = Math.min(1060, maxBottom ?? 1060);
1112
+ const vertical = style.verticalAlign === "bottom" ? "b" : style.verticalAlign === "middle" ? "ctr" : "t";
1113
+ let virtualRect = {
1114
+ ...resolvedRect,
1115
+ y: Math.min(resolvedRect.y, safeBottom - 1),
1116
+ height: Math.max(1, Math.min(resolvedRect.height, safeBottom - resolvedRect.y))
1117
+ };
1118
+ const visibleBox = !!style.background || !!style.backgroundGradient || !!(style.borderColor && (style.borderWidth ?? 0) > 0);
1119
+ if (!visibleBox && virtualRect.height < estimatedHeight) {
1120
+ const extra = estimatedHeight - virtualRect.height;
1121
+ const above = vertical === "b" ? extra : vertical === "ctr" ? extra / 2 : 0;
1122
+ const top = Math.max(0, virtualRect.y - above);
1123
+ const bottom = Math.min(safeBottom, virtualRect.y + virtualRect.height + extra - above);
1124
+ virtualRect = { ...virtualRect, y: top, height: Math.max(virtualRect.height, bottom - top) };
1125
+ }
1126
+ const availableHeight = Math.max(1, virtualRect.height - padding * 2);
1127
+ const heightScale = neededHeight > availableHeight + FIT_TOLERANCE_PX2 ? availableHeight / neededHeight : 1;
1128
+ const renderedFontSize = Math.max(1, fittedFontSize * heightScale * (heightScale < 1 ? 0.95 : 1));
1129
+ const renderedStyle = renderedFontSize === style.fontSize ? style : { ...style, fontSize: renderedFontSize };
1130
+ const rect = toEmuRect(virtualRect);
1131
+ const align = style.textAlign === "center" ? "ctr" : style.textAlign === "right" ? "r" : "l";
1132
+ const inset = Math.max(0, Math.round(padding * EMU_PER_CANVAS_PX));
1133
+ const bodyPr = `<a:bodyPr wrap="square" anchor="${vertical}" vertOverflow="clip" horzOverflow="clip" lIns="${inset}" rIns="${inset}" tIns="${inset}" bIns="${inset}"><a:noAutofit/></a:bodyPr>`;
1134
+ const paragraphs = layer.content.paragraphs || blockGaps ? buildStructuredParagraphs(paragraphSource, renderedStyle, align, {
1135
+ // A list with no marker column of its own hangs its bullets by the
1136
+ // player's list indent.
1137
+ indentPx: listIndentPx ?? renderedStyle.fontSize * LIST_INDENT_EM,
1138
+ spacingScale: renderedStyle.fontSize / fittedFontSize,
1139
+ blockGaps
1140
+ }) : sourceText.split("\n").map((line) => buildLayerTextParagraph(line, renderedStyle, align)).join("");
1141
+ return `<p:sp><p:nvSpPr><p:cNvPr id="${shapeId}" name="${escapeXml(layer.id)}"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` + fillXml(style.background, style.backgroundOpacity, style.backgroundGradient) + lineXml(style.borderColor, style.borderWidth, style.borderStyle) + `</p:spPr><p:txBody>${bodyPr}<a:lstStyle/>${paragraphs}</p:txBody></p:sp>`;
1142
+ }
1143
+ var TEXT_PX_TO_CENTIPOINTS = 50;
1144
+ var OFFICE_SINGLE_LINE_EM = 1.2;
1145
+ var OFFICE_TEXT_RESERVE_EM = 0.3;
1146
+ var FIT_TOLERANCE_PX2 = 1;
1147
+ function buildStructuredParagraphs(paragraphs, style, align, options) {
1148
+ const plans = planListParagraphs(paragraphs);
1149
+ const gaps = options.blockGaps ? blockGapsEm(paragraphs, style.lineHeight ?? 1.4) : void 0;
1150
+ return paragraphs.map((paragraph, index) => {
1151
+ const gap = gaps?.[index];
1152
+ if (gap === null) return "";
1153
+ return buildLayerTextParagraph(paragraph.text, style, align, {
1154
+ spaceBefore: (gap ?? 0) * style.fontSize + (paragraph.spaceBefore ?? 0) * options.spacingScale,
1155
+ list: plans[index],
1156
+ indentPx: options.indentPx
1157
+ });
1158
+ }).join("");
1159
+ }
1160
+ function buildLayerTextParagraph(text, style, align, options = {}) {
1161
+ const size = Math.max(100, Math.round(style.fontSize * TEXT_PX_TO_CENTIPOINTS));
1162
+ const color = parseCssColor(style.color, "000000");
1163
+ const typeface = officeTypeface(style.fontFamily, DEFAULT_FONT);
1164
+ const textRunProperties = [
1165
+ `lang="en-US"`,
1166
+ `dirty="0"`,
1167
+ `sz="${size}"`,
1168
+ ...style.fontWeight === "bold" ? ['b="1"'] : [],
1169
+ ...style.fontStyle === "italic" ? ['i="1"'] : []
1170
+ ].join(" ");
1171
+ const lineSpacing = Math.max(
1172
+ 1e4,
1173
+ Math.round((style.lineHeight ?? 1.4) / OFFICE_SINGLE_LINE_EM * 1e5)
1174
+ );
1175
+ const lineRuns = (line) => splitIconMarkers2(line).map((run) => {
1176
+ if (run.type === "text") {
1177
+ if (!run.text) return "";
1178
+ return `<a:r><a:rPr ${textRunProperties}>${solidColorXml(color)}<a:latin typeface="${escapeXml(typeface)}"/></a:rPr><a:t>${escapeXml(run.text)}</a:t></a:r>`;
1179
+ }
1180
+ const glyph = fontAwesomeGlyph(run.family, run.name);
1181
+ if (!glyph) return "";
1182
+ const iconTypeface = fontAwesomeFace(run.family).typeface;
1183
+ return `<a:r><a:rPr lang="en-US" dirty="0" sz="${size}">${solidColorXml(color)}<a:latin typeface="${escapeXml(iconTypeface)}"/></a:rPr><a:t>${escapeXml(glyph)}</a:t></a:r>`;
1184
+ }).join("");
1185
+ const runs = text.split("\n").map(lineRuns).join(`<a:br><a:rPr ${textRunProperties}/></a:br>`);
1186
+ const spaceBefore = Math.round((options.spaceBefore ?? 0) * TEXT_PX_TO_CENTIPOINTS);
1187
+ const { attributes, bullet } = options.list ? listParagraphXml(options.list, options.indentPx ?? style.fontSize * LIST_INDENT_EM) : { attributes: "", bullet: "" };
1188
+ return `<a:p><a:pPr${attributes} algn="${align}"><a:lnSpc><a:spcPct val="${lineSpacing}"/></a:lnSpc>` + (spaceBefore > 0 ? `<a:spcBef><a:spcPts val="${spaceBefore}"/></a:spcBef>` : "") + // The bullet takes the text's colour explicitly: renderers that do not
1189
+ // follow the first run fall back to the master's default — black, which
1190
+ // vanishes on a dark deck.
1191
+ (bullet ? `<a:buClr><a:srgbClr val="${color.hex}"/></a:buClr>${bullet}` : "") + `</a:pPr>` + (runs || `<a:r><a:rPr ${textRunProperties}>${solidColorXml(color)}<a:latin typeface="${escapeXml(typeface)}"/></a:rPr><a:t> </a:t></a:r>`) + `</a:p>`;
1192
+ }
1193
+
1194
+ // src/pptx/masters.ts
1195
+ import { relativeLuminance } from "@bendyline/squisq/schemas";
1196
+ var GROUP_SHAPE_PROPS = `<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>`;
1197
+ function backgroundXml(fill) {
1198
+ return `<p:bg><p:bgPr>${fill}<a:effectLst/></p:bgPr></p:bg>`;
1199
+ }
1200
+ function paintBackgroundXml(base) {
1201
+ const { fill, fillOpacity, gradient, pattern } = base.content;
1202
+ return backgroundXml(
1203
+ pattern ? patternFillXml(pattern, fill) : fillXml(fill, fillOpacity, gradient)
1204
+ );
1205
+ }
1206
+ function colorBackgroundXml(hex) {
1207
+ return backgroundXml(`<a:solidFill><a:srgbClr val="${hex}"/></a:solidFill>`);
1208
+ }
1209
+ function photoBackgroundXml(relId, layer, data) {
1210
+ let crop = "";
1211
+ const size = layer.content.fit === "cover" ? readImageDimensions(data) : null;
1212
+ if (size && size.width > 0 && size.height > 0) {
1213
+ const imageAspect = size.width / size.height;
1214
+ const slideAspect = 16 / 9;
1215
+ if (Math.abs(imageAspect - slideAspect) > 1e-3) {
1216
+ const trim = imageAspect > slideAspect ? 1 - slideAspect / imageAspect : 1 - imageAspect / slideAspect;
1217
+ const edge = Math.round(trim / 2 * 1e5);
1218
+ crop = imageAspect > slideAspect ? `<a:srcRect l="${edge}" r="${edge}"/>` : `<a:srcRect t="${edge}" b="${edge}"/>`;
1219
+ }
1220
+ }
1221
+ return backgroundXml(
1222
+ `<a:blipFill dpi="0" rotWithShape="1"><a:blip r:embed="${relId}"/>${crop}<a:stretch><a:fillRect/></a:stretch></a:blipFill>`
1223
+ );
1224
+ }
1225
+ function overlayShapesXml(overlays) {
1226
+ return overlays.map((overlay, index) => buildLayerShape(overlay, index + 2)).join("");
1227
+ }
1228
+ function buildDeckLayoutXml(layout) {
1229
+ return xmlDeclaration() + `<p:sldLayout xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}" preserve="1" userDrawn="1"><p:cSld name="${escapeXml(layout.name)}">` + (layout.base ? paintBackgroundXml(layout.base) : "") + `<p:spTree>${GROUP_SHAPE_PROPS}${overlayShapesXml(layout.overlays)}</p:spTree></p:cSld><p:clrMapOvr><a:masterClrMapping/></p:clrMapOvr></p:sldLayout>`;
1230
+ }
1231
+ function isDarkTheme(theme) {
1232
+ const background = toOoxmlHex(theme.colors.background, "FFFFFF");
1233
+ const text = toOoxmlHex(theme.colors.text, "000000");
1234
+ return relativeLuminance(`#${background}`) < relativeLuminance(`#${text}`);
1235
+ }
1236
+ function colorMapXml(dark) {
1237
+ const roles = dark ? 'bg1="dk1" tx1="lt1" bg2="dk2" tx2="lt2"' : 'bg1="lt1" tx1="dk1" bg2="lt2" tx2="dk2"';
1238
+ return `<p:clrMap ${roles} accent1="accent1" accent2="accent2" accent3="accent3" accent4="accent4" accent5="accent5" accent6="accent6" hlink="hlink" folHlink="folHlink"/>`;
1239
+ }
1240
+ function buildDeckMasterXml(options) {
1241
+ const layoutIds = options.layoutRelIds.map((relId, index) => `<p:sldLayoutId id="${2147483649 + index}" r:id="${relId}"/>`).join("");
1242
+ return xmlDeclaration() + `<p:sldMaster xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}"><p:cSld>${options.backgroundXml}<p:spTree>${GROUP_SHAPE_PROPS}${overlayShapesXml(options.overlays)}</p:spTree></p:cSld>` + colorMapXml(options.dark) + `<p:sldLayoutIdLst>${layoutIds}</p:sldLayoutIdLst></p:sldMaster>`;
1243
+ }
1244
+ function buildDeckThemeXml(theme) {
1245
+ const c = theme.colors;
1246
+ const hex = (color, fallback) => toOoxmlHex(color, fallback);
1247
+ const dark = isDarkTheme(theme);
1248
+ const background = hex(c.background, "FFFFFF");
1249
+ const text = hex(c.text, "000000");
1250
+ const backgroundLight = hex(c.backgroundLight, background);
1251
+ const textMuted = hex(c.textMuted, text);
1252
+ const accents = [c.primary, c.secondary, c.highlight, c.warning].concat(Object.values(theme.colorSchemes ?? {}).map((scheme) => scheme.accent)).map((color) => color ? toOoxmlHex(color, "") : "").filter((color, index, all) => color && all.indexOf(color) === index);
1253
+ const distinct = accents.length > 0 ? [...accents] : [text];
1254
+ for (let index = accents.length; index < 6; index++) {
1255
+ accents.push(distinct[index % distinct.length]);
1256
+ }
1257
+ const slots = [
1258
+ ["dk1", dark ? background : text],
1259
+ ["lt1", dark ? text : background],
1260
+ ["dk2", dark ? backgroundLight : textMuted],
1261
+ ["lt2", dark ? textMuted : backgroundLight],
1262
+ ...accents.slice(0, 6).map((color, index) => [`accent${index + 1}`, color]),
1263
+ ["hlink", hex(c.highlight || c.primary, "0563C1")],
1264
+ ["folHlink", textMuted]
1265
+ ];
1266
+ const name = escapeXml(theme.name || "Squisq");
1267
+ const titleFont = escapeXml(officeTypeface(theme.typography?.titleFont, DEFAULT_TITLE_FONT));
1268
+ const bodyFont = escapeXml(officeTypeface(theme.typography?.bodyFont, DEFAULT_FONT));
1269
+ const solid = `<a:solidFill><a:schemeClr val="phClr"/></a:solidFill>`;
1270
+ return xmlDeclaration() + `<a:theme xmlns:a="${NS_DRAWINGML}" name="${name}"><a:themeElements><a:clrScheme name="${name}">` + slots.map(([slot, color]) => `<a:${slot}><a:srgbClr val="${color}"/></a:${slot}>`).join("") + `</a:clrScheme><a:fontScheme name="${name}"><a:majorFont><a:latin typeface="${titleFont}"/><a:ea typeface=""/><a:cs typeface=""/></a:majorFont><a:minorFont><a:latin typeface="${bodyFont}"/><a:ea typeface=""/><a:cs typeface=""/></a:minorFont></a:fontScheme><a:fmtScheme name="${name}"><a:fillStyleLst>${solid}${solid}${solid}</a:fillStyleLst><a:lnStyleLst><a:ln w="6350">${solid}</a:ln><a:ln w="6350">${solid}</a:ln><a:ln w="6350">${solid}</a:ln></a:lnStyleLst><a:effectStyleLst><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle><a:effectStyle><a:effectLst/></a:effectStyle></a:effectStyleLst><a:bgFillStyleLst>${solid}${solid}${solid}</a:bgFillStyleLst></a:fmtScheme></a:themeElements></a:theme>`;
1271
+ }
1272
+
182
1273
  // src/pptx/templates.ts
183
1274
  function buildPresentationXml(slideCount, slideRelIds, slideMasterRelId, _themeRelId, embeddedFonts = []) {
184
1275
  const sldIdList = [];
@@ -290,9 +1381,10 @@ async function docToPptx(doc, options = {}) {
290
1381
  const hasLayer = (type) => slideBlocks.some((b) => b.layers?.some((l) => l.type === type));
291
1382
  const videoPosters = hasLayer("videoEmbed") ? await import("./videoPoster-D7DTZC2L.js") : null;
292
1383
  const hasSvgImage = slideBlocks.some(
293
- (b) => b.layers?.some((l) => l.type === "image" && isSvgImageSource(l.content.src))
1384
+ (b) => b.layers?.some((l) => l.type === "image" && isSvgImageSource2(l.content.src))
294
1385
  );
295
1386
  const svgIslandPosters = hasLayer("svg") || hasSvgImage ? await import("./svgIslandPoster-RYA6PDX7.js") : null;
1387
+ const backdrops = planDeckBackdrops(slideBlocks);
296
1388
  const slideXmls = [];
297
1389
  const slideContexts = [];
298
1390
  for (let index = 0; index < slideBlocks.length; index++) {
@@ -307,15 +1399,21 @@ async function docToPptx(doc, options = {}) {
307
1399
  ctx.diagramImages = effectiveOptions.diagramImages;
308
1400
  ctx.videoPosters = videoPosters;
309
1401
  ctx.svgIslandPosters = svgIslandPosters;
310
- slideXmls.push(buildLayerSlideXml(slideBlocks[index], ctx));
1402
+ const plan = backdrops.slides[index];
1403
+ ctx.useLayout(plan.layout + 1);
1404
+ slideXmls.push(buildLayerSlideXml(slideBlocks[index], ctx, plan));
311
1405
  slideContexts.push(ctx);
312
1406
  }
313
- return buildPptxPackage(slideXmls, slideContexts, effectiveOptions);
314
- }
315
- function officeTypeface(family, fallback) {
316
- const resolved = resolveFontFamily(family, fallback);
317
- const first = resolved.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
318
- return first || fallback;
1407
+ return buildPptxPackage(slideXmls, slideContexts, effectiveOptions, {
1408
+ layoutXmls: backdrops.layouts.map(buildDeckLayoutXml),
1409
+ masterXml: (layoutRelIds) => buildDeckMasterXml({
1410
+ backgroundXml: backdrops.master.base ? paintBackgroundXml(backdrops.master.base) : colorBackgroundXml(style.background),
1411
+ overlays: backdrops.master.overlays,
1412
+ layoutRelIds,
1413
+ dark: isDarkTheme(theme)
1414
+ }),
1415
+ themeXml: buildDeckThemeXml(theme)
1416
+ });
319
1417
  }
320
1418
  function resolveSlideStyle(themeId, options, doc) {
321
1419
  if (!themeId) {
@@ -430,6 +1528,10 @@ var SlideContext = class {
430
1528
  allocRelId() {
431
1529
  return this.relIds.alloc();
432
1530
  }
1531
+ /** Point this slide at slide layout `number` (1-based, as its part is named). */
1532
+ useLayout(number) {
1533
+ this.relationships[0].target = `../slideLayouts/slideLayout${number}.xml`;
1534
+ }
433
1535
  allocShapeId() {
434
1536
  return this.nextShapeId++;
435
1537
  }
@@ -527,31 +1629,46 @@ function buildSlideXml(slide, ctx) {
527
1629
  const extensionAttrs = transitionXml.includes("p14:") ? ` xmlns:mc="${NS_MC}" xmlns:p14="${NS_PML_2010}" mc:Ignorable="p14"` : "";
528
1630
  return xmlDeclaration() + `<p:sld xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}"${extensionAttrs}><p:cSld>` + bgXml + `<p:spTree>` + shapes.join("") + `</p:spTree></p:cSld>` + transitionXml + `</p:sld>`;
529
1631
  }
530
- function buildLayerSlideXml(block, ctx) {
1632
+ function buildLayerSlideXml(block, ctx, plan) {
531
1633
  const shapes = [
532
1634
  `<p:nvGrpSpPr><p:cNvPr id="1" name=""/><p:cNvGrpSpPr/><p:nvPr/></p:nvGrpSpPr><p:grpSpPr/>`
533
1635
  ];
534
- const textLayerY = (block.layers ?? []).filter((layer) => layer.type === "text").map((layer) => resolvePositionValue(layer.position.y, 1080));
535
- for (const layer of block.layers ?? []) {
1636
+ let layers = plan.content;
1637
+ let bgXml = "";
1638
+ if (plan.base?.type === "image") {
1639
+ const photo = plan.base;
1640
+ const data = ctx.images?.get(photo.content.src);
1641
+ if (data) {
1642
+ bgXml = photoBackgroundXml(
1643
+ ctx.addImage(photo.content.src, data, photo.content.alt),
1644
+ photo,
1645
+ data
1646
+ );
1647
+ } else {
1648
+ layers = [photo, ...layers];
1649
+ }
1650
+ } else if (plan.base) {
1651
+ bgXml = paintBackgroundXml(plan.base);
1652
+ }
1653
+ const textScale = isGeometryBoundTemplate(block.template) ? 1 : estimateSlideTextScale(layers);
1654
+ const units = mergeTextFlows(layers, textScale);
1655
+ const textLayerY = units.filter(({ layer }) => layer.type === "text").map(({ layer }) => resolvePositionValue(layer.position.y, 1080));
1656
+ for (const layer of layers) {
536
1657
  if (layer.type !== "text") continue;
537
- for (const run of splitIconMarkers(layer.content.text ?? "")) {
1658
+ for (const run of splitIconMarkers3(layer.content.text ?? "")) {
538
1659
  if (run.type === "icon") ctx.iconFamilies.add(run.family);
539
1660
  }
540
1661
  }
541
- for (let index = 0; index < (block.layers?.length ?? 0); index++) {
1662
+ for (let index = 0; index < units.length; index++) {
542
1663
  if ((index & 31) === 0) ctx.signal?.throwIfAborted();
543
- const layer = block.layers[index];
1664
+ const { layer, listIndentPx } = units[index];
544
1665
  const currentY = layer.type === "text" ? resolvePositionValue(layer.position.y, 1080) : void 0;
545
1666
  const nextTextY = currentY === void 0 ? void 0 : textLayerY.filter((candidate) => candidate > currentY + 1).sort((a, b) => a - b)[0];
1667
+ const textBottom = nextTextY === void 0 || currentY === void 0 ? void 0 : Math.max(currentY + 1, nextTextY - 8);
546
1668
  shapes.push(
547
- ...convertLayerToShapes(
548
- layer,
549
- ctx,
550
- nextTextY === void 0 || currentY === void 0 ? void 0 : Math.max(currentY + 1, nextTextY - 8)
551
- )
1669
+ ...layer.type === "text" ? [buildLayerTextShape(layer, ctx.allocShapeId(), textBottom, listIndentPx, textScale)] : convertLayerToShapes(layer, ctx, textBottom)
552
1670
  );
553
1671
  }
554
- const bgXml = ctx.style.hasTheme ? `<p:bg><p:bgPr><a:solidFill><a:srgbClr val="${ctx.style.background}"/></a:solidFill><a:effectLst/></p:bgPr></p:bg>` : "";
555
1672
  const transitionXml = buildTransitionXml(block.transition);
556
1673
  const extensionAttrs = transitionXml.includes("p14:") ? ` xmlns:mc="${NS_MC}" xmlns:p14="${NS_PML_2010}" mc:Ignorable="p14"` : "";
557
1674
  return xmlDeclaration() + `<p:sld xmlns:a="${NS_DRAWINGML}" xmlns:r="${NS_R}" xmlns:p="${NS_PML}"${extensionAttrs}><p:cSld>${bgXml}<p:spTree>${shapes.join("")}</p:spTree></p:cSld>` + transitionXml + `</p:sld>`;
@@ -564,7 +1681,7 @@ function convertLayerToShapes(layer, ctx, textBottom) {
564
1681
  if (layer.content.filter?.type === "noise") return [];
565
1682
  return [buildLayerShape(layer, ctx.allocShapeId())];
566
1683
  case "image": {
567
- const svg = isSvgImageSource(layer.content.src);
1684
+ const svg = isSvgImageSource2(layer.content.src);
568
1685
  const area = toEmuRect(resolveLayerRect(layer.position, 1920, 1080));
569
1686
  const picture = svg ? svgImagePictureShapes(layer, area, ctx) : null;
570
1687
  if (picture) return picture;
@@ -635,7 +1752,7 @@ function convertLayerToShapes(layer, ctx, textBottom) {
635
1752
  }
636
1753
  case "video": {
637
1754
  const poster = layer.content.posterSrc;
638
- const data = poster && !isSvgImageSource(poster) ? ctx.images?.get(poster) : void 0;
1755
+ const data = poster && !isSvgImageSource2(poster) ? ctx.images?.get(poster) : void 0;
639
1756
  if (poster && data) {
640
1757
  const relId = ctx.addImage(poster, data, layer.content.alt);
641
1758
  const embedded = ctx.embeddedImages.find((candidate) => candidate.relId === relId);
@@ -716,127 +1833,6 @@ function convertLayerToShapes(layer, ctx, textBottom) {
716
1833
  }
717
1834
  }
718
1835
  }
719
- function resolvePositionValue(value, dimension) {
720
- if (typeof value === "number") return value;
721
- if (typeof value === "string") {
722
- const trimmed = value.trim();
723
- if (trimmed.endsWith("%")) {
724
- const percent = Number.parseFloat(trimmed);
725
- if (Number.isFinite(percent)) return percent / 100 * dimension;
726
- }
727
- const numeric = Number.parseFloat(trimmed);
728
- if (Number.isFinite(numeric)) return numeric;
729
- }
730
- return 0;
731
- }
732
- function resolveLayerRect(position, defaultWidth, defaultHeight) {
733
- const width = Math.max(
734
- 1,
735
- position.width != null ? resolvePositionValue(position.width, 1920) : defaultWidth
736
- );
737
- const height = Math.max(
738
- 1,
739
- position.height != null ? resolvePositionValue(position.height, 1080) : defaultHeight
740
- );
741
- let x = resolvePositionValue(position.x, 1920);
742
- let y = resolvePositionValue(position.y, 1080);
743
- const anchor = position.anchor ?? "top-left";
744
- if (anchor === "center") {
745
- x -= width / 2;
746
- y -= height / 2;
747
- } else {
748
- if (anchor.endsWith("right")) x -= width;
749
- if (anchor.startsWith("bottom")) y -= height;
750
- }
751
- const clippedX = Math.min(1919, Math.max(0, x));
752
- const clippedY = Math.min(1079, Math.max(0, y));
753
- const clippedWidth = Math.max(1, Math.min(width - (clippedX - x), 1920 - clippedX));
754
- const clippedHeight = Math.max(1, Math.min(height - (clippedY - y), 1080 - clippedY));
755
- return { x: clippedX, y: clippedY, width: clippedWidth, height: clippedHeight };
756
- }
757
- function toEmuRect(rect) {
758
- return {
759
- x: Math.round(rect.x / 1920 * SLIDE_WIDTH),
760
- y: Math.round(rect.y / 1080 * SLIDE_HEIGHT),
761
- width: Math.max(1, Math.round(rect.width / 1920 * SLIDE_WIDTH)),
762
- height: Math.max(1, Math.round(rect.height / 1080 * SLIDE_HEIGHT))
763
- };
764
- }
765
- function buildLayerTextShape(layer, shapeId, maxBottom) {
766
- const sourceText = layer.content.text ?? "";
767
- const text = splitIconMarkers(sourceText).map((run) => run.type === "text" ? run.text : "M").join("");
768
- const style = layer.content.style;
769
- const padding = style.padding ?? 0;
770
- const rawLines = text.split("\n");
771
- const estimatedWidth = Math.min(
772
- 1920,
773
- Math.max(1, Math.max(...rawLines.map((line) => line.length), 1) * style.fontSize * 0.55)
774
- );
775
- const boxWidth = layer.position.width != null ? resolvePositionValue(layer.position.width, 1920) : estimatedWidth;
776
- const fittedFontSize = rawLines.length === 1 && style.fontSize >= 80 && text.length > 0 ? Math.min(style.fontSize, boxWidth / (text.length * 0.78)) : style.fontSize;
777
- const charsPerLine = Math.max(1, Math.floor(boxWidth / Math.max(1, fittedFontSize * 0.58)));
778
- const wrappedLineCount = Math.max(
779
- 1,
780
- rawLines.reduce((count, line) => count + Math.max(1, Math.ceil(line.length / charsPerLine)), 0)
781
- );
782
- const officeFontMetricSlack = fittedFontSize * 0.8;
783
- const estimatedHeight = Math.max(
784
- fittedFontSize * (style.lineHeight ?? 1.4) * wrappedLineCount + officeFontMetricSlack + padding * 2,
785
- fittedFontSize + officeFontMetricSlack + padding * 2
786
- );
787
- const resolvedRect = resolveLayerRect(layer.position, boxWidth, estimatedHeight);
788
- const safeBottom = Math.min(1060, maxBottom ?? 1060);
789
- const virtualRect = {
790
- ...resolvedRect,
791
- y: Math.min(resolvedRect.y, safeBottom - 1),
792
- height: Math.max(1, Math.min(resolvedRect.height, safeBottom - resolvedRect.y))
793
- };
794
- const heightScale = Math.min(1, virtualRect.height / estimatedHeight);
795
- const renderedFontSize = Math.max(1, fittedFontSize * heightScale * (heightScale < 1 ? 0.9 : 1));
796
- const renderedStyle = renderedFontSize === style.fontSize ? style : { ...style, fontSize: renderedFontSize };
797
- const rect = toEmuRect(virtualRect);
798
- const align = style.textAlign === "center" ? "ctr" : style.textAlign === "right" ? "r" : "l";
799
- const vertical = style.verticalAlign === "bottom" ? "b" : style.verticalAlign === "middle" ? "ctr" : "t";
800
- const inset = Math.max(0, Math.round(padding / 1920 * SLIDE_WIDTH));
801
- const bodyPr = `<a:bodyPr wrap="square" anchor="${vertical}" vertOverflow="clip" horzOverflow="clip" lIns="${inset}" rIns="${inset}" tIns="${inset}" bIns="${inset}"><a:noAutofit/></a:bodyPr>`;
802
- const paragraphs = sourceText.split("\n").map((line) => buildLayerTextParagraph(line, renderedStyle, align)).join("");
803
- return `<p:sp><p:nvSpPr><p:cNvPr id="${shapeId}" name="${escapeXml(layer.id)}"/><p:cNvSpPr txBox="1"/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom>` + fillXml(style.background, style.backgroundOpacity, style.backgroundGradient) + lineXml(style.borderColor, style.borderWidth, style.borderStyle) + `</p:spPr><p:txBody>${bodyPr}<a:lstStyle/>${paragraphs}</p:txBody></p:sp>`;
804
- }
805
- function buildLayerTextParagraph(text, style, align) {
806
- const size = Math.max(100, Math.round(style.fontSize * 75));
807
- const color = parseCssColor(style.color, "000000");
808
- const typeface = officeTypeface(style.fontFamily, DEFAULT_FONT);
809
- const textRunProperties = [
810
- `lang="en-US"`,
811
- `dirty="0"`,
812
- `sz="${size}"`,
813
- ...style.fontWeight === "bold" ? ['b="1"'] : [],
814
- ...style.fontStyle === "italic" ? ['i="1"'] : []
815
- ].join(" ");
816
- const lineSpacing = Math.max(1e4, Math.round((style.lineHeight ?? 1.4) * 1e5));
817
- const runs = splitIconMarkers(text).map((run) => {
818
- if (run.type === "text") {
819
- if (!run.text) return "";
820
- return `<a:r><a:rPr ${textRunProperties}>${solidColorXml(color)}<a:latin typeface="${escapeXml(typeface)}"/></a:rPr><a:t>${escapeXml(run.text)}</a:t></a:r>`;
821
- }
822
- const glyph = fontAwesomeGlyph(run.family, run.name);
823
- if (!glyph) return "";
824
- const iconTypeface = fontAwesomeFace(run.family).typeface;
825
- return `<a:r><a:rPr lang="en-US" dirty="0" sz="${size}">${solidColorXml(color)}<a:latin typeface="${escapeXml(iconTypeface)}"/></a:rPr><a:t>${escapeXml(glyph)}</a:t></a:r>`;
826
- }).join("");
827
- return `<a:p><a:pPr algn="${align}"><a:lnSpc><a:spcPct val="${lineSpacing}"/></a:lnSpc></a:pPr>` + (runs || `<a:r><a:rPr ${textRunProperties}>${solidColorXml(color)}<a:latin typeface="${escapeXml(typeface)}"/></a:rPr><a:t> </a:t></a:r>`) + `</a:p>`;
828
- }
829
- function buildLayerShape(layer, shapeId) {
830
- const rect = toEmuRect(resolveLayerRect(layer.position, 100, 100));
831
- const geometry = layer.content.shape === "circle" ? "ellipse" : layer.content.shape === "line" ? "line" : layer.content.borderRadius ? "roundRect" : "rect";
832
- const fill = layer.content.shape === "line" ? "<a:noFill/>" : fillXml(layer.content.fill, layer.content.fillOpacity, layer.content.gradient);
833
- const stroke = lineXml(
834
- layer.content.stroke ?? (layer.content.shape === "line" ? "#ffffff" : void 0),
835
- layer.content.strokeWidth ?? (layer.content.shape === "line" ? 2 : void 0),
836
- layer.content.borderStyle
837
- );
838
- return `<p:sp><p:nvSpPr><p:cNvPr id="${shapeId}" name="${escapeXml(layer.id)}"/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr><a:xfrm><a:off x="${rect.x}" y="${rect.y}"/><a:ext cx="${rect.width}" cy="${rect.height}"/></a:xfrm><a:prstGeom prst="${geometry}"><a:avLst/></a:prstGeom>` + fill + stroke + `</p:spPr></p:sp>`;
839
- }
840
1836
  function buildTableLayerShapes(layer, ctx) {
841
1837
  const visibleBodyRows = layer.content.maxVisibleRows ? layer.content.rows.slice(0, Math.max(1, Math.floor(layer.content.maxVisibleRows))) : layer.content.rows;
842
1838
  const rows = [layer.content.headers, ...visibleBodyRows];
@@ -928,7 +1924,7 @@ function buildPathLayerShapes(layer, ctx) {
928
1924
  const flipH = x2 < x1 ? ' flipH="1"' : "";
929
1925
  const flipV = y2 < y1 ? ' flipV="1"' : "";
930
1926
  const stroke = parseCssColor(layer.content.stroke, "FFFFFF");
931
- const width = Math.max(1, Math.round((layer.content.strokeWidth ?? 2) * 9525));
1927
+ const width = Math.max(1, Math.round((layer.content.strokeWidth ?? 2) * EMU_PER_CANVAS_PX));
932
1928
  const head = markerXml("headEnd", layer.content.startMarker);
933
1929
  const tail = markerXml("tailEnd", layer.content.endMarker);
934
1930
  const dash = dashXml(layer.content.borderStyle, layer.content.dasharray);
@@ -956,79 +1952,6 @@ function textLayerForPlaceholder(position, text, ctx) {
956
1952
  }
957
1953
  };
958
1954
  }
959
- function parseCssColor(color, fallback) {
960
- if (!color) return { hex: toOoxmlHex(fallback), alpha: 1e5 };
961
- const trimmed = color.trim();
962
- if (trimmed === "transparent" || trimmed === "none") return { hex: "000000", alpha: 0 };
963
- const hexMatch = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(trimmed);
964
- if (hexMatch) {
965
- const raw = hexMatch[1];
966
- const expanded = raw.length === 3 ? raw.split("").map((char) => char + char).join("") : raw;
967
- const alpha = expanded.length === 8 ? Math.round(Number.parseInt(expanded.slice(6, 8), 16) / 255 * 1e5) : 1e5;
968
- return { hex: expanded.slice(0, 6).toUpperCase(), alpha };
969
- }
970
- const rgbMatch = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(trimmed);
971
- if (rgbMatch) {
972
- const channel = (value) => Math.max(0, Math.min(255, Math.round(Number(value)))).toString(16).padStart(2, "0");
973
- const alpha = rgbMatch[4] ? Math.round(Math.max(0, Math.min(1, Number(rgbMatch[4]))) * 1e5) : 1e5;
974
- return {
975
- hex: `${channel(rgbMatch[1])}${channel(rgbMatch[2])}${channel(rgbMatch[3])}`.toUpperCase(),
976
- alpha
977
- };
978
- }
979
- const named = {
980
- black: "000000",
981
- white: "FFFFFF",
982
- red: "FF0000",
983
- blue: "0000FF",
984
- green: "008000",
985
- gray: "808080",
986
- grey: "808080"
987
- };
988
- return { hex: named[trimmed.toLowerCase()] ?? toOoxmlHex(fallback), alpha: 1e5 };
989
- }
990
- function solidColorXml(color) {
991
- const alpha = color.alpha < 1e5 ? `<a:alpha val="${color.alpha}"/>` : "";
992
- return `<a:solidFill><a:srgbClr val="${color.hex}">${alpha}</a:srgbClr></a:solidFill>`;
993
- }
994
- function fillXml(fill, opacity, gradient) {
995
- if (gradient) return gradientFillXml(gradient.from, gradient.to, gradient.angle ?? 0, opacity);
996
- if (!fill || fill === "none" || fill === "transparent") return "<a:noFill/>";
997
- if (fill.includes("gradient(")) {
998
- const colors = fill.match(/#[0-9a-f]{3,8}|rgba?\([^)]*\)/gi) ?? [];
999
- if (colors.length >= 2) {
1000
- const angle = Number.parseFloat(
1001
- /(?:linear-gradient\()\s*([\d.-]+)deg/i.exec(fill)?.[1] ?? "0"
1002
- );
1003
- return gradientFillXml(colors[0], colors[colors.length - 1], angle, opacity);
1004
- }
1005
- }
1006
- const color = parseCssColor(fill, "000000");
1007
- if (opacity !== void 0)
1008
- color.alpha = Math.round(color.alpha * Math.max(0, Math.min(1, opacity)));
1009
- return solidColorXml(color);
1010
- }
1011
- function gradientFillXml(from, to, angle, opacity) {
1012
- const first = parseCssColor(from, "000000");
1013
- const second = parseCssColor(to, first.hex);
1014
- if (opacity !== void 0) {
1015
- const factor = Math.max(0, Math.min(1, opacity));
1016
- first.alpha = Math.round(first.alpha * factor);
1017
- second.alpha = Math.round(second.alpha * factor);
1018
- }
1019
- const ooxmlAngle = Math.round(((90 - angle) % 360 + 360) % 360 * 6e4);
1020
- return `<a:gradFill rotWithShape="1"><a:gsLst><a:gs pos="0"><a:srgbClr val="${first.hex}">${first.alpha < 1e5 ? `<a:alpha val="${first.alpha}"/>` : ""}</a:srgbClr></a:gs><a:gs pos="100000"><a:srgbClr val="${second.hex}">${second.alpha < 1e5 ? `<a:alpha val="${second.alpha}"/>` : ""}</a:srgbClr></a:gs></a:gsLst><a:lin ang="${ooxmlAngle}" scaled="1"/></a:gradFill>`;
1021
- }
1022
- function lineXml(color, widthPx, borderStyle) {
1023
- if (!color || !widthPx || widthPx <= 0) return "<a:ln><a:noFill/></a:ln>";
1024
- const width = Math.max(1, Math.round(widthPx * 9525));
1025
- return `<a:ln w="${width}">${solidColorXml(parseCssColor(color, "000000"))}${dashXml(borderStyle)}</a:ln>`;
1026
- }
1027
- function dashXml(style, dasharray) {
1028
- if (style === "dashed" || dasharray && dasharray !== "none") return '<a:prstDash val="dash"/>';
1029
- if (style === "dotted") return '<a:prstDash val="dot"/>';
1030
- return "";
1031
- }
1032
1955
  function markerXml(tag, marker) {
1033
1956
  if (!marker || marker === "none") return "";
1034
1957
  const type = marker === "arrow" ? "triangle" : marker === "open" ? "arrow" : marker === "diamond" ? "diamond" : marker === "circle" ? "oval" : "square";
@@ -1285,7 +2208,7 @@ function isImageOnlyParagraph(node) {
1285
2208
  function convertParagraph(node, ctx) {
1286
2209
  const soloImage = isImageOnlyParagraph(node);
1287
2210
  if (soloImage && ctx.images) {
1288
- const data = isSvgImageSource(soloImage.url) ? void 0 : ctx.images.get(soloImage.url);
2211
+ const data = isSvgImageSource2(soloImage.url) ? void 0 : ctx.images.get(soloImage.url);
1289
2212
  if (data) {
1290
2213
  ctx.addImage(soloImage.url, data, soloImage.alt || "");
1291
2214
  return "";
@@ -1437,7 +2360,7 @@ function makeHyperlinkRun(text, rId, format) {
1437
2360
  }
1438
2361
  function convertImage(node, format, ctx) {
1439
2362
  if (ctx.images) {
1440
- const data = isSvgImageSource(node.url) ? void 0 : ctx.images.get(node.url);
2363
+ const data = isSvgImageSource2(node.url) ? void 0 : ctx.images.get(node.url);
1441
2364
  if (data) {
1442
2365
  ctx.addImage(node.url, data, node.alt || "");
1443
2366
  return "";
@@ -1446,13 +2369,17 @@ function convertImage(node, format, ctx) {
1446
2369
  const alt = node.alt || node.url;
1447
2370
  return makeRun(`[Image: ${alt}]`, { ...format, italic: true }, ctx.style);
1448
2371
  }
1449
- async function buildPptxPackage(slideXmls, slideContexts, options) {
2372
+ var SEMANTIC_DECK_STRUCTURE = {
2373
+ layoutXmls: [buildSlideLayoutXml("")],
2374
+ masterXml: ([layoutRelId]) => buildSlideMasterXml(layoutRelId),
2375
+ themeXml: buildThemeXml()
2376
+ };
2377
+ async function buildPptxPackage(slideXmls, slideContexts, options, structure = SEMANTIC_DECK_STRUCTURE) {
1450
2378
  const pkg = createPackage();
1451
2379
  const slideCount = slideXmls.length;
1452
2380
  const relIds = new RelIdAllocator();
1453
2381
  const PRESENTATION_PART = "ppt/presentation.xml";
1454
2382
  const SLIDE_MASTER_PART = "ppt/slideMasters/slideMaster1.xml";
1455
- const SLIDE_LAYOUT_PART = "ppt/slideLayouts/slideLayout1.xml";
1456
2383
  const slideRelIds = [];
1457
2384
  for (let i = 0; i < slideCount; i++) {
1458
2385
  slideRelIds.push(relIds.alloc(PRESENTATION_PART));
@@ -1491,30 +2418,36 @@ async function buildPptxPackage(slideXmls, slideContexts, options) {
1491
2418
  pkg.addBinaryPart(img.mediaPath, img.data, img.contentType);
1492
2419
  }
1493
2420
  }
1494
- const layoutMasterRelId = relIds.alloc(SLIDE_LAYOUT_PART);
1495
- const slideLayoutXml = buildSlideLayoutXml(layoutMasterRelId);
1496
- pkg.addPart(SLIDE_LAYOUT_PART, slideLayoutXml, CONTENT_TYPE_PPTX_SLIDE_LAYOUT);
1497
- pkg.addRelationship(SLIDE_LAYOUT_PART, {
1498
- id: layoutMasterRelId,
1499
- type: REL_SLIDE_MASTER,
1500
- target: "../slideMasters/slideMaster1.xml"
2421
+ const masterLayoutRelIds = [];
2422
+ structure.layoutXmls.forEach((layoutXml, index) => {
2423
+ const layoutPart = `ppt/slideLayouts/slideLayout${index + 1}.xml`;
2424
+ const layoutMasterRelId = relIds.alloc(layoutPart);
2425
+ pkg.addPart(layoutPart, layoutXml, CONTENT_TYPE_PPTX_SLIDE_LAYOUT);
2426
+ pkg.addRelationship(layoutPart, {
2427
+ id: layoutMasterRelId,
2428
+ type: REL_SLIDE_MASTER,
2429
+ target: "../slideMasters/slideMaster1.xml"
2430
+ });
2431
+ const masterLayoutRelId = relIds.alloc(SLIDE_MASTER_PART);
2432
+ masterLayoutRelIds.push(masterLayoutRelId);
2433
+ pkg.addRelationship(SLIDE_MASTER_PART, {
2434
+ id: masterLayoutRelId,
2435
+ type: REL_SLIDE_LAYOUT,
2436
+ target: `../slideLayouts/slideLayout${index + 1}.xml`
2437
+ });
1501
2438
  });
1502
- const masterLayoutRelId = relIds.alloc(SLIDE_MASTER_PART);
1503
2439
  const masterThemeRelId = relIds.alloc(SLIDE_MASTER_PART);
1504
- const slideMasterXml = buildSlideMasterXml(masterLayoutRelId);
1505
- pkg.addPart(SLIDE_MASTER_PART, slideMasterXml, CONTENT_TYPE_PPTX_SLIDE_MASTER);
1506
- pkg.addRelationship(SLIDE_MASTER_PART, {
1507
- id: masterLayoutRelId,
1508
- type: REL_SLIDE_LAYOUT,
1509
- target: "../slideLayouts/slideLayout1.xml"
1510
- });
2440
+ pkg.addPart(
2441
+ SLIDE_MASTER_PART,
2442
+ structure.masterXml(masterLayoutRelIds),
2443
+ CONTENT_TYPE_PPTX_SLIDE_MASTER
2444
+ );
1511
2445
  pkg.addRelationship(SLIDE_MASTER_PART, {
1512
2446
  id: masterThemeRelId,
1513
2447
  type: REL_THEME,
1514
2448
  target: "../theme/theme1.xml"
1515
2449
  });
1516
- const themeXml = buildThemeXml();
1517
- pkg.addPart("ppt/theme/theme1.xml", themeXml, CONTENT_TYPE_PPTX_THEME);
2450
+ pkg.addPart("ppt/theme/theme1.xml", structure.themeXml, CONTENT_TYPE_PPTX_THEME);
1518
2451
  pkg.addRelationship("", {
1519
2452
  id: "rId1",
1520
2453
  type: REL_OFFICE_DOCUMENT,