@openpresentation/opf-pptx 0.0.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.
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +1776 -0
- package/package.json +57 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1776 @@
|
|
|
1
|
+
import PptxGenJS from "pptxgenjs";
|
|
2
|
+
import { unzipSync, zipSync } from "fflate";
|
|
3
|
+
import { XMLParser } from "fast-xml-parser";
|
|
4
|
+
import {
|
|
5
|
+
catalogs as bundledCatalogs,
|
|
6
|
+
validatePresentation
|
|
7
|
+
} from "@openpresentation/opf";
|
|
8
|
+
|
|
9
|
+
export const packageName = "@openpresentation/opf-pptx";
|
|
10
|
+
|
|
11
|
+
export const releaseLane = Object.freeze({
|
|
12
|
+
githubRepository: "OpenPresentation/opf-pptx",
|
|
13
|
+
npmPackage: "@openpresentation/opf-pptx",
|
|
14
|
+
compatibilityPackage: "@openpresentation/opf",
|
|
15
|
+
rendererPackage: "@openpresentation/opf-render"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const runtimePolicy = Object.freeze({
|
|
19
|
+
hostedServiceInCriticalPath: false,
|
|
20
|
+
telemetry: false,
|
|
21
|
+
commercialSdkInCriticalPath: false,
|
|
22
|
+
requiredAiDependency: false,
|
|
23
|
+
requiredLibreOfficeDependency: false,
|
|
24
|
+
requiredNetworkCalls: false,
|
|
25
|
+
deterministicLocalExecution: true
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export class OPFPptxError extends Error {
|
|
29
|
+
constructor(code, message, details = {}) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "OPFPptxError";
|
|
32
|
+
this.code = code;
|
|
33
|
+
this.details = details;
|
|
34
|
+
if (details.issues) this.issues = details.issues;
|
|
35
|
+
if (details.path) this.path = details.path;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const FIXED_TIMESTAMP = "1980-01-01T00:00:00Z";
|
|
40
|
+
// fflate derives the DOS zip date from local-time fields, so build this from
|
|
41
|
+
// local components: a UTC instant rolls back to 1979 west of UTC (below
|
|
42
|
+
// fflate's 1980 floor) and would otherwise yield timezone-dependent bytes.
|
|
43
|
+
const FIXED_ZIP_DATE = new Date(1980, 0, 1, 0, 0, 0);
|
|
44
|
+
const DEFAULT_SEED = 0x4f504658;
|
|
45
|
+
const CANONICAL_SCHEMA = "https://openpresentation.org/schema/opf/v1";
|
|
46
|
+
const EMUS_PER_INCH = 914400;
|
|
47
|
+
|
|
48
|
+
const xmlParser = new XMLParser({
|
|
49
|
+
ignoreAttributes: false,
|
|
50
|
+
attributeNamePrefix: "",
|
|
51
|
+
textNodeName: "#text",
|
|
52
|
+
parseAttributeValue: false,
|
|
53
|
+
parseTagValue: false,
|
|
54
|
+
trimValues: false
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const ROOT_PAYLOAD_FIELDS = [
|
|
58
|
+
"text",
|
|
59
|
+
"items",
|
|
60
|
+
"bullets",
|
|
61
|
+
"image",
|
|
62
|
+
"video",
|
|
63
|
+
"chart",
|
|
64
|
+
"table",
|
|
65
|
+
"code",
|
|
66
|
+
"metric",
|
|
67
|
+
"quote",
|
|
68
|
+
"timeline"
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const PROMOTED_REGION_KEYS = [
|
|
72
|
+
"left",
|
|
73
|
+
"center",
|
|
74
|
+
"right",
|
|
75
|
+
"left+center",
|
|
76
|
+
"center+right",
|
|
77
|
+
"left+center+right",
|
|
78
|
+
"top",
|
|
79
|
+
"middle",
|
|
80
|
+
"bottom",
|
|
81
|
+
"top+middle",
|
|
82
|
+
"middle+bottom",
|
|
83
|
+
"top+middle+bottom",
|
|
84
|
+
"top:left",
|
|
85
|
+
"top:center",
|
|
86
|
+
"top:right",
|
|
87
|
+
"top:left+center",
|
|
88
|
+
"top:center+right",
|
|
89
|
+
"top:left+center+right",
|
|
90
|
+
"middle:left",
|
|
91
|
+
"middle:center",
|
|
92
|
+
"middle:right",
|
|
93
|
+
"middle:left+center",
|
|
94
|
+
"middle:center+right",
|
|
95
|
+
"middle:left+center+right",
|
|
96
|
+
"bottom:left",
|
|
97
|
+
"bottom:center",
|
|
98
|
+
"bottom:right",
|
|
99
|
+
"bottom:left+center",
|
|
100
|
+
"bottom:center+right",
|
|
101
|
+
"bottom:left+center+right",
|
|
102
|
+
"top+middle:left",
|
|
103
|
+
"top+middle:center",
|
|
104
|
+
"top+middle:right",
|
|
105
|
+
"top+middle:left+center",
|
|
106
|
+
"top+middle:center+right",
|
|
107
|
+
"top+middle:left+center+right",
|
|
108
|
+
"middle+bottom:left",
|
|
109
|
+
"middle+bottom:center",
|
|
110
|
+
"middle+bottom:right",
|
|
111
|
+
"middle+bottom:left+center",
|
|
112
|
+
"middle+bottom:center+right",
|
|
113
|
+
"middle+bottom:left+center+right",
|
|
114
|
+
"top+middle+bottom:left",
|
|
115
|
+
"top+middle+bottom:center",
|
|
116
|
+
"top+middle+bottom:right",
|
|
117
|
+
"top+middle+bottom:left+center",
|
|
118
|
+
"top+middle+bottom:center+right",
|
|
119
|
+
"top+middle+bottom:left+center+right"
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const DIMENSION_PRESETS = Object.freeze({
|
|
123
|
+
widescreen: Object.freeze({ widthInches: 13.333333, heightInches: 7.5 }),
|
|
124
|
+
"16:9": Object.freeze({ widthInches: 13.333333, heightInches: 7.5 }),
|
|
125
|
+
standard: Object.freeze({ widthInches: 10, heightInches: 7.5 }),
|
|
126
|
+
"4:3": Object.freeze({ widthInches: 10, heightInches: 7.5 }),
|
|
127
|
+
"16:10": Object.freeze({ widthInches: 10, heightInches: 6.25 }),
|
|
128
|
+
letter: Object.freeze({ widthInches: 11, heightInches: 8.5 }),
|
|
129
|
+
a4: Object.freeze({ widthInches: 11.69, heightInches: 8.27 })
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const DEFAULTS = Object.freeze({
|
|
133
|
+
theme: "minimal",
|
|
134
|
+
colorScheme: "cool-horizon",
|
|
135
|
+
fontScheme: "aptos"
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const CHART_COLORS = [
|
|
139
|
+
"2874A6",
|
|
140
|
+
"1B4F72",
|
|
141
|
+
"5499C7",
|
|
142
|
+
"7BDBB2",
|
|
143
|
+
"3AC67A",
|
|
144
|
+
"24A89E",
|
|
145
|
+
"F59E0B",
|
|
146
|
+
"EF4444",
|
|
147
|
+
"8B5CF6",
|
|
148
|
+
"14B8A6",
|
|
149
|
+
"0F172A",
|
|
150
|
+
"64748B"
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
export async function toPptx(input, options = {}) {
|
|
154
|
+
const presentation = parseInput(input);
|
|
155
|
+
assertValidBoundary(presentation);
|
|
156
|
+
|
|
157
|
+
const context = resolvePresentationContext(presentation, options);
|
|
158
|
+
const pptx = new PptxGenJS();
|
|
159
|
+
configurePresentation(pptx, presentation, context);
|
|
160
|
+
|
|
161
|
+
for (let index = 0; index < presentation.slides.length; index += 1) {
|
|
162
|
+
await addSlide(pptx, presentation, presentation.slides[index], index, context, options);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
let raw;
|
|
166
|
+
try {
|
|
167
|
+
raw = await withDeterministicRandom(context.seed, () => pptx.write({
|
|
168
|
+
outputType: "uint8array",
|
|
169
|
+
compression: true
|
|
170
|
+
}));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
throw new OPFPptxError("pptxgen-failed", "PPTX generation failed.", {
|
|
173
|
+
cause: errorMessage(error)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return normalizePptxZip(asUint8Array(raw), context);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export async function fromPptx(input, options = {}) {
|
|
181
|
+
const entries = readPptxZip(input);
|
|
182
|
+
const presentationDoc = parseRequiredXml(entries, "ppt/presentation.xml");
|
|
183
|
+
const presentationRoot = presentationDoc["p:presentation"];
|
|
184
|
+
if (!presentationRoot) {
|
|
185
|
+
throw new OPFPptxError("invalid-pptx", "PPTX is missing ppt/presentation.xml.");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const presentationRels = parseRelationships(entries, "ppt/presentation.xml");
|
|
189
|
+
const slidePaths = resolveSlidePaths(entries, presentationRoot, presentationRels);
|
|
190
|
+
if (slidePaths.length === 0) {
|
|
191
|
+
throw new OPFPptxError("invalid-pptx", "PPTX does not contain any slides.");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const core = readCoreProperties(entries);
|
|
195
|
+
const dimensions = dimensionsFromPresentation(presentationRoot);
|
|
196
|
+
const imported = {
|
|
197
|
+
$schema: options.schema ?? CANONICAL_SCHEMA,
|
|
198
|
+
name: core.title || options.fallbackName || "Imported PPTX",
|
|
199
|
+
slides: []
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (core.description) imported.description = core.description;
|
|
203
|
+
if (core.author) imported.author = core.author;
|
|
204
|
+
if (dimensions) imported.design = { dimensions };
|
|
205
|
+
|
|
206
|
+
for (let index = 0; index < slidePaths.length; index += 1) {
|
|
207
|
+
imported.slides.push(importSlide(entries, slidePaths[index], index, dimensions));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result = validatePresentation(imported);
|
|
211
|
+
if (!result.valid) {
|
|
212
|
+
throw new OPFPptxError("invalid-import-opf", "Imported PPTX did not produce valid OPF.", {
|
|
213
|
+
issues: result.errors,
|
|
214
|
+
result
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return imported;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readPptxZip(input) {
|
|
222
|
+
let bytes;
|
|
223
|
+
if (input instanceof Uint8Array) {
|
|
224
|
+
bytes = input;
|
|
225
|
+
} else if (input instanceof ArrayBuffer) {
|
|
226
|
+
bytes = new Uint8Array(input);
|
|
227
|
+
} else {
|
|
228
|
+
throw new OPFPptxError("invalid-input", "PPTX input must be a Uint8Array or ArrayBuffer.");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
return unzipSync(bytes);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
throw new OPFPptxError("invalid-pptx", "PPTX input is not a readable ZIP archive.", {
|
|
235
|
+
cause: errorMessage(error)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseRequiredXml(entries, path) {
|
|
241
|
+
const bytes = entries[path];
|
|
242
|
+
if (!bytes) {
|
|
243
|
+
throw new OPFPptxError("invalid-pptx", `PPTX is missing ${path}.`, { path });
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
return xmlParser.parse(decodeText(bytes));
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw new OPFPptxError("invalid-pptx", `PPTX XML part could not be parsed: ${path}.`, {
|
|
249
|
+
path,
|
|
250
|
+
cause: errorMessage(error)
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseOptionalXml(entries, path) {
|
|
256
|
+
if (!entries[path]) return null;
|
|
257
|
+
return parseRequiredXml(entries, path);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseRelationships(entries, sourcePartPath) {
|
|
261
|
+
const relsPath = relationshipsPathForPart(sourcePartPath);
|
|
262
|
+
const doc = parseOptionalXml(entries, relsPath);
|
|
263
|
+
const relationships = asArray(doc?.Relationships?.Relationship);
|
|
264
|
+
const map = new Map();
|
|
265
|
+
for (const relationship of relationships) {
|
|
266
|
+
if (!relationship?.Id) continue;
|
|
267
|
+
map.set(relationship.Id, {
|
|
268
|
+
id: relationship.Id,
|
|
269
|
+
type: relationship.Type ?? "",
|
|
270
|
+
target: relationship.Target ?? "",
|
|
271
|
+
path: resolveRelationshipTarget(sourcePartPath, relationship.Target ?? "")
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return map;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function relationshipsPathForPart(partPath) {
|
|
278
|
+
const slash = partPath.lastIndexOf("/");
|
|
279
|
+
const dir = slash >= 0 ? partPath.slice(0, slash + 1) : "";
|
|
280
|
+
const file = slash >= 0 ? partPath.slice(slash + 1) : partPath;
|
|
281
|
+
return `${dir}_rels/${file}.rels`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function resolveRelationshipTarget(sourcePartPath, target) {
|
|
285
|
+
if (!target || /^https?:\/\//i.test(target)) return target;
|
|
286
|
+
const raw = target.startsWith("/")
|
|
287
|
+
? target.slice(1)
|
|
288
|
+
: `${sourcePartPath.slice(0, sourcePartPath.lastIndexOf("/") + 1)}${target}`;
|
|
289
|
+
const parts = [];
|
|
290
|
+
for (const part of raw.split("/")) {
|
|
291
|
+
if (!part || part === ".") continue;
|
|
292
|
+
if (part === "..") {
|
|
293
|
+
parts.pop();
|
|
294
|
+
} else {
|
|
295
|
+
parts.push(part);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return parts.join("/");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function resolveSlidePaths(entries, presentationRoot, relationships) {
|
|
302
|
+
const slideIds = asArray(presentationRoot["p:sldIdLst"]?.["p:sldId"]);
|
|
303
|
+
const paths = [];
|
|
304
|
+
for (const slideId of slideIds) {
|
|
305
|
+
const relId = slideId?.["r:id"];
|
|
306
|
+
const relationship = relationships.get(relId);
|
|
307
|
+
if (relationship?.type.endsWith("/slide") && entries[relationship.path]) {
|
|
308
|
+
paths.push(relationship.path);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (paths.length > 0) return paths;
|
|
313
|
+
return Object.keys(entries)
|
|
314
|
+
.filter((path) => /^ppt\/slides\/slide\d+\.xml$/.test(path))
|
|
315
|
+
.sort(compareSlidePaths);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function compareSlidePaths(left, right) {
|
|
319
|
+
return slideNumber(left) - slideNumber(right) || left.localeCompare(right);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function slideNumber(path) {
|
|
323
|
+
return Number(path.match(/slide(\d+)\.xml$/)?.[1] ?? 0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function readCoreProperties(entries) {
|
|
327
|
+
const doc = parseOptionalXml(entries, "docProps/core.xml");
|
|
328
|
+
const core = doc?.["cp:coreProperties"] ?? {};
|
|
329
|
+
return {
|
|
330
|
+
title: scalarText(core["dc:title"]).trim(),
|
|
331
|
+
description: scalarText(core["dc:description"] ?? core["dc:subject"]).trim(),
|
|
332
|
+
author: scalarText(core["dc:creator"]).trim()
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function dimensionsFromPresentation(presentationRoot) {
|
|
337
|
+
const size = presentationRoot["p:sldSz"];
|
|
338
|
+
const width = emuToInches(size?.cx);
|
|
339
|
+
const height = emuToInches(size?.cy);
|
|
340
|
+
if (!width || !height) return null;
|
|
341
|
+
return {
|
|
342
|
+
widthInches: width,
|
|
343
|
+
heightInches: height
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function importSlide(entries, slidePath, slideIndex, presentationDimensions) {
|
|
348
|
+
const doc = parseRequiredXml(entries, slidePath);
|
|
349
|
+
const slideRoot = doc["p:sld"];
|
|
350
|
+
if (!slideRoot) {
|
|
351
|
+
throw new OPFPptxError("invalid-pptx", `PPTX slide is not a PresentationML slide: ${slidePath}.`, {
|
|
352
|
+
path: slidePath
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const relationships = parseRelationships(entries, slidePath);
|
|
357
|
+
const dimensions = presentationDimensions ?? DIMENSION_PRESETS.widescreen;
|
|
358
|
+
const slide = {};
|
|
359
|
+
if (slideRoot.show === "0") slide.hidden = true;
|
|
360
|
+
|
|
361
|
+
const background = slideBackground(slideRoot);
|
|
362
|
+
if (background) {
|
|
363
|
+
slide.design = {
|
|
364
|
+
background: {
|
|
365
|
+
type: "solid",
|
|
366
|
+
color: background
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const items = collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions)
|
|
372
|
+
.sort(comparePositionedItems);
|
|
373
|
+
const titleItem = takeTitleItem(items, dimensions);
|
|
374
|
+
if (titleItem) slide.title = firstLine(titleItem.text);
|
|
375
|
+
const subtitleItem = takeSubtitleItem(items, titleItem, dimensions);
|
|
376
|
+
if (subtitleItem) slide.subtitle = firstLine(subtitleItem.text);
|
|
377
|
+
|
|
378
|
+
const blocks = items
|
|
379
|
+
.map((item) => payloadFromSlideItem(item))
|
|
380
|
+
.filter(Boolean);
|
|
381
|
+
if (blocks.length > 0) slide.blocks = blocks;
|
|
382
|
+
|
|
383
|
+
const notes = readSlideNotes(entries, relationships);
|
|
384
|
+
if (notes) slide.notes = notes;
|
|
385
|
+
|
|
386
|
+
if (!slide.title && !slide.blocks && !slide.notes) {
|
|
387
|
+
slide.title = `Slide ${slideIndex + 1}`;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return slide;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensions) {
|
|
394
|
+
const tree = slideRoot["p:cSld"]?.["p:spTree"];
|
|
395
|
+
const items = [];
|
|
396
|
+
|
|
397
|
+
for (const shape of asArray(tree?.["p:sp"])) {
|
|
398
|
+
const item = importShape(shape, dimensions);
|
|
399
|
+
if (item) items.push(item);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
for (const frame of asArray(tree?.["p:graphicFrame"])) {
|
|
403
|
+
const item = importGraphicFrame(entries, frame, slidePath, relationships);
|
|
404
|
+
if (item) items.push(item);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
for (const picture of asArray(tree?.["p:pic"])) {
|
|
408
|
+
const item = importPicture(entries, picture, slidePath, relationships);
|
|
409
|
+
if (item) items.push(item);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return items;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function importShape(shape, dimensions) {
|
|
416
|
+
const paragraphs = readParagraphs(shape["p:txBody"]);
|
|
417
|
+
const text = paragraphs.map((paragraph) => paragraph.text).filter(Boolean).join("\n").trim();
|
|
418
|
+
const placeholder = shapePlaceholderType(shape);
|
|
419
|
+
const bounds = shapeBounds(shape["p:spPr"]?.["a:xfrm"]);
|
|
420
|
+
const name = scalarText(shape["p:nvSpPr"]?.["p:cNvPr"]?.name).trim();
|
|
421
|
+
|
|
422
|
+
if (text) {
|
|
423
|
+
return {
|
|
424
|
+
kind: "text",
|
|
425
|
+
text,
|
|
426
|
+
paragraphs,
|
|
427
|
+
bounds,
|
|
428
|
+
placeholder,
|
|
429
|
+
name,
|
|
430
|
+
maxFontSize: Math.max(0, ...paragraphs.map((paragraph) => paragraph.maxFontSize))
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (placeholder || !bounds || isFullSlide(bounds, dimensions)) return null;
|
|
435
|
+
return {
|
|
436
|
+
kind: "unknown",
|
|
437
|
+
bounds,
|
|
438
|
+
name,
|
|
439
|
+
text: `PowerPoint shape: ${name || "unsupported shape"}`
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function importGraphicFrame(entries, frame, slidePath, relationships) {
|
|
444
|
+
const bounds = shapeBounds(frame["p:xfrm"]);
|
|
445
|
+
const name = scalarText(frame["p:nvGraphicFramePr"]?.["p:cNvPr"]?.name).trim();
|
|
446
|
+
const graphicData = frame["a:graphic"]?.["a:graphicData"];
|
|
447
|
+
const table = graphicData?.["a:tbl"];
|
|
448
|
+
if (table) {
|
|
449
|
+
return {
|
|
450
|
+
kind: "table",
|
|
451
|
+
bounds,
|
|
452
|
+
name,
|
|
453
|
+
payload: {
|
|
454
|
+
type: "table",
|
|
455
|
+
table: tableFromXml(table)
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const chartRelId = graphicData?.["c:chart"]?.["r:id"];
|
|
461
|
+
if (chartRelId) {
|
|
462
|
+
const chart = chartFromRelationship(entries, slidePath, relationships, chartRelId);
|
|
463
|
+
return {
|
|
464
|
+
kind: "chart",
|
|
465
|
+
bounds,
|
|
466
|
+
name,
|
|
467
|
+
payload: chart
|
|
468
|
+
? { type: "chart", chart }
|
|
469
|
+
: { type: "text", text: `PowerPoint chart: ${name || chartRelId}` }
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
kind: "unknown",
|
|
475
|
+
bounds,
|
|
476
|
+
name,
|
|
477
|
+
text: `PowerPoint object: ${name || "unsupported object"}`
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function importPicture(entries, picture, slidePath, relationships) {
|
|
482
|
+
const bounds = shapeBounds(picture["p:spPr"]?.["a:xfrm"]);
|
|
483
|
+
const name = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.name).trim();
|
|
484
|
+
const alt = scalarText(picture["p:nvPicPr"]?.["p:cNvPr"]?.descr).trim();
|
|
485
|
+
const relId = picture["p:blipFill"]?.["a:blip"]?.["r:embed"];
|
|
486
|
+
const relationship = relationships.get(relId);
|
|
487
|
+
const bytes = relationship?.path ? entries[relationship.path] : null;
|
|
488
|
+
if (!bytes) {
|
|
489
|
+
return {
|
|
490
|
+
kind: "unknown",
|
|
491
|
+
bounds,
|
|
492
|
+
name,
|
|
493
|
+
text: `PowerPoint image: ${alt || name || "unresolved image"}`
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
kind: "image",
|
|
499
|
+
bounds,
|
|
500
|
+
name,
|
|
501
|
+
payload: {
|
|
502
|
+
type: "image",
|
|
503
|
+
image: {
|
|
504
|
+
src: `data:${mediaTypeForPath(relationship.path)};base64,${bytesToBase64(bytes)}`,
|
|
505
|
+
...(alt ? { alt } : {})
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function readParagraphs(txBody) {
|
|
512
|
+
return asArray(txBody?.["a:p"])
|
|
513
|
+
.map((paragraph) => {
|
|
514
|
+
const runs = [
|
|
515
|
+
...asArray(paragraph?.["a:r"]),
|
|
516
|
+
...asArray(paragraph?.["a:fld"])
|
|
517
|
+
];
|
|
518
|
+
const texts = [];
|
|
519
|
+
const sizes = [];
|
|
520
|
+
for (const run of runs) {
|
|
521
|
+
const text = scalarText(run?.["a:t"]);
|
|
522
|
+
if (text) texts.push(text);
|
|
523
|
+
const size = Number(run?.["a:rPr"]?.sz);
|
|
524
|
+
if (Number.isFinite(size)) sizes.push(size / 100);
|
|
525
|
+
}
|
|
526
|
+
return {
|
|
527
|
+
text: texts.join("").trim(),
|
|
528
|
+
level: Number(paragraph?.["a:pPr"]?.lvl ?? 0),
|
|
529
|
+
maxFontSize: sizes.length > 0 ? Math.max(...sizes) : 0
|
|
530
|
+
};
|
|
531
|
+
})
|
|
532
|
+
.filter((paragraph) => paragraph.text);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function shapePlaceholderType(shape) {
|
|
536
|
+
return shape["p:nvSpPr"]?.["p:nvPr"]?.["p:ph"]?.type ?? null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function shapeBounds(xfrm) {
|
|
540
|
+
if (!xfrm) return null;
|
|
541
|
+
const x = emuToInches(xfrm["a:off"]?.x);
|
|
542
|
+
const y = emuToInches(xfrm["a:off"]?.y);
|
|
543
|
+
const w = emuToInches(xfrm["a:ext"]?.cx);
|
|
544
|
+
const h = emuToInches(xfrm["a:ext"]?.cy);
|
|
545
|
+
if ([x, y, w, h].some((value) => value === null)) return null;
|
|
546
|
+
return { x, y, w, h };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function takeTitleItem(items, dimensions) {
|
|
550
|
+
const explicitIndex = items.findIndex((item) => ["title", "ctrTitle"].includes(item.placeholder));
|
|
551
|
+
if (explicitIndex >= 0) return items.splice(explicitIndex, 1)[0];
|
|
552
|
+
|
|
553
|
+
const titleLimit = dimensions.heightInches * 0.28;
|
|
554
|
+
const candidateIndex = items.findIndex((item) => {
|
|
555
|
+
if (item.kind !== "text" || !item.text) return false;
|
|
556
|
+
const y = item.bounds?.y ?? 0;
|
|
557
|
+
return y <= titleLimit && (item.maxFontSize >= 20 || /^title\b/i.test(item.name ?? ""));
|
|
558
|
+
});
|
|
559
|
+
if (candidateIndex >= 0) return items.splice(candidateIndex, 1)[0];
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function takeSubtitleItem(items, titleItem, dimensions) {
|
|
564
|
+
const explicitIndex = items.findIndex((item) => item.placeholder === "subTitle");
|
|
565
|
+
if (explicitIndex >= 0) return items.splice(explicitIndex, 1)[0];
|
|
566
|
+
if (!titleItem) return null;
|
|
567
|
+
|
|
568
|
+
const titleBottom = (titleItem.bounds?.y ?? 0) + (titleItem.bounds?.h ?? 0);
|
|
569
|
+
const subtitleLimit = Math.min(dimensions.heightInches * 0.34, 1.45);
|
|
570
|
+
const candidateIndex = items.findIndex((item) => {
|
|
571
|
+
if (item.kind !== "text" || !item.text) return false;
|
|
572
|
+
const y = item.bounds?.y ?? 0;
|
|
573
|
+
const h = item.bounds?.h ?? 0;
|
|
574
|
+
return y >= titleBottom - 0.05
|
|
575
|
+
&& y <= subtitleLimit
|
|
576
|
+
&& h <= 0.75
|
|
577
|
+
&& item.paragraphs.length === 1
|
|
578
|
+
&& item.maxFontSize <= Math.max(22, titleItem.maxFontSize);
|
|
579
|
+
});
|
|
580
|
+
if (candidateIndex >= 0) return items.splice(candidateIndex, 1)[0];
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function payloadFromSlideItem(item) {
|
|
585
|
+
if (item.payload) return item.payload;
|
|
586
|
+
if (item.kind === "text") {
|
|
587
|
+
if (item.paragraphs.length > 1) {
|
|
588
|
+
return {
|
|
589
|
+
type: "list",
|
|
590
|
+
items: item.paragraphs.map((paragraph) => (
|
|
591
|
+
paragraph.level > 0
|
|
592
|
+
? { text: paragraph.text, level: paragraph.level }
|
|
593
|
+
: paragraph.text
|
|
594
|
+
))
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return { type: "text", text: item.text };
|
|
598
|
+
}
|
|
599
|
+
if (item.kind === "unknown" && item.text) return { type: "text", text: item.text };
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function tableFromXml(table) {
|
|
604
|
+
const rows = asArray(table["a:tr"])
|
|
605
|
+
.map((row) => asArray(row?.["a:tc"]).map((cell) => textFromTextBody(cell?.["a:txBody"])))
|
|
606
|
+
.filter((row) => row.some(Boolean));
|
|
607
|
+
if (rows.length === 0) return { rows: [] };
|
|
608
|
+
return {
|
|
609
|
+
columns: rows[0],
|
|
610
|
+
rows: rows.slice(1)
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function chartFromRelationship(entries, slidePath, relationships, relId) {
|
|
615
|
+
const relationship = relationships.get(relId);
|
|
616
|
+
if (!relationship?.path || !entries[relationship.path]) return null;
|
|
617
|
+
const doc = parseRequiredXml(entries, relationship.path);
|
|
618
|
+
const plotArea = doc["c:chartSpace"]?.["c:chart"]?.["c:plotArea"];
|
|
619
|
+
if (!plotArea) return null;
|
|
620
|
+
|
|
621
|
+
const chartNode = firstChartNode(plotArea);
|
|
622
|
+
if (!chartNode) return null;
|
|
623
|
+
const series = asArray(chartNode.node["c:ser"]);
|
|
624
|
+
if (series.length === 0) return null;
|
|
625
|
+
|
|
626
|
+
const labels = cachedValues(series[0]?.["c:cat"]);
|
|
627
|
+
const names = series.map((entry, index) => firstCachedValue(entry?.["c:tx"]) || `Series ${index + 1}`);
|
|
628
|
+
const values = series.map((entry) => cachedValues(entry?.["c:val"] ?? entry?.["c:yVal"]).map(numericValue));
|
|
629
|
+
const rowCount = Math.max(labels.length, ...values.map((row) => row.length));
|
|
630
|
+
if (rowCount === 0) return null;
|
|
631
|
+
|
|
632
|
+
const rows = [];
|
|
633
|
+
for (let index = 0; index < rowCount; index += 1) {
|
|
634
|
+
rows.push([
|
|
635
|
+
labels[index] ?? `Item ${index + 1}`,
|
|
636
|
+
...values.map((row) => row[index] ?? 0)
|
|
637
|
+
]);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
type: chartNode.type,
|
|
642
|
+
data: {
|
|
643
|
+
columns: ["Category", ...names],
|
|
644
|
+
rows
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function firstChartNode(plotArea) {
|
|
650
|
+
const candidates = [
|
|
651
|
+
["c:barChart", (node) => node?.["c:barDir"]?.val === "bar" ? "bar" : "column"],
|
|
652
|
+
["c:lineChart", () => "line"],
|
|
653
|
+
["c:pieChart", () => "pie"],
|
|
654
|
+
["c:doughnutChart", () => "doughnut"],
|
|
655
|
+
["c:areaChart", () => "area"],
|
|
656
|
+
["c:scatterChart", () => "scatter"],
|
|
657
|
+
["c:radarChart", () => "radar"]
|
|
658
|
+
];
|
|
659
|
+
for (const [key, type] of candidates) {
|
|
660
|
+
const node = asArray(plotArea[key])[0];
|
|
661
|
+
if (node) return { node, type: type(node) };
|
|
662
|
+
}
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function cachedValues(node) {
|
|
667
|
+
const cache = node?.["c:strRef"]?.["c:strCache"]
|
|
668
|
+
?? node?.["c:numRef"]?.["c:numCache"]
|
|
669
|
+
?? node?.["c:multiLvlStrRef"]?.["c:multiLvlStrCache"]?.["c:lvl"]
|
|
670
|
+
?? node?.["c:numLit"]
|
|
671
|
+
?? node?.["c:strLit"];
|
|
672
|
+
const points = asArray(cache?.["c:pt"]);
|
|
673
|
+
if (points.length > 0) return points.map((point) => scalarText(point?.["c:v"]));
|
|
674
|
+
const nestedPoints = asArray(cache)
|
|
675
|
+
.flatMap((level) => asArray(level?.["c:pt"]))
|
|
676
|
+
.map((point) => scalarText(point?.["c:v"]));
|
|
677
|
+
return nestedPoints;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function firstCachedValue(node) {
|
|
681
|
+
return cachedValues(node).find(Boolean) ?? "";
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function readSlideNotes(entries, relationships) {
|
|
685
|
+
const notesRel = [...relationships.values()].find((relationship) => relationship.type.endsWith("/notesSlide"));
|
|
686
|
+
if (!notesRel?.path || !entries[notesRel.path]) return "";
|
|
687
|
+
const doc = parseRequiredXml(entries, notesRel.path);
|
|
688
|
+
const shapes = asArray(doc["p:notes"]?.["p:cSld"]?.["p:spTree"]?.["p:sp"]);
|
|
689
|
+
const bodyNotes = shapes
|
|
690
|
+
.filter((shape) => shapePlaceholderType(shape) === "body")
|
|
691
|
+
.map((shape) => textFromTextBody(shape["p:txBody"]))
|
|
692
|
+
.filter(Boolean);
|
|
693
|
+
return bodyNotes.join("\n").trim();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function slideBackground(slideRoot) {
|
|
697
|
+
const color = slideRoot["p:cSld"]?.["p:bg"]?.["p:bgPr"]?.["a:solidFill"]?.["a:srgbClr"]?.val;
|
|
698
|
+
return color ? `#${normalizeHex(color)}` : "";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function textFromTextBody(txBody) {
|
|
702
|
+
return readParagraphs(txBody).map((paragraph) => paragraph.text).filter(Boolean).join("\n").trim();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function comparePositionedItems(left, right) {
|
|
706
|
+
const leftY = left.bounds?.y ?? Number.MAX_SAFE_INTEGER;
|
|
707
|
+
const rightY = right.bounds?.y ?? Number.MAX_SAFE_INTEGER;
|
|
708
|
+
const leftX = left.bounds?.x ?? Number.MAX_SAFE_INTEGER;
|
|
709
|
+
const rightX = right.bounds?.x ?? Number.MAX_SAFE_INTEGER;
|
|
710
|
+
return leftY - rightY || leftX - rightX;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function firstLine(value) {
|
|
714
|
+
return String(value ?? "").split(/\r?\n/).find((line) => line.trim())?.trim() ?? "";
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function mediaTypeForPath(path) {
|
|
718
|
+
const ext = path.toLowerCase().split(".").pop();
|
|
719
|
+
if (ext === "jpg" || ext === "jpeg") return "image/jpeg";
|
|
720
|
+
if (ext === "gif") return "image/gif";
|
|
721
|
+
if (ext === "webp") return "image/webp";
|
|
722
|
+
if (ext === "svg") return "image/svg+xml";
|
|
723
|
+
return "image/png";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function isFullSlide(bounds, dimensions) {
|
|
727
|
+
return bounds.x <= 0.02
|
|
728
|
+
&& bounds.y <= 0.02
|
|
729
|
+
&& bounds.w >= dimensions.widthInches - 0.04
|
|
730
|
+
&& bounds.h >= dimensions.heightInches - 0.04;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function emuToInches(value) {
|
|
734
|
+
const number = Number(value);
|
|
735
|
+
if (!Number.isFinite(number)) return null;
|
|
736
|
+
return Math.round((number / EMUS_PER_INCH) * 1_000_000) / 1_000_000;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function asArray(value) {
|
|
740
|
+
if (value === undefined || value === null) return [];
|
|
741
|
+
return Array.isArray(value) ? value : [value];
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function scalarText(value) {
|
|
745
|
+
if (value === null || value === undefined) return "";
|
|
746
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
747
|
+
if (Array.isArray(value)) return value.map(scalarText).join("");
|
|
748
|
+
if (typeof value === "object" && value["#text"] !== undefined) return scalarText(value["#text"]);
|
|
749
|
+
return "";
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function parseInput(input) {
|
|
753
|
+
if (typeof input === "string") {
|
|
754
|
+
try {
|
|
755
|
+
return JSON.parse(input);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
throw new OPFPptxError("invalid-json", "OPF input is not valid JSON.", {
|
|
758
|
+
cause: errorMessage(error)
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (input instanceof Uint8Array) {
|
|
764
|
+
return parseInput(new TextDecoder().decode(input));
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
768
|
+
return input;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
throw new OPFPptxError("invalid-input", "OPF input must be a parsed object, JSON string, or Uint8Array.");
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function assertValidBoundary(presentation) {
|
|
775
|
+
const result = validatePresentation(presentation);
|
|
776
|
+
if (!result.valid) {
|
|
777
|
+
throw new OPFPptxError("invalid-opf", "OPF validation failed.", {
|
|
778
|
+
issues: result.errors,
|
|
779
|
+
result
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function resolvePresentationContext(presentation, options) {
|
|
785
|
+
const design = presentation.design ?? {};
|
|
786
|
+
const theme = resolveCatalogRecord(presentation, "themes", design.theme, DEFAULTS.theme);
|
|
787
|
+
const colorScheme = resolveDesignRecord(
|
|
788
|
+
presentation,
|
|
789
|
+
"colorSchemes",
|
|
790
|
+
design.colorScheme ?? theme?.colorScheme,
|
|
791
|
+
DEFAULTS.colorScheme
|
|
792
|
+
);
|
|
793
|
+
const fontScheme = resolveDesignRecord(
|
|
794
|
+
presentation,
|
|
795
|
+
"fontSchemes",
|
|
796
|
+
design.fontScheme ?? theme?.fontScheme,
|
|
797
|
+
DEFAULTS.fontScheme
|
|
798
|
+
);
|
|
799
|
+
const dimensions = resolveDimensions(design.dimensions ?? theme?.dimensions);
|
|
800
|
+
const background = resolveBackground(design.background ?? theme?.background, colorScheme);
|
|
801
|
+
const fonts = resolveFonts(fontScheme);
|
|
802
|
+
const textColor = readableTextColor(background, colorScheme);
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
seed: Number.isInteger(options.seed) ? options.seed : DEFAULT_SEED,
|
|
806
|
+
timestamp: options.timestamp ?? FIXED_TIMESTAMP,
|
|
807
|
+
zipDate: options.zipDate ? new Date(options.zipDate) : FIXED_ZIP_DATE,
|
|
808
|
+
compressionLevel: Number.isInteger(options.compressionLevel) ? options.compressionLevel : 6,
|
|
809
|
+
layoutName: "OPF_CANVAS",
|
|
810
|
+
dimensions,
|
|
811
|
+
colorScheme,
|
|
812
|
+
fonts,
|
|
813
|
+
colors: {
|
|
814
|
+
background,
|
|
815
|
+
text: textColor,
|
|
816
|
+
mutedText: normalizeHex(colorScheme.textSecondary ?? colorScheme.dark2 ?? "#475569"),
|
|
817
|
+
accent: normalizeHex(colorScheme.primary ?? colorScheme.accent1 ?? "#2874A6"),
|
|
818
|
+
surface: normalizeHex(colorScheme.surface ?? colorScheme.light2 ?? "#F8FAFC"),
|
|
819
|
+
border: normalizeHex(colorScheme.accent3 ?? "#CBD5E1")
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function configurePresentation(pptx, presentation, context) {
|
|
825
|
+
pptx.defineLayout({
|
|
826
|
+
name: context.layoutName,
|
|
827
|
+
width: context.dimensions.widthInches,
|
|
828
|
+
height: context.dimensions.heightInches
|
|
829
|
+
});
|
|
830
|
+
pptx.layout = context.layoutName;
|
|
831
|
+
pptx.author = normalizeAuthor(presentation.author) ?? "OpenPresentation";
|
|
832
|
+
pptx.company = "OpenPresentation";
|
|
833
|
+
pptx.subject = presentation.description ?? "";
|
|
834
|
+
pptx.title = presentation.name ?? presentation.filename ?? "OPF Presentation";
|
|
835
|
+
pptx.revision = "1";
|
|
836
|
+
pptx.theme = {
|
|
837
|
+
headFontFace: context.fonts.heading,
|
|
838
|
+
bodyFontFace: context.fonts.body
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
async function addSlide(pptx, presentation, opfSlide, slideIndex, context, options) {
|
|
843
|
+
const slide = pptx.addSlide();
|
|
844
|
+
const slideContext = resolveSlideContext(presentation, opfSlide, context);
|
|
845
|
+
slide.background = { color: slideContext.colors.background };
|
|
846
|
+
slide.color = slideContext.colors.text;
|
|
847
|
+
if (opfSlide.hidden === true) slide.hidden = true;
|
|
848
|
+
|
|
849
|
+
const { widthInches, heightInches } = slideContext.dimensions;
|
|
850
|
+
const margin = 0.55;
|
|
851
|
+
let y = 0.34;
|
|
852
|
+
|
|
853
|
+
if (opfSlide.tag) {
|
|
854
|
+
slide.addText(String(opfSlide.tag), {
|
|
855
|
+
x: margin,
|
|
856
|
+
y,
|
|
857
|
+
w: widthInches - margin * 2,
|
|
858
|
+
h: 0.24,
|
|
859
|
+
margin: 0,
|
|
860
|
+
fontFace: slideContext.fonts.body,
|
|
861
|
+
fontSize: 9,
|
|
862
|
+
bold: true,
|
|
863
|
+
color: slideContext.colors.accent,
|
|
864
|
+
fit: "shrink"
|
|
865
|
+
});
|
|
866
|
+
y += 0.32;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const title = opfSlide.title ?? presentation.title ?? presentation.name;
|
|
870
|
+
if (title) {
|
|
871
|
+
slide.addText(String(title), {
|
|
872
|
+
x: margin,
|
|
873
|
+
y,
|
|
874
|
+
w: widthInches - margin * 2,
|
|
875
|
+
h: 0.58,
|
|
876
|
+
margin: 0,
|
|
877
|
+
fontFace: slideContext.fonts.heading,
|
|
878
|
+
fontSize: 28,
|
|
879
|
+
bold: true,
|
|
880
|
+
color: slideContext.colors.text,
|
|
881
|
+
fit: "shrink",
|
|
882
|
+
breakLine: false
|
|
883
|
+
});
|
|
884
|
+
y += 0.68;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const subtitle = opfSlide.subtitle ?? presentation.subtitle;
|
|
888
|
+
if (subtitle) {
|
|
889
|
+
slide.addText(String(subtitle), {
|
|
890
|
+
x: margin,
|
|
891
|
+
y,
|
|
892
|
+
w: widthInches - margin * 2,
|
|
893
|
+
h: 0.34,
|
|
894
|
+
margin: 0,
|
|
895
|
+
fontFace: slideContext.fonts.body,
|
|
896
|
+
fontSize: 14,
|
|
897
|
+
color: slideContext.colors.mutedText,
|
|
898
|
+
fit: "shrink"
|
|
899
|
+
});
|
|
900
|
+
y += 0.48;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const contentTop = Math.max(y + 0.08, title || subtitle || opfSlide.tag ? 1.25 : 0.55);
|
|
904
|
+
const contentArea = {
|
|
905
|
+
x: margin,
|
|
906
|
+
y: contentTop,
|
|
907
|
+
w: widthInches - margin * 2,
|
|
908
|
+
h: Math.max(0.7, heightInches - contentTop - 0.48)
|
|
909
|
+
};
|
|
910
|
+
const bindings = collectSlideBindings(opfSlide, slideIndex);
|
|
911
|
+
|
|
912
|
+
for (let index = 0; index < bindings.length; index += 1) {
|
|
913
|
+
const binding = bindings[index];
|
|
914
|
+
const region = binding.regionKey
|
|
915
|
+
? regionFromPromotedKey(binding.regionKey, contentArea)
|
|
916
|
+
: regionFromIndex(index, bindings.length, contentArea);
|
|
917
|
+
await addPayload(slide, presentation, binding.payload, insetRegion(region, 0.08), binding.path, slideContext, options);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (opfSlide.notes) slide.addNotes(String(opfSlide.notes));
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function resolveSlideContext(presentation, slide, baseContext) {
|
|
924
|
+
if (!slide.design) return baseContext;
|
|
925
|
+
const design = slide.design;
|
|
926
|
+
const colorScheme = resolveDesignRecord(
|
|
927
|
+
presentation,
|
|
928
|
+
"colorSchemes",
|
|
929
|
+
design.colorScheme,
|
|
930
|
+
baseContext.colorScheme.id ?? DEFAULTS.colorScheme
|
|
931
|
+
);
|
|
932
|
+
const fontScheme = resolveDesignRecord(
|
|
933
|
+
presentation,
|
|
934
|
+
"fontSchemes",
|
|
935
|
+
design.fontScheme,
|
|
936
|
+
baseContext.fonts.id ?? DEFAULTS.fontScheme
|
|
937
|
+
);
|
|
938
|
+
const background = design.background
|
|
939
|
+
? resolveBackground(design.background, colorScheme)
|
|
940
|
+
: baseContext.colors.background;
|
|
941
|
+
const fonts = design.fontScheme ? resolveFonts(fontScheme) : baseContext.fonts;
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
...baseContext,
|
|
945
|
+
colorScheme,
|
|
946
|
+
fonts,
|
|
947
|
+
colors: {
|
|
948
|
+
...baseContext.colors,
|
|
949
|
+
background,
|
|
950
|
+
text: readableTextColor(background, colorScheme),
|
|
951
|
+
mutedText: normalizeHex(colorScheme.textSecondary ?? colorScheme.dark2 ?? baseContext.colors.mutedText),
|
|
952
|
+
accent: normalizeHex(colorScheme.primary ?? colorScheme.accent1 ?? baseContext.colors.accent),
|
|
953
|
+
surface: normalizeHex(colorScheme.surface ?? colorScheme.light2 ?? baseContext.colors.surface),
|
|
954
|
+
border: normalizeHex(colorScheme.accent3 ?? baseContext.colors.border)
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function collectSlideBindings(slide, slideIndex) {
|
|
960
|
+
const promoted = PROMOTED_REGION_KEYS
|
|
961
|
+
.filter((key) => slide[key] !== undefined)
|
|
962
|
+
.map((key) => ({
|
|
963
|
+
payload: slide[key],
|
|
964
|
+
regionKey: key,
|
|
965
|
+
path: `slides.${slideIndex}.${key}`
|
|
966
|
+
}));
|
|
967
|
+
|
|
968
|
+
if (promoted.length > 0) return promoted;
|
|
969
|
+
|
|
970
|
+
if (Array.isArray(slide.blocks) && slide.blocks.length > 0) {
|
|
971
|
+
return slide.blocks.map((payload, index) => ({
|
|
972
|
+
payload,
|
|
973
|
+
path: `slides.${slideIndex}.blocks.${index}`
|
|
974
|
+
}));
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
return ROOT_PAYLOAD_FIELDS
|
|
978
|
+
.filter((field) => slide[field] !== undefined)
|
|
979
|
+
.map((field) => ({
|
|
980
|
+
payload: { type: fieldToType(field), [field]: slide[field] },
|
|
981
|
+
path: `slides.${slideIndex}.${field}`
|
|
982
|
+
}));
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function fieldToType(field) {
|
|
986
|
+
if (field === "items") return "list";
|
|
987
|
+
if (field === "bullets") return "text";
|
|
988
|
+
return field;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
async function addPayload(slide, presentation, payload, region, path, context, options) {
|
|
992
|
+
const kind = inferPayloadKind(payload);
|
|
993
|
+
switch (kind) {
|
|
994
|
+
case "text":
|
|
995
|
+
addTextPayload(slide, payload.text ?? payload.bullets, region, context);
|
|
996
|
+
break;
|
|
997
|
+
case "list":
|
|
998
|
+
addListPayload(slide, payload.items ?? payload.bullets, region, context);
|
|
999
|
+
break;
|
|
1000
|
+
case "image":
|
|
1001
|
+
await addImagePayload(slide, presentation, payload.image, region, path, context, options);
|
|
1002
|
+
break;
|
|
1003
|
+
case "video":
|
|
1004
|
+
addPlaceholderPayload(slide, "Video", payload.video, region, context);
|
|
1005
|
+
break;
|
|
1006
|
+
case "chart":
|
|
1007
|
+
addChartPayload(slide, payload.chart, region, context);
|
|
1008
|
+
break;
|
|
1009
|
+
case "table":
|
|
1010
|
+
addTablePayload(slide, payload.table, region, context);
|
|
1011
|
+
break;
|
|
1012
|
+
case "code":
|
|
1013
|
+
addCodePayload(slide, payload.code, region, context);
|
|
1014
|
+
break;
|
|
1015
|
+
case "metric":
|
|
1016
|
+
addMetricPayload(slide, payload.metric, region, context);
|
|
1017
|
+
break;
|
|
1018
|
+
case "quote":
|
|
1019
|
+
addQuotePayload(slide, payload.quote, region, context);
|
|
1020
|
+
break;
|
|
1021
|
+
case "timeline":
|
|
1022
|
+
addTimelinePayload(slide, payload.timeline, region, context);
|
|
1023
|
+
break;
|
|
1024
|
+
default:
|
|
1025
|
+
addPlaceholderPayload(slide, "Unsupported OPF payload", payload, region, context);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function inferPayloadKind(payload) {
|
|
1030
|
+
if (payload?.type === "list") return "list";
|
|
1031
|
+
if (payload?.type && ROOT_PAYLOAD_FIELDS.includes(payload.type)) return fieldToType(payload.type);
|
|
1032
|
+
if (payload?.items !== undefined) return "list";
|
|
1033
|
+
for (const field of ROOT_PAYLOAD_FIELDS) {
|
|
1034
|
+
if (payload?.[field] !== undefined) return fieldToType(field);
|
|
1035
|
+
}
|
|
1036
|
+
return "unknown";
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function addTextPayload(slide, value, region, context) {
|
|
1040
|
+
if (Array.isArray(value)) {
|
|
1041
|
+
slide.addText(textRuns(value, context, 18), textBoxOptions(region, context, 18));
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
if (Array.isArray(value?.bullets)) {
|
|
1045
|
+
addListPayload(slide, value.bullets, region, context);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
slide.addText(stringifyText(value), textBoxOptions(region, context, 18));
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function addListPayload(slide, items, region, context) {
|
|
1052
|
+
const list = Array.isArray(items) ? items : [];
|
|
1053
|
+
if (list.length === 0) {
|
|
1054
|
+
slide.addText("", textBoxOptions(region, context, 16));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
const runs = [];
|
|
1059
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
1060
|
+
const item = list[index];
|
|
1061
|
+
const level = isPlainObject(item) && Number.isInteger(item.level) ? item.level : 0;
|
|
1062
|
+
runs.push({
|
|
1063
|
+
text: stringifyText(isPlainObject(item) ? item.text : item),
|
|
1064
|
+
options: {
|
|
1065
|
+
bullet: { type: "bullet", indent: 14 + level * 14 },
|
|
1066
|
+
breakLine: index < list.length - 1 || Boolean(isPlainObject(item) && item.description),
|
|
1067
|
+
color: context.colors.text,
|
|
1068
|
+
fontFace: context.fonts.body,
|
|
1069
|
+
fontSize: 15,
|
|
1070
|
+
hanging: 4 + level * 10
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
if (isPlainObject(item) && item.description) {
|
|
1074
|
+
runs.push({
|
|
1075
|
+
text: stringifyText(item.description),
|
|
1076
|
+
options: {
|
|
1077
|
+
breakLine: index < list.length - 1,
|
|
1078
|
+
color: context.colors.mutedText,
|
|
1079
|
+
fontFace: context.fonts.body,
|
|
1080
|
+
fontSize: 11,
|
|
1081
|
+
margin: [0, 0, 0, 18 + level * 14]
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
slide.addText(runs, textBoxOptions(region, context, 15));
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function addImagePayload(slide, presentation, asset, region, path, context, options) {
|
|
1091
|
+
const resolved = await resolveImage(asset, presentation, options, path);
|
|
1092
|
+
if (!resolved) {
|
|
1093
|
+
addPlaceholderPayload(slide, "Image", asset, region, context);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
slide.addImage({
|
|
1097
|
+
...resolved,
|
|
1098
|
+
x: region.x,
|
|
1099
|
+
y: region.y,
|
|
1100
|
+
w: region.w,
|
|
1101
|
+
h: region.h,
|
|
1102
|
+
altText: assetAlt(asset, presentation)
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function addChartPayload(slide, chart, region, context) {
|
|
1107
|
+
const chartData = toPptxChartData(chart);
|
|
1108
|
+
if (!chartData) {
|
|
1109
|
+
addPlaceholderPayload(slide, "Chart", chart, region, context);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
slide.addChart(chartData.type, chartData.series, {
|
|
1114
|
+
x: region.x,
|
|
1115
|
+
y: region.y,
|
|
1116
|
+
w: region.w,
|
|
1117
|
+
h: region.h,
|
|
1118
|
+
showLegend: chartData.series.length > 1,
|
|
1119
|
+
showTitle: false,
|
|
1120
|
+
chartColors: CHART_COLORS,
|
|
1121
|
+
catAxisLabelFontFace: context.fonts.body,
|
|
1122
|
+
catAxisLabelFontSize: 9,
|
|
1123
|
+
valAxisLabelFontFace: context.fonts.body,
|
|
1124
|
+
valAxisLabelFontSize: 9,
|
|
1125
|
+
showValue: false,
|
|
1126
|
+
valGridLine: { color: context.colors.border, transparency: 30, size: 1 },
|
|
1127
|
+
barDir: chartData.barDir,
|
|
1128
|
+
barGrouping: chartData.barGrouping
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function addTablePayload(slide, table, region, context) {
|
|
1133
|
+
const rows = [];
|
|
1134
|
+
if (Array.isArray(table?.columns) && table.columns.length > 0) {
|
|
1135
|
+
rows.push(table.columns.map((value) => ({
|
|
1136
|
+
text: stringifyText(value),
|
|
1137
|
+
options: {
|
|
1138
|
+
bold: true,
|
|
1139
|
+
color: context.colors.text,
|
|
1140
|
+
fill: { color: context.colors.surface }
|
|
1141
|
+
}
|
|
1142
|
+
})));
|
|
1143
|
+
}
|
|
1144
|
+
if (Array.isArray(table?.rows)) {
|
|
1145
|
+
for (const row of table.rows) {
|
|
1146
|
+
rows.push((Array.isArray(row) ? row : [row]).map((value) => ({
|
|
1147
|
+
text: stringifyText(value),
|
|
1148
|
+
options: { color: context.colors.text }
|
|
1149
|
+
})));
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
if (rows.length === 0) {
|
|
1154
|
+
addPlaceholderPayload(slide, "Table", table, region, context);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
slide.addTable(rows, {
|
|
1159
|
+
x: region.x,
|
|
1160
|
+
y: region.y,
|
|
1161
|
+
w: region.w,
|
|
1162
|
+
h: region.h,
|
|
1163
|
+
fontFace: context.fonts.body,
|
|
1164
|
+
fontSize: 10,
|
|
1165
|
+
color: context.colors.text,
|
|
1166
|
+
border: { type: "solid", color: context.colors.border, pt: 0.75 },
|
|
1167
|
+
margin: 0.05,
|
|
1168
|
+
valign: "mid",
|
|
1169
|
+
fit: "shrink"
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function addCodePayload(slide, value, region, context) {
|
|
1174
|
+
const code = typeof value === "string" ? { source: value } : value;
|
|
1175
|
+
const title = code?.filename ? `${code.filename}${code.language ? ` (${code.language})` : ""}` : code?.language;
|
|
1176
|
+
const body = [title, code?.source].filter(Boolean).join("\n");
|
|
1177
|
+
slide.addText(body, {
|
|
1178
|
+
...textBoxOptions(region, context, 11),
|
|
1179
|
+
fontFace: context.fonts.code,
|
|
1180
|
+
fill: { color: context.colors.surface },
|
|
1181
|
+
line: { color: context.colors.border, pt: 0.75 },
|
|
1182
|
+
margin: 8,
|
|
1183
|
+
fit: "shrink"
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function addMetricPayload(slide, value, region, context) {
|
|
1188
|
+
const metric = isPlainObject(value) ? value : { value };
|
|
1189
|
+
slide.addText(String(metric.value ?? ""), {
|
|
1190
|
+
x: region.x,
|
|
1191
|
+
y: region.y,
|
|
1192
|
+
w: region.w,
|
|
1193
|
+
h: Math.min(region.h, 0.68),
|
|
1194
|
+
margin: 0,
|
|
1195
|
+
fontFace: context.fonts.heading,
|
|
1196
|
+
fontSize: 30,
|
|
1197
|
+
bold: true,
|
|
1198
|
+
color: context.colors.accent,
|
|
1199
|
+
fit: "shrink"
|
|
1200
|
+
});
|
|
1201
|
+
slide.addText([metric.label, metric.description, metric.delta].filter(Boolean).join("\n"), {
|
|
1202
|
+
x: region.x,
|
|
1203
|
+
y: region.y + 0.76,
|
|
1204
|
+
w: region.w,
|
|
1205
|
+
h: Math.max(0.3, region.h - 0.78),
|
|
1206
|
+
margin: 0,
|
|
1207
|
+
fontFace: context.fonts.body,
|
|
1208
|
+
fontSize: 12,
|
|
1209
|
+
color: context.colors.text,
|
|
1210
|
+
fit: "shrink"
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function addQuotePayload(slide, value, region, context) {
|
|
1215
|
+
const quote = typeof value === "string" ? { text: value } : value;
|
|
1216
|
+
const attribution = quote?.attribution ? `\n- ${quote.attribution}` : "";
|
|
1217
|
+
slide.addText(`${quote?.text ?? ""}${attribution}`, {
|
|
1218
|
+
...textBoxOptions(region, context, 17),
|
|
1219
|
+
italic: true,
|
|
1220
|
+
color: context.colors.text,
|
|
1221
|
+
fit: "shrink"
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function addTimelinePayload(slide, value, region, context) {
|
|
1226
|
+
const timeline = Array.isArray(value) ? { events: value } : value;
|
|
1227
|
+
const events = Array.isArray(timeline?.events) ? timeline.events : [];
|
|
1228
|
+
const lines = events.map((event) => {
|
|
1229
|
+
const when = event.when ? `${event.when}: ` : "";
|
|
1230
|
+
const detail = event.description ? ` - ${event.description}` : "";
|
|
1231
|
+
return `${when}${event.what ?? ""}${detail}`;
|
|
1232
|
+
});
|
|
1233
|
+
slide.addText(lines.join("\n"), textBoxOptions(region, context, 13));
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function addPlaceholderPayload(slide, label, value, region, context) {
|
|
1237
|
+
slide.addShape("rect", {
|
|
1238
|
+
x: region.x,
|
|
1239
|
+
y: region.y,
|
|
1240
|
+
w: region.w,
|
|
1241
|
+
h: region.h,
|
|
1242
|
+
fill: { color: context.colors.surface, transparency: 10 },
|
|
1243
|
+
line: { color: context.colors.border, pt: 0.75 }
|
|
1244
|
+
});
|
|
1245
|
+
slide.addText(`${label}\n${summarizeValue(value)}`, {
|
|
1246
|
+
x: region.x + 0.12,
|
|
1247
|
+
y: region.y + 0.12,
|
|
1248
|
+
w: Math.max(0.2, region.w - 0.24),
|
|
1249
|
+
h: Math.max(0.2, region.h - 0.24),
|
|
1250
|
+
margin: 0,
|
|
1251
|
+
fontFace: context.fonts.body,
|
|
1252
|
+
fontSize: 11,
|
|
1253
|
+
color: context.colors.mutedText,
|
|
1254
|
+
fit: "shrink",
|
|
1255
|
+
valign: "mid",
|
|
1256
|
+
align: "center"
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function textBoxOptions(region, context, fontSize) {
|
|
1261
|
+
return {
|
|
1262
|
+
x: region.x,
|
|
1263
|
+
y: region.y,
|
|
1264
|
+
w: region.w,
|
|
1265
|
+
h: region.h,
|
|
1266
|
+
margin: 4,
|
|
1267
|
+
fontFace: context.fonts.body,
|
|
1268
|
+
fontSize,
|
|
1269
|
+
color: context.colors.text,
|
|
1270
|
+
breakLine: false,
|
|
1271
|
+
fit: "shrink",
|
|
1272
|
+
valign: "mid"
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function textRuns(value, context, fallbackFontSize) {
|
|
1277
|
+
const runs = Array.isArray(value) ? value : [value];
|
|
1278
|
+
return runs.map((run) => {
|
|
1279
|
+
if (typeof run === "string") {
|
|
1280
|
+
return {
|
|
1281
|
+
text: run,
|
|
1282
|
+
options: {
|
|
1283
|
+
color: context.colors.text,
|
|
1284
|
+
fontFace: context.fonts.body,
|
|
1285
|
+
fontSize: fallbackFontSize
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
return {
|
|
1290
|
+
text: String(run?.text ?? ""),
|
|
1291
|
+
options: {
|
|
1292
|
+
bold: run?.bold,
|
|
1293
|
+
italic: run?.italic,
|
|
1294
|
+
underline: run?.underline ? { color: normalizeHex(run.color ?? context.colors.text) } : undefined,
|
|
1295
|
+
strike: run?.strikethrough ? "sngStrike" : undefined,
|
|
1296
|
+
color: normalizeHex(run?.color ?? context.colors.text),
|
|
1297
|
+
fontFace: run?.fontFamily ?? context.fonts.body,
|
|
1298
|
+
fontSize: run?.fontSize ?? fallbackFontSize,
|
|
1299
|
+
hyperlink: run?.link ? { url: run.link } : undefined
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function toPptxChartData(chart) {
|
|
1306
|
+
const data = chart?.data;
|
|
1307
|
+
if (!data || !Array.isArray(data.columns) || !Array.isArray(data.rows)) return null;
|
|
1308
|
+
if (data.columns.length < 2 || data.rows.length === 0) return null;
|
|
1309
|
+
|
|
1310
|
+
const labels = data.rows.map((row) => stringifyText(row?.[0]));
|
|
1311
|
+
const series = data.columns.slice(1).map((name, seriesIndex) => ({
|
|
1312
|
+
name: stringifyText(name),
|
|
1313
|
+
labels,
|
|
1314
|
+
values: data.rows.map((row) => numericValue(row?.[seriesIndex + 1]))
|
|
1315
|
+
}));
|
|
1316
|
+
const mapped = mapChartType(chart.type);
|
|
1317
|
+
|
|
1318
|
+
return { ...mapped, series };
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
function mapChartType(type) {
|
|
1322
|
+
const normalized = String(type ?? "").toLowerCase();
|
|
1323
|
+
if (normalized.includes("pie")) return { type: "pie" };
|
|
1324
|
+
if (normalized.includes("doughnut") || normalized.includes("donut")) return { type: "doughnut" };
|
|
1325
|
+
if (normalized.includes("area")) return { type: "area" };
|
|
1326
|
+
if (normalized.includes("line") || normalized.includes("sparkline")) return { type: "line" };
|
|
1327
|
+
if (normalized.includes("scatter")) return { type: "scatter" };
|
|
1328
|
+
if (normalized.includes("radar")) return { type: "radar" };
|
|
1329
|
+
if (normalized.includes("bar")) {
|
|
1330
|
+
return {
|
|
1331
|
+
type: "bar",
|
|
1332
|
+
barDir: "bar",
|
|
1333
|
+
barGrouping: normalized.includes("stacked") ? "stacked" : "clustered"
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
return {
|
|
1337
|
+
type: "bar",
|
|
1338
|
+
barDir: "col",
|
|
1339
|
+
barGrouping: normalized.includes("stacked") ? "stacked" : "clustered"
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
async function resolveImage(asset, presentation, options, path) {
|
|
1344
|
+
const assetObject = dereferenceAsset(asset, presentation, path);
|
|
1345
|
+
const src = typeof assetObject === "string" ? assetObject : assetObject?.src;
|
|
1346
|
+
if (!src) {
|
|
1347
|
+
if (options.strictAssets) {
|
|
1348
|
+
throw new OPFPptxError("missing-asset", "Image asset is missing a source.", { path });
|
|
1349
|
+
}
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const resolvedByHost = options.imageResolver
|
|
1354
|
+
? await options.imageResolver(src, { asset: assetObject, presentation, path })
|
|
1355
|
+
: null;
|
|
1356
|
+
if (resolvedByHost) return normalizeResolvedImage(resolvedByHost, assetObject);
|
|
1357
|
+
|
|
1358
|
+
if (src.startsWith("data:")) return { data: src };
|
|
1359
|
+
if (/^https?:\/\//i.test(src)) {
|
|
1360
|
+
if (options.strictAssets) {
|
|
1361
|
+
throw new OPFPptxError("unsupported-asset", "Remote image assets require an imageResolver; network fetch is not used.", {
|
|
1362
|
+
path,
|
|
1363
|
+
src
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
if (src.startsWith("asset:")) {
|
|
1369
|
+
if (options.strictAssets) {
|
|
1370
|
+
throw new OPFPptxError("missing-asset", "Image asset reference could not be resolved.", { path, src });
|
|
1371
|
+
}
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
const pathValue = options.baseDir && !isAbsolutePath(src) ? joinPath(options.baseDir, src) : src;
|
|
1376
|
+
return { path: pathValue };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function normalizeResolvedImage(value, asset) {
|
|
1380
|
+
if (typeof value === "string") {
|
|
1381
|
+
if (value.startsWith("data:")) return { data: value };
|
|
1382
|
+
return { path: value };
|
|
1383
|
+
}
|
|
1384
|
+
if (value instanceof Uint8Array) {
|
|
1385
|
+
const mediaType = asset?.mediaType ?? "image/png";
|
|
1386
|
+
return { data: `data:${mediaType};base64,${bytesToBase64(value)}` };
|
|
1387
|
+
}
|
|
1388
|
+
if (value && typeof value === "object") {
|
|
1389
|
+
if (typeof value.data === "string") return { data: value.data };
|
|
1390
|
+
if (value.data instanceof Uint8Array) {
|
|
1391
|
+
const mediaType = value.mediaType ?? asset?.mediaType ?? "image/png";
|
|
1392
|
+
return { data: `data:${mediaType};base64,${bytesToBase64(value.data)}` };
|
|
1393
|
+
}
|
|
1394
|
+
if (typeof value.path === "string") return { path: value.path };
|
|
1395
|
+
}
|
|
1396
|
+
throw new OPFPptxError("invalid-image-resolution", "imageResolver must return a data URI, path, Uint8Array, or { data | path } object.");
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function dereferenceAsset(asset, presentation, path, seen = new Set()) {
|
|
1400
|
+
const source = typeof asset === "string" ? asset : asset?.src;
|
|
1401
|
+
if (typeof source === "string" && source.startsWith("asset:")) {
|
|
1402
|
+
const id = source.slice("asset:".length);
|
|
1403
|
+
if (seen.has(id)) {
|
|
1404
|
+
throw new OPFPptxError("invalid-asset-reference", "Circular asset reference detected.", { path, assetId: id });
|
|
1405
|
+
}
|
|
1406
|
+
seen.add(id);
|
|
1407
|
+
const target = presentation.assets?.[id];
|
|
1408
|
+
if (!target) return asset;
|
|
1409
|
+
return dereferenceAsset(target, presentation, path, seen);
|
|
1410
|
+
}
|
|
1411
|
+
return asset;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
function assetAlt(asset, presentation) {
|
|
1415
|
+
const resolved = dereferenceAsset(asset, presentation, "asset-alt");
|
|
1416
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
1417
|
+
return resolved.alt ?? resolved.title ?? resolved.description;
|
|
1418
|
+
}
|
|
1419
|
+
return undefined;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function resolveCatalogRecord(presentation, kind, reference, fallbackId) {
|
|
1423
|
+
const id = referenceId(reference) ?? fallbackId;
|
|
1424
|
+
const inlineRecords = normalizeRecords(presentation.catalogs?.[kind]);
|
|
1425
|
+
return findById(inlineRecords, id)
|
|
1426
|
+
?? findById(defaultCatalog(kind), id)
|
|
1427
|
+
?? findById(defaultCatalog(kind), fallbackId)
|
|
1428
|
+
?? null;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
function resolveDesignRecord(presentation, kind, reference, fallbackId) {
|
|
1432
|
+
const base = resolveCatalogRecord(presentation, kind, reference, fallbackId) ?? {};
|
|
1433
|
+
return {
|
|
1434
|
+
...base,
|
|
1435
|
+
...(isPlainObject(reference) ? withoutSchema(reference) : {})
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
function referenceId(reference) {
|
|
1440
|
+
if (typeof reference === "string") return reference;
|
|
1441
|
+
if (isPlainObject(reference) && typeof reference.id === "string") return reference.id;
|
|
1442
|
+
return null;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function defaultCatalog(kind) {
|
|
1446
|
+
return Array.isArray(bundledCatalogs[kind]) ? bundledCatalogs[kind] : [];
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function normalizeRecords(catalog) {
|
|
1450
|
+
if (!catalog) return [];
|
|
1451
|
+
if (Array.isArray(catalog)) return catalog;
|
|
1452
|
+
if (Array.isArray(catalog.records)) return catalog.records;
|
|
1453
|
+
return [];
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function findById(records, id) {
|
|
1457
|
+
return records.find((record) => record?.id === id) ?? null;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function withoutSchema(value) {
|
|
1461
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => key !== "$schema"));
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function resolveDimensions(value) {
|
|
1465
|
+
if (typeof value === "string") return DIMENSION_PRESETS[value] ?? DIMENSION_PRESETS.widescreen;
|
|
1466
|
+
if (isPlainObject(value)) {
|
|
1467
|
+
const preset = DIMENSION_PRESETS[value.preset] ?? DIMENSION_PRESETS.widescreen;
|
|
1468
|
+
return {
|
|
1469
|
+
widthInches: value.widthInches ?? preset.widthInches,
|
|
1470
|
+
heightInches: value.heightInches ?? preset.heightInches
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
return DIMENSION_PRESETS.widescreen;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function resolveBackground(value, colorScheme) {
|
|
1477
|
+
if (typeof value === "string") {
|
|
1478
|
+
if (value.startsWith("#")) return normalizeHex(value);
|
|
1479
|
+
return normalizeHex(colorScheme[value] ?? colorScheme.background ?? colorScheme.light1 ?? "#FFFFFF");
|
|
1480
|
+
}
|
|
1481
|
+
if (isPlainObject(value)) {
|
|
1482
|
+
if (value.type === "solid" && value.color) return normalizeHex(value.color);
|
|
1483
|
+
if (value.type === "theme" && value.slot) {
|
|
1484
|
+
return normalizeHex(colorScheme[value.slot] ?? colorScheme.light1 ?? "#FFFFFF");
|
|
1485
|
+
}
|
|
1486
|
+
if (value.backgroundColor) return normalizeHex(value.backgroundColor);
|
|
1487
|
+
}
|
|
1488
|
+
return normalizeHex(colorScheme.background ?? colorScheme.light1 ?? "#FFFFFF");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function resolveFonts(fontScheme) {
|
|
1492
|
+
const heading = fontFamily(fontScheme.heading) ?? fontScheme.major ?? "Aptos Display";
|
|
1493
|
+
const body = fontFamily(fontScheme.body) ?? fontScheme.minor ?? "Aptos";
|
|
1494
|
+
return {
|
|
1495
|
+
id: fontScheme.id,
|
|
1496
|
+
heading,
|
|
1497
|
+
body,
|
|
1498
|
+
code: fontFamily(fontScheme.code) ?? "Consolas"
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function fontFamily(value) {
|
|
1503
|
+
if (typeof value === "string") return value;
|
|
1504
|
+
if (isPlainObject(value) && typeof value.family === "string") return value.family;
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function readableTextColor(background, colorScheme) {
|
|
1509
|
+
return isDarkHex(background)
|
|
1510
|
+
? normalizeHex(colorScheme.light1 ?? "#FFFFFF")
|
|
1511
|
+
: normalizeHex(colorScheme.text ?? colorScheme.dark1 ?? "#0F172A");
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
function normalizeHex(value) {
|
|
1515
|
+
if (typeof value !== "string") return "000000";
|
|
1516
|
+
const raw = value.trim().replace(/^#/, "");
|
|
1517
|
+
if (/^[0-9a-fA-F]{3}$/.test(raw)) {
|
|
1518
|
+
return raw.split("").map((char) => char + char).join("").toUpperCase();
|
|
1519
|
+
}
|
|
1520
|
+
if (/^[0-9a-fA-F]{6,8}$/.test(raw)) {
|
|
1521
|
+
return raw.slice(0, 6).toUpperCase();
|
|
1522
|
+
}
|
|
1523
|
+
return raw.toUpperCase();
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function isDarkHex(value) {
|
|
1527
|
+
const hex = normalizeHex(value);
|
|
1528
|
+
if (!/^[0-9A-F]{6}$/.test(hex)) return false;
|
|
1529
|
+
const red = Number.parseInt(hex.slice(0, 2), 16);
|
|
1530
|
+
const green = Number.parseInt(hex.slice(2, 4), 16);
|
|
1531
|
+
const blue = Number.parseInt(hex.slice(4, 6), 16);
|
|
1532
|
+
return (red * 299 + green * 587 + blue * 114) / 1000 < 128;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
function regionFromPromotedKey(key, area) {
|
|
1536
|
+
const [first, second] = key.includes(":") ? key.split(":") : [key, null];
|
|
1537
|
+
const rowPart = second ? first : isRowPart(first) ? first : "top+middle+bottom";
|
|
1538
|
+
const colPart = second ? second : isColumnPart(first) ? first : "left+center+right";
|
|
1539
|
+
const rowSpan = span(rowPart, ["top", "middle", "bottom"]);
|
|
1540
|
+
const colSpan = span(colPart, ["left", "center", "right"]);
|
|
1541
|
+
const cellW = area.w / 3;
|
|
1542
|
+
const cellH = area.h / 3;
|
|
1543
|
+
|
|
1544
|
+
return {
|
|
1545
|
+
x: area.x + colSpan.start * cellW,
|
|
1546
|
+
y: area.y + rowSpan.start * cellH,
|
|
1547
|
+
w: (colSpan.end - colSpan.start + 1) * cellW,
|
|
1548
|
+
h: (rowSpan.end - rowSpan.start + 1) * cellH
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
function isRowPart(value) {
|
|
1553
|
+
return value.split("+").every((part) => ["top", "middle", "bottom"].includes(part));
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function isColumnPart(value) {
|
|
1557
|
+
return value.split("+").every((part) => ["left", "center", "right"].includes(part));
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
function span(value, order) {
|
|
1561
|
+
const indexes = value.split("+").map((part) => order.indexOf(part)).filter((index) => index >= 0);
|
|
1562
|
+
if (indexes.length === 0) return { start: 0, end: order.length - 1 };
|
|
1563
|
+
return { start: Math.min(...indexes), end: Math.max(...indexes) };
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
function regionFromIndex(index, total, area) {
|
|
1567
|
+
if (total <= 1) return area;
|
|
1568
|
+
const columns = total === 2 ? 2 : Math.ceil(Math.sqrt(total));
|
|
1569
|
+
const rows = Math.ceil(total / columns);
|
|
1570
|
+
const row = Math.floor(index / columns);
|
|
1571
|
+
const col = index % columns;
|
|
1572
|
+
return {
|
|
1573
|
+
x: area.x + (area.w / columns) * col,
|
|
1574
|
+
y: area.y + (area.h / rows) * row,
|
|
1575
|
+
w: area.w / columns,
|
|
1576
|
+
h: area.h / rows
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function insetRegion(region, amount) {
|
|
1581
|
+
return {
|
|
1582
|
+
x: region.x + amount,
|
|
1583
|
+
y: region.y + amount,
|
|
1584
|
+
w: Math.max(0.2, region.w - amount * 2),
|
|
1585
|
+
h: Math.max(0.2, region.h - amount * 2)
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function normalizePptxZip(raw, context) {
|
|
1590
|
+
let entries;
|
|
1591
|
+
try {
|
|
1592
|
+
entries = unzipSync(raw);
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
throw new OPFPptxError("packaging-failed", "Generated PPTX could not be read back as a ZIP.", {
|
|
1595
|
+
cause: errorMessage(error)
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
const output = {};
|
|
1600
|
+
const renameMaps = buildRenameMaps(Object.keys(entries));
|
|
1601
|
+
for (const path of Object.keys(entries).sort()) {
|
|
1602
|
+
const normalizedPath = normalizePartPath(path, renameMaps);
|
|
1603
|
+
const bytes = normalizePartBytes(path, entries[path], context, renameMaps);
|
|
1604
|
+
output[normalizedPath] = [bytes, {
|
|
1605
|
+
level: context.compressionLevel,
|
|
1606
|
+
mtime: context.zipDate
|
|
1607
|
+
}];
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
return zipSync(output, {
|
|
1611
|
+
level: context.compressionLevel,
|
|
1612
|
+
mtime: context.zipDate
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function normalizeCoreProperties(xml, timestamp) {
|
|
1617
|
+
return xml
|
|
1618
|
+
.replace(/<dcterms:created xsi:type="dcterms:W3CDTF">[^<]*<\/dcterms:created>/g, `<dcterms:created xsi:type="dcterms:W3CDTF">${timestamp}</dcterms:created>`)
|
|
1619
|
+
.replace(/<dcterms:modified xsi:type="dcterms:W3CDTF">[^<]*<\/dcterms:modified>/g, `<dcterms:modified xsi:type="dcterms:W3CDTF">${timestamp}</dcterms:modified>`);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function normalizePartBytes(path, bytes, context, renameMaps) {
|
|
1623
|
+
if (path.endsWith(".xlsx")) {
|
|
1624
|
+
return normalizeNestedZip(bytes, context);
|
|
1625
|
+
}
|
|
1626
|
+
if (path === "docProps/core.xml") {
|
|
1627
|
+
return encodeText(normalizePartReferences(normalizeCoreProperties(decodeText(bytes), context.timestamp), renameMaps));
|
|
1628
|
+
}
|
|
1629
|
+
if (isXmlPart(path)) {
|
|
1630
|
+
return encodeText(normalizePartReferences(decodeText(bytes), renameMaps));
|
|
1631
|
+
}
|
|
1632
|
+
return bytes;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function isXmlPart(path) {
|
|
1636
|
+
return path.endsWith(".xml") || path.endsWith(".rels") || path === "[Content_Types].xml";
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
function buildRenameMaps(paths) {
|
|
1640
|
+
return {
|
|
1641
|
+
charts: numberedFilenameMap(paths, /^ppt\/charts\/chart(\d+)\.xml$/),
|
|
1642
|
+
worksheets: numberedFilenameMap(paths, /^ppt\/embeddings\/Microsoft_Excel_Worksheet(\d+)\.xlsx$/)
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function numberedFilenameMap(paths, pattern) {
|
|
1647
|
+
const ids = [...new Set(paths.flatMap((path) => {
|
|
1648
|
+
const match = pattern.exec(path);
|
|
1649
|
+
return match ? [Number(match[1])] : [];
|
|
1650
|
+
}))].sort((a, b) => a - b);
|
|
1651
|
+
return new Map(ids.map((id, index) => [String(id), String(index + 1)]));
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function normalizePartPath(path, renameMaps) {
|
|
1655
|
+
return normalizePartReferences(path, renameMaps);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function normalizePartReferences(value, renameMaps) {
|
|
1659
|
+
let output = value;
|
|
1660
|
+
for (const [oldId, newId] of renameMaps.charts) {
|
|
1661
|
+
output = output.replace(new RegExp(escapeRegExp(`chart${oldId}.xml`), "g"), `chart${newId}.xml`);
|
|
1662
|
+
}
|
|
1663
|
+
for (const [oldId, newId] of renameMaps.worksheets) {
|
|
1664
|
+
output = output.replace(
|
|
1665
|
+
new RegExp(escapeRegExp(`Microsoft_Excel_Worksheet${oldId}.xlsx`), "g"),
|
|
1666
|
+
`Microsoft_Excel_Worksheet${newId}.xlsx`
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
return output;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function normalizeNestedZip(bytes, context) {
|
|
1673
|
+
const entries = unzipSync(bytes);
|
|
1674
|
+
const output = {};
|
|
1675
|
+
for (const path of Object.keys(entries).sort()) {
|
|
1676
|
+
const entryBytes = path === "docProps/core.xml"
|
|
1677
|
+
? encodeText(normalizeCoreProperties(decodeText(entries[path]), context.timestamp))
|
|
1678
|
+
: entries[path];
|
|
1679
|
+
output[path] = [entryBytes, {
|
|
1680
|
+
level: context.compressionLevel,
|
|
1681
|
+
mtime: context.zipDate
|
|
1682
|
+
}];
|
|
1683
|
+
}
|
|
1684
|
+
return zipSync(output, {
|
|
1685
|
+
level: context.compressionLevel,
|
|
1686
|
+
mtime: context.zipDate
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function escapeRegExp(value) {
|
|
1691
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
async function withDeterministicRandom(seed, callback) {
|
|
1695
|
+
const originalRandom = Math.random;
|
|
1696
|
+
let state = seed >>> 0;
|
|
1697
|
+
Math.random = () => {
|
|
1698
|
+
state = (1664525 * state + 1013904223) >>> 0;
|
|
1699
|
+
return state / 0x100000000;
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
try {
|
|
1703
|
+
return await callback();
|
|
1704
|
+
} finally {
|
|
1705
|
+
Math.random = originalRandom;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
function asUint8Array(value) {
|
|
1710
|
+
if (value instanceof Uint8Array) return value;
|
|
1711
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
1712
|
+
throw new OPFPptxError("invalid-output", "PPTX generator returned an unsupported output type.");
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function stringifyText(value) {
|
|
1716
|
+
if (value === null || value === undefined) return "";
|
|
1717
|
+
if (Array.isArray(value)) return value.map(stringifyText).join("");
|
|
1718
|
+
if (isPlainObject(value)) {
|
|
1719
|
+
if (value.text !== undefined) return stringifyText(value.text);
|
|
1720
|
+
if (value.value !== undefined) return stringifyText(value.value);
|
|
1721
|
+
return JSON.stringify(value);
|
|
1722
|
+
}
|
|
1723
|
+
return String(value);
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
function numericValue(value) {
|
|
1727
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1728
|
+
const parsed = Number.parseFloat(String(value ?? "").replace(/[^0-9.-]/g, ""));
|
|
1729
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function summarizeValue(value) {
|
|
1733
|
+
const text = stringifyText(value);
|
|
1734
|
+
if (text.length > 160) return `${text.slice(0, 157)}...`;
|
|
1735
|
+
return text;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function normalizeAuthor(author) {
|
|
1739
|
+
if (typeof author === "string") return author;
|
|
1740
|
+
if (Array.isArray(author)) return author.join("; ");
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function isPlainObject(value) {
|
|
1745
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function isAbsolutePath(value) {
|
|
1749
|
+
return /^(?:[a-zA-Z]:[\\/]|\/)/.test(value);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
function joinPath(base, relative) {
|
|
1753
|
+
return `${String(base).replace(/[\\/]+$/, "")}/${String(relative).replace(/^[\\/]+/, "")}`;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
function bytesToBase64(bytes) {
|
|
1757
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
1758
|
+
let binary = "";
|
|
1759
|
+
const chunkSize = 0x8000;
|
|
1760
|
+
for (let index = 0; index < bytes.length; index += chunkSize) {
|
|
1761
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize));
|
|
1762
|
+
}
|
|
1763
|
+
return btoa(binary);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function decodeText(bytes) {
|
|
1767
|
+
return new TextDecoder().decode(bytes);
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function encodeText(value) {
|
|
1771
|
+
return new TextEncoder().encode(value);
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
function errorMessage(error) {
|
|
1775
|
+
return error instanceof Error ? error.message : String(error);
|
|
1776
|
+
}
|