@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/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
+ }