@markdstage/markdstage 3.0.0 → 3.1.0
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.
- package/package.json +1 -1
- package/shared/README.md +13 -6
- package/shared/renderer/renderer.js +676 -57
- package/shared/renderer/slides.css +19 -0
- package/shared/runtime/browser.mjs +98 -7
- package/shared/runtime/output.mjs +232 -50
- package/shared/runtime/pptx-package.mjs +404 -67
|
@@ -276,6 +276,12 @@ body.mermaid-loading .mermaid{visibility:hidden;}
|
|
|
276
276
|
.architecture-routing-warning span{font-family:"Cascadia Code","Cascadia Mono",Consolas,monospace;
|
|
277
277
|
font-size:.85em;overflow-wrap:anywhere;}
|
|
278
278
|
hr{border:0;border-top:1px solid var(--border);margin:.7em 0;}
|
|
279
|
+
/* Flex item margins do not collapse, so regular slide bodies use their gap as
|
|
280
|
+
the single source of vertical spacing. Nested component spacing is unchanged. */
|
|
281
|
+
.deck:not(.title-slide):not(.section-slide):not(.backcover-slide)>.body>:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table):has(+:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table)),
|
|
282
|
+
.deck:not(.title-slide):not(.section-slide):not(.backcover-slide)>.body>:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table)+:where(p,h1,h2,h3,h4,h5,h6,ul,ol,blockquote,pre,table){
|
|
283
|
+
margin-block:0;
|
|
284
|
+
}
|
|
279
285
|
footer{flex:0 0 auto;display:flex;justify-content:space-between;align-items:center;
|
|
280
286
|
color:var(--muted);font-size:clamp(12px,1.7vh,16px);
|
|
281
287
|
margin-top:clamp(12px,2.2vh,26px);border-top:1px solid var(--border);padding-top:12px;}
|
|
@@ -656,6 +662,7 @@ body.pptx-artwork-mode [data-pptx-native="table"] *{
|
|
|
656
662
|
}
|
|
657
663
|
body.pptx-artwork-mode li[data-pptx-native="text"]::marker{color:transparent!important;}
|
|
658
664
|
body.pptx-artwork-mode [data-pptx-native="table"]{visibility:hidden!important;}
|
|
665
|
+
body.pptx-artwork-mode pre[data-pptx-native="code"]{visibility:hidden!important;}
|
|
659
666
|
body.pptx-artwork-mode img[data-pptx-native="image"]{
|
|
660
667
|
object-fit:none!important;
|
|
661
668
|
object-position:99999px 99999px!important;
|
|
@@ -674,6 +681,18 @@ body.pptx-artwork-mode svg [data-pptx-native="table"]{
|
|
|
674
681
|
}
|
|
675
682
|
body.pptx-artwork-mode svg path[data-pptx-native="connector"]{marker-end:none!important;}
|
|
676
683
|
body.pptx-artwork-mode svg [data-pptx-native="image"]{opacity:0!important;}
|
|
684
|
+
body.pptx-artwork-mode [data-pptx-shadow-fallback]{box-shadow:none!important;}
|
|
685
|
+
body.pptx-artwork-mode .pptx-fallback-hidden{visibility:hidden!important;}
|
|
686
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template){
|
|
687
|
+
background:transparent!important;
|
|
688
|
+
}
|
|
689
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)::before{
|
|
690
|
+
display:none!important;
|
|
691
|
+
}
|
|
692
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-background,
|
|
693
|
+
body.pptx-slide-artwork-mode .deck:not(.pptx-layout-template)>.theme-cover-logo{
|
|
694
|
+
visibility:hidden!important;
|
|
695
|
+
}
|
|
677
696
|
|
|
678
697
|
/* ===== Headless PDF export ===== */
|
|
679
698
|
@page{size:13.333333in 7.5in;margin:0;}
|
|
@@ -453,7 +453,13 @@ export async function runPptxOutputBrowser(browser, pageUrl, profileDir, job, to
|
|
|
453
453
|
);
|
|
454
454
|
}
|
|
455
455
|
const model = evaluated.result?.value;
|
|
456
|
-
if (
|
|
456
|
+
if (
|
|
457
|
+
!model ||
|
|
458
|
+
!Array.isArray(model.masters) ||
|
|
459
|
+
!Array.isArray(model.layouts) ||
|
|
460
|
+
!Array.isArray(model.slides) ||
|
|
461
|
+
model.slides.length !== total
|
|
462
|
+
) {
|
|
457
463
|
throw new Error("The renderer returned an invalid PowerPoint export model.");
|
|
458
464
|
}
|
|
459
465
|
|
|
@@ -463,26 +469,111 @@ export async function runPptxOutputBrowser(browser, pageUrl, profileDir, job, to
|
|
|
463
469
|
awaitPromise: true,
|
|
464
470
|
});
|
|
465
471
|
|
|
466
|
-
const
|
|
467
|
-
for (
|
|
472
|
+
const layoutArtworks = [];
|
|
473
|
+
for (const [index, layout] of model.layouts.entries()) {
|
|
474
|
+
const captureIndex = Number.isInteger(layout.captureIndex)
|
|
475
|
+
? layout.captureIndex
|
|
476
|
+
: total + index;
|
|
468
477
|
const screenshot = await cdp.send("Page.captureScreenshot", {
|
|
469
478
|
format: "png",
|
|
470
479
|
fromSurface: true,
|
|
471
480
|
captureBeyondViewport: true,
|
|
472
481
|
clip: {
|
|
473
482
|
x: 0,
|
|
474
|
-
y:
|
|
483
|
+
y: captureIndex * 720,
|
|
475
484
|
width: 1280,
|
|
476
485
|
height: 720,
|
|
477
486
|
scale: 1,
|
|
478
487
|
},
|
|
479
488
|
});
|
|
480
489
|
if (typeof screenshot.data !== "string" || screenshot.data.length === 0) {
|
|
481
|
-
throw new Error(`Chromium did not return
|
|
490
|
+
throw new Error(`Chromium did not return artwork for PowerPoint layout ${layout.id}.`);
|
|
482
491
|
}
|
|
483
|
-
|
|
492
|
+
layoutArtworks.push(Buffer.from(screenshot.data, "base64"));
|
|
484
493
|
}
|
|
485
|
-
|
|
494
|
+
|
|
495
|
+
await cdp.send("Runtime.evaluate", {
|
|
496
|
+
expression:
|
|
497
|
+
"document.body.classList.remove('pptx-layout-artwork-mode');" +
|
|
498
|
+
"document.body.classList.add('pptx-slide-artwork-mode');" +
|
|
499
|
+
"new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)))",
|
|
500
|
+
awaitPromise: true,
|
|
501
|
+
});
|
|
502
|
+
await cdp.send("Emulation.setDefaultBackgroundColorOverride", {
|
|
503
|
+
color: { r: 0, g: 0, b: 0, a: 0 },
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const slideFallbackImages = [];
|
|
507
|
+
for (const [slideIndex, slide] of model.slides.entries()) {
|
|
508
|
+
const images = [];
|
|
509
|
+
const fallbacks = Array.isArray(slide.fallbacks) ? slide.fallbacks : [];
|
|
510
|
+
for (const [fallbackIndex, fallback] of fallbacks.entries()) {
|
|
511
|
+
if (fallback?.artwork === false) continue;
|
|
512
|
+
if (typeof fallback?.captureId !== "string" || !fallback.captureId) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
`PowerPoint fallback ${fallbackIndex + 1} on slide ${slideIndex + 1} is missing its capture id.`,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
const left = Math.max(0, Number(fallback?.x));
|
|
518
|
+
const top = Math.max(0, Number(fallback?.y));
|
|
519
|
+
const right = Math.min(1280, Number(fallback?.x) + Number(fallback?.width));
|
|
520
|
+
const bottom = Math.min(720, Number(fallback?.y) + Number(fallback?.height));
|
|
521
|
+
if (
|
|
522
|
+
![left, top, right, bottom].every(Number.isFinite) ||
|
|
523
|
+
right <= left ||
|
|
524
|
+
bottom <= top
|
|
525
|
+
) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`PowerPoint fallback ${fallbackIndex + 1} on slide ${slideIndex + 1} has invalid bounds.`,
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
const bounds = {
|
|
531
|
+
x: left,
|
|
532
|
+
y: top,
|
|
533
|
+
width: right - left,
|
|
534
|
+
height: bottom - top,
|
|
535
|
+
};
|
|
536
|
+
await cdp.send("Runtime.evaluate", {
|
|
537
|
+
expression: `(() => {
|
|
538
|
+
const active = ${JSON.stringify(fallback.captureId)};
|
|
539
|
+
for (const element of document.querySelectorAll("[data-pptx-fallback-ids]")) {
|
|
540
|
+
const ids = (element.getAttribute("data-pptx-fallback-ids") || "").split(/\\s+/);
|
|
541
|
+
element.classList.toggle("pptx-fallback-hidden", !ids.includes(active));
|
|
542
|
+
}
|
|
543
|
+
return new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
544
|
+
})()`,
|
|
545
|
+
awaitPromise: true,
|
|
546
|
+
});
|
|
547
|
+
const screenshot = await cdp.send("Page.captureScreenshot", {
|
|
548
|
+
format: "png",
|
|
549
|
+
fromSurface: true,
|
|
550
|
+
captureBeyondViewport: true,
|
|
551
|
+
clip: {
|
|
552
|
+
x: bounds.x,
|
|
553
|
+
y: slideIndex * 720 + bounds.y,
|
|
554
|
+
width: bounds.width,
|
|
555
|
+
height: bounds.height,
|
|
556
|
+
scale: 1,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
if (typeof screenshot.data !== "string" || screenshot.data.length === 0) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Chromium did not return fallback artwork ${fallbackIndex + 1} for slide ${slideIndex + 1}.`,
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
images.push({
|
|
565
|
+
fallbackIndex,
|
|
566
|
+
...bounds,
|
|
567
|
+
data: Buffer.from(screenshot.data, "base64"),
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
slideFallbackImages.push(images);
|
|
571
|
+
}
|
|
572
|
+
await cdp.send("Runtime.evaluate", {
|
|
573
|
+
expression:
|
|
574
|
+
'document.querySelectorAll(".pptx-fallback-hidden").forEach(element => element.classList.remove("pptx-fallback-hidden"));',
|
|
575
|
+
});
|
|
576
|
+
return { model, layoutArtworks, slideFallbackImages };
|
|
486
577
|
} finally {
|
|
487
578
|
await closeCdpOutputPage(cdp, child);
|
|
488
579
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
10
10
|
import { basename, extname, join } from "node:path";
|
|
11
11
|
import { tmpdir } from "node:os";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
13
|
import { getOutputSnapshotSlides } from "../deck-state.mjs";
|
|
14
14
|
import { normalizeTheme } from "../renderer/theme.mjs";
|
|
15
15
|
import { MarkdStageError } from "./errors.mjs";
|
|
@@ -81,13 +81,21 @@ export function createOutputJob(snapshot, kind) {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function decodeDataImage(source) {
|
|
84
|
-
const match = /^data:(image\/(?:png|jpeg|gif))(
|
|
84
|
+
const match = /^data:(image\/(?:png|jpeg|gif|svg\+xml))((?:;[^,]*)*),([\s\S]*)$/i.exec(source);
|
|
85
85
|
if (!match) {
|
|
86
|
-
throw new Error("Only PNG, JPEG, or
|
|
86
|
+
throw new Error("Only PNG, JPEG, GIF, or SVG data URLs can be embedded in PowerPoint.");
|
|
87
87
|
}
|
|
88
|
-
const
|
|
88
|
+
const parameters = match[2]
|
|
89
|
+
.split(";")
|
|
90
|
+
.map((parameter) => parameter.trim().toLowerCase())
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
const base64 = parameters.includes("base64");
|
|
93
|
+
const data = base64
|
|
89
94
|
? Buffer.from(match[3], "base64")
|
|
90
|
-
: Buffer.from(
|
|
95
|
+
: Buffer.from(
|
|
96
|
+
decodeURIComponent(match[3]),
|
|
97
|
+
match[1].toLowerCase() === "image/svg+xml" ? "utf8" : "binary",
|
|
98
|
+
);
|
|
91
99
|
return { data, contentType: match[1].toLowerCase() };
|
|
92
100
|
}
|
|
93
101
|
|
|
@@ -126,7 +134,7 @@ async function loadPptxImage(inst, source, fetchImpl, currentTotal) {
|
|
|
126
134
|
const data = Buffer.from(await response.arrayBuffer());
|
|
127
135
|
ensurePptxAssetSize(data, source, currentTotal);
|
|
128
136
|
const responseType = response.headers.get("content-type")?.split(";")[0].trim().toLowerCase();
|
|
129
|
-
const contentType = ["image/png", "image/jpeg", "image/gif"].includes(responseType)
|
|
137
|
+
const contentType = ["image/png", "image/jpeg", "image/gif", "image/svg+xml"].includes(responseType)
|
|
130
138
|
? responseType
|
|
131
139
|
: undefined;
|
|
132
140
|
return { data, contentType };
|
|
@@ -135,7 +143,8 @@ async function loadPptxImage(inst, source, fetchImpl, currentTotal) {
|
|
|
135
143
|
export async function preparePptxPackageModel(
|
|
136
144
|
inst,
|
|
137
145
|
model,
|
|
138
|
-
|
|
146
|
+
layoutArtworks,
|
|
147
|
+
slideFallbackImages,
|
|
139
148
|
fetchImpl = fetch,
|
|
140
149
|
) {
|
|
141
150
|
if (
|
|
@@ -143,29 +152,148 @@ export async function preparePptxPackageModel(
|
|
|
143
152
|
model.version !== 1 ||
|
|
144
153
|
model.width !== PPTX_DIMENSIONS.widthPx ||
|
|
145
154
|
model.height !== PPTX_DIMENSIONS.heightPx ||
|
|
155
|
+
!Array.isArray(model.masters) ||
|
|
156
|
+
model.masters.length === 0 ||
|
|
157
|
+
!Array.isArray(model.layouts) ||
|
|
158
|
+
model.layouts.length === 0 ||
|
|
146
159
|
!Array.isArray(model.slides) ||
|
|
147
160
|
model.slides.length === 0
|
|
148
161
|
) {
|
|
149
162
|
throw new Error("The renderer returned an unsupported PowerPoint export model.");
|
|
150
163
|
}
|
|
151
|
-
if (!Array.isArray(
|
|
152
|
-
throw new Error("PowerPoint
|
|
164
|
+
if (!Array.isArray(layoutArtworks) || layoutArtworks.length !== model.layouts.length) {
|
|
165
|
+
throw new Error("PowerPoint layout artwork does not match the layout count.");
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
!Array.isArray(slideFallbackImages) ||
|
|
169
|
+
slideFallbackImages.length !== model.slides.length ||
|
|
170
|
+
slideFallbackImages.some((images) => !Array.isArray(images))
|
|
171
|
+
) {
|
|
172
|
+
throw new Error("PowerPoint fallback images do not match the slide count.");
|
|
153
173
|
}
|
|
154
174
|
|
|
155
175
|
const assets = [];
|
|
156
176
|
const sourceAssets = new Map();
|
|
177
|
+
const pngAssets = new Map();
|
|
157
178
|
let totalAssetBytes = 0;
|
|
158
|
-
|
|
159
|
-
if (!Buffer.isBuffer(
|
|
160
|
-
throw new Error(
|
|
179
|
+
const addPngAsset = (id, data, label) => {
|
|
180
|
+
if (!Buffer.isBuffer(data)) {
|
|
181
|
+
throw new Error(`${label} is invalid.`);
|
|
161
182
|
}
|
|
162
|
-
|
|
163
|
-
|
|
183
|
+
const key = createHash("sha256").update(data).digest("hex");
|
|
184
|
+
const existing = pngAssets.get(key);
|
|
185
|
+
if (existing) return existing;
|
|
186
|
+
ensurePptxAssetSize(data, label, totalAssetBytes);
|
|
187
|
+
totalAssetBytes += data.length;
|
|
164
188
|
assets.push({
|
|
165
|
-
id
|
|
189
|
+
id,
|
|
166
190
|
contentType: "image/png",
|
|
167
|
-
data
|
|
191
|
+
data,
|
|
168
192
|
});
|
|
193
|
+
pngAssets.set(key, id);
|
|
194
|
+
return id;
|
|
195
|
+
};
|
|
196
|
+
const prepareImage = async (sourceElement) => {
|
|
197
|
+
const source = sourceElement.src;
|
|
198
|
+
let assetId = sourceAssets.get(source);
|
|
199
|
+
if (!assetId) {
|
|
200
|
+
const loaded = await loadPptxImage(inst, source, fetchImpl, totalAssetBytes);
|
|
201
|
+
totalAssetBytes += loaded.data.length;
|
|
202
|
+
assetId = `markdstage-image-${sourceAssets.size + 1}`;
|
|
203
|
+
sourceAssets.set(source, assetId);
|
|
204
|
+
assets.push({ id: assetId, ...loaded });
|
|
205
|
+
}
|
|
206
|
+
const { src: _src, source: _source, ...image } = sourceElement;
|
|
207
|
+
if (
|
|
208
|
+
(image.fit === "contain" || image.fit === "scale-down") &&
|
|
209
|
+
image.naturalWidth > 0 &&
|
|
210
|
+
image.naturalHeight > 0
|
|
211
|
+
) {
|
|
212
|
+
const scale = Math.min(
|
|
213
|
+
image.width / image.naturalWidth,
|
|
214
|
+
image.height / image.naturalHeight,
|
|
215
|
+
image.fit === "scale-down" ? 1 : Number.POSITIVE_INFINITY,
|
|
216
|
+
);
|
|
217
|
+
const width = image.naturalWidth * scale;
|
|
218
|
+
const height = image.naturalHeight * scale;
|
|
219
|
+
image.x += (image.width - width) / 2;
|
|
220
|
+
image.y += (image.height - height) / 2;
|
|
221
|
+
image.width = width;
|
|
222
|
+
image.height = height;
|
|
223
|
+
}
|
|
224
|
+
return { ...image, assetId };
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const layouts = [];
|
|
228
|
+
const layoutById = new Map();
|
|
229
|
+
for (const [index, sourceLayout] of model.layouts.entries()) {
|
|
230
|
+
if (
|
|
231
|
+
!sourceLayout ||
|
|
232
|
+
typeof sourceLayout.id !== "string" ||
|
|
233
|
+
!sourceLayout.id ||
|
|
234
|
+
typeof sourceLayout.name !== "string" ||
|
|
235
|
+
!sourceLayout.name ||
|
|
236
|
+
typeof sourceLayout.theme !== "string" ||
|
|
237
|
+
!sourceLayout.theme
|
|
238
|
+
) {
|
|
239
|
+
throw new Error(`PowerPoint layout ${index + 1} is invalid.`);
|
|
240
|
+
}
|
|
241
|
+
if (layoutById.has(sourceLayout.id)) {
|
|
242
|
+
throw new Error(`PowerPoint layout id is duplicated: ${sourceLayout.id}`);
|
|
243
|
+
}
|
|
244
|
+
const artworkAssetId = addPngAsset(
|
|
245
|
+
`markdstage-layout-${index + 1}`,
|
|
246
|
+
layoutArtworks[index],
|
|
247
|
+
`PowerPoint artwork for layout ${sourceLayout.id}`,
|
|
248
|
+
);
|
|
249
|
+
const sourceElements = sourceLayout.elements ?? [];
|
|
250
|
+
if (!Array.isArray(sourceElements) || sourceElements.some((element) => element?.type !== "image")) {
|
|
251
|
+
throw new Error(`PowerPoint layout ${sourceLayout.id} has an invalid element list.`);
|
|
252
|
+
}
|
|
253
|
+
const elements = [];
|
|
254
|
+
for (const sourceElement of sourceElements) {
|
|
255
|
+
elements.push(await prepareImage(sourceElement));
|
|
256
|
+
}
|
|
257
|
+
const layout = {
|
|
258
|
+
id: sourceLayout.id,
|
|
259
|
+
name: sourceLayout.name,
|
|
260
|
+
theme: sourceLayout.theme,
|
|
261
|
+
artworkAssetId,
|
|
262
|
+
elements,
|
|
263
|
+
};
|
|
264
|
+
layoutById.set(layout.id, layout);
|
|
265
|
+
layouts.push(layout);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const masters = model.masters.map((sourceMaster, index) => {
|
|
269
|
+
if (
|
|
270
|
+
!sourceMaster ||
|
|
271
|
+
typeof sourceMaster.id !== "string" ||
|
|
272
|
+
!sourceMaster.id ||
|
|
273
|
+
typeof sourceMaster.theme !== "string" ||
|
|
274
|
+
!sourceMaster.theme ||
|
|
275
|
+
!Array.isArray(sourceMaster.layoutIds) ||
|
|
276
|
+
sourceMaster.layoutIds.length === 0
|
|
277
|
+
) {
|
|
278
|
+
throw new Error(`PowerPoint master ${index + 1} is invalid.`);
|
|
279
|
+
}
|
|
280
|
+
const layoutIds = sourceMaster.layoutIds.map((layoutId) => {
|
|
281
|
+
const layout = layoutById.get(layoutId);
|
|
282
|
+
if (!layout || layout.theme !== sourceMaster.theme) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`PowerPoint master ${sourceMaster.id} references invalid layout ${layoutId}.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return layoutId;
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
id: sourceMaster.id,
|
|
291
|
+
theme: sourceMaster.theme,
|
|
292
|
+
layoutIds,
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
if (new Set(masters.map((master) => master.id)).size !== masters.length) {
|
|
296
|
+
throw new Error("PowerPoint master ids must be unique.");
|
|
169
297
|
}
|
|
170
298
|
|
|
171
299
|
const slides = [];
|
|
@@ -173,6 +301,12 @@ export async function preparePptxPackageModel(
|
|
|
173
301
|
if (!sourceSlide || !Array.isArray(sourceSlide.elements)) {
|
|
174
302
|
throw new Error(`PowerPoint slide ${slideIndex + 1} has an invalid element list.`);
|
|
175
303
|
}
|
|
304
|
+
if (
|
|
305
|
+
typeof sourceSlide.layoutId !== "string" ||
|
|
306
|
+
!layoutById.has(sourceSlide.layoutId)
|
|
307
|
+
) {
|
|
308
|
+
throw new Error(`PowerPoint slide ${slideIndex + 1} references an invalid layout.`);
|
|
309
|
+
}
|
|
176
310
|
if (sourceSlide.notes !== undefined && typeof sourceSlide.notes !== "string") {
|
|
177
311
|
throw new Error(`PowerPoint slide ${slideIndex + 1} has invalid speaker notes.`);
|
|
178
312
|
}
|
|
@@ -182,42 +316,75 @@ export async function preparePptxPackageModel(
|
|
|
182
316
|
elements.push(sourceElement);
|
|
183
317
|
continue;
|
|
184
318
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
319
|
+
elements.push(await prepareImage(sourceElement));
|
|
320
|
+
}
|
|
321
|
+
const fallbacks = Array.isArray(sourceSlide.fallbacks) ? sourceSlide.fallbacks : [];
|
|
322
|
+
const expectedFallbackIndexes = fallbacks
|
|
323
|
+
.map((fallback, fallbackIndex) => ({ fallback, fallbackIndex }))
|
|
324
|
+
.filter(({ fallback }) => fallback?.artwork !== false);
|
|
325
|
+
const captures = slideFallbackImages[slideIndex];
|
|
326
|
+
if (captures.length !== expectedFallbackIndexes.length) {
|
|
327
|
+
throw new Error(
|
|
328
|
+
`PowerPoint fallback images do not match slide ${slideIndex + 1}.`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const captureByFallback = new Map();
|
|
332
|
+
for (const capture of captures) {
|
|
195
333
|
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
334
|
+
!capture ||
|
|
335
|
+
!Number.isInteger(capture.fallbackIndex) ||
|
|
336
|
+
captureByFallback.has(capture.fallbackIndex)
|
|
199
337
|
) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
338
|
+
throw new Error(`PowerPoint fallback image for slide ${slideIndex + 1} is invalid.`);
|
|
339
|
+
}
|
|
340
|
+
captureByFallback.set(capture.fallbackIndex, capture);
|
|
341
|
+
}
|
|
342
|
+
const fallbackElements = [];
|
|
343
|
+
for (const { fallback, fallbackIndex } of expectedFallbackIndexes) {
|
|
344
|
+
const capture = captureByFallback.get(fallbackIndex);
|
|
345
|
+
if (!capture) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`PowerPoint fallback image ${fallbackIndex + 1} for slide ${slideIndex + 1} is missing.`,
|
|
204
348
|
);
|
|
205
|
-
const width = image.naturalWidth * scale;
|
|
206
|
-
const height = image.naturalHeight * scale;
|
|
207
|
-
image.x += (image.width - width) / 2;
|
|
208
|
-
image.y += (image.height - height) / 2;
|
|
209
|
-
image.width = width;
|
|
210
|
-
image.height = height;
|
|
211
349
|
}
|
|
212
|
-
|
|
350
|
+
const assetId = addPngAsset(
|
|
351
|
+
`markdstage-slide-${slideIndex + 1}-fallback-${fallbackIndex + 1}`,
|
|
352
|
+
capture.data,
|
|
353
|
+
`PowerPoint fallback image ${fallbackIndex + 1} for slide ${slideIndex + 1}`,
|
|
354
|
+
);
|
|
355
|
+
fallbackElements.push({
|
|
356
|
+
type: "image",
|
|
357
|
+
path: fallback.path,
|
|
358
|
+
name: `${fallback.type || "Fallback"} artwork`,
|
|
359
|
+
x: capture.x,
|
|
360
|
+
y: capture.y,
|
|
361
|
+
width: capture.width,
|
|
362
|
+
height: capture.height,
|
|
363
|
+
fit: "fill",
|
|
364
|
+
opacity: 1,
|
|
365
|
+
...(Number.isFinite(fallback.zOrder) ? { zOrder: fallback.zOrder } : {}),
|
|
366
|
+
assetId,
|
|
367
|
+
});
|
|
213
368
|
}
|
|
369
|
+
const orderedElements = [...fallbackElements, ...elements]
|
|
370
|
+
.map((element, elementIndex) => ({ element, elementIndex }))
|
|
371
|
+
.sort((left, right) => {
|
|
372
|
+
const leftOrder = Number.isFinite(left.element.zOrder)
|
|
373
|
+
? left.element.zOrder
|
|
374
|
+
: Number.POSITIVE_INFINITY;
|
|
375
|
+
const rightOrder = Number.isFinite(right.element.zOrder)
|
|
376
|
+
? right.element.zOrder
|
|
377
|
+
: Number.POSITIVE_INFINITY;
|
|
378
|
+
return leftOrder - rightOrder || left.elementIndex - right.elementIndex;
|
|
379
|
+
})
|
|
380
|
+
.map(({ element }) => element);
|
|
214
381
|
slides.push({
|
|
215
|
-
|
|
382
|
+
layoutId: sourceSlide.layoutId,
|
|
216
383
|
...(sourceSlide.notes ? { notes: sourceSlide.notes } : {}),
|
|
217
|
-
elements,
|
|
384
|
+
elements: orderedElements,
|
|
218
385
|
});
|
|
219
386
|
}
|
|
220
|
-
return { slides, assets };
|
|
387
|
+
return { masters, layouts, slides, assets };
|
|
221
388
|
}
|
|
222
389
|
|
|
223
390
|
async function runLayoutInspectionJob(inst, snapshot, browser) {
|
|
@@ -585,14 +752,19 @@ export async function exportPptx(
|
|
|
585
752
|
inst.exportJobs.set(token, job);
|
|
586
753
|
|
|
587
754
|
const pageUrl = pageUrlFor(inst, { pptx: 1, token });
|
|
588
|
-
const { model,
|
|
755
|
+
const { model, layoutArtworks, slideFallbackImages } = await runBrowser(
|
|
589
756
|
browser,
|
|
590
757
|
pageUrl,
|
|
591
758
|
profileDir,
|
|
592
759
|
job,
|
|
593
760
|
snapshot.slides.length,
|
|
594
761
|
);
|
|
595
|
-
const packageModel = await prepareModel(
|
|
762
|
+
const packageModel = await prepareModel(
|
|
763
|
+
inst,
|
|
764
|
+
model,
|
|
765
|
+
layoutArtworks,
|
|
766
|
+
slideFallbackImages,
|
|
767
|
+
);
|
|
596
768
|
const buffer = buildPackage({
|
|
597
769
|
title: model.slides[0]?.title || outputBase,
|
|
598
770
|
...packageModel,
|
|
@@ -605,6 +777,8 @@ export async function exportPptx(
|
|
|
605
777
|
!packageSummary.valid ||
|
|
606
778
|
packageSummary.slideCount !== snapshot.slides.length ||
|
|
607
779
|
packageSummary.notesCount !== expectedNotes ||
|
|
780
|
+
packageSummary.masterCount !== model.masters.length ||
|
|
781
|
+
packageSummary.layoutCount !== model.layouts.length ||
|
|
608
782
|
packageSummary.dimensions.widthEmu !== PPTX_DIMENSIONS.widthEmu ||
|
|
609
783
|
packageSummary.dimensions.heightEmu !== PPTX_DIMENSIONS.heightEmu
|
|
610
784
|
) {
|
|
@@ -615,11 +789,19 @@ export async function exportPptx(
|
|
|
615
789
|
await rename(temporaryOutputPath, outputPath);
|
|
616
790
|
temporaryOutputPath = "";
|
|
617
791
|
const fallbacks = model.slides.flatMap((slide, slideIndex) =>
|
|
618
|
-
(Array.isArray(slide.fallbacks) ? slide.fallbacks : []).map((fallback) =>
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
792
|
+
(Array.isArray(slide.fallbacks) ? slide.fallbacks : []).map((fallback) => {
|
|
793
|
+
const {
|
|
794
|
+
artwork: _artwork,
|
|
795
|
+
captureId: _captureId,
|
|
796
|
+
zOrder: _zOrder,
|
|
797
|
+
...reportedFallback
|
|
798
|
+
} = fallback;
|
|
799
|
+
return {
|
|
800
|
+
slideIndex,
|
|
801
|
+
page: slideIndex + 1,
|
|
802
|
+
...reportedFallback,
|
|
803
|
+
};
|
|
804
|
+
}),
|
|
623
805
|
);
|
|
624
806
|
logFor(
|
|
625
807
|
inst,
|