@pollium/vyva-convert 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +100 -2
  2. package/dist/attachment.d.ts +3 -0
  3. package/dist/attachment.js +8 -0
  4. package/dist/clean-content.d.ts +6 -0
  5. package/dist/clean-content.js +63 -0
  6. package/dist/clean-html.d.ts +3 -0
  7. package/dist/clean-html.js +161 -0
  8. package/dist/errors.d.ts +7 -0
  9. package/dist/errors.js +11 -0
  10. package/dist/export/bundle.d.ts +34 -0
  11. package/dist/export/bundle.js +41 -0
  12. package/dist/export/docx.d.ts +5 -0
  13. package/dist/export/docx.js +382 -0
  14. package/dist/export/fonts.d.ts +16 -0
  15. package/dist/export/fonts.js +37 -0
  16. package/dist/export/html-blocks.d.ts +58 -0
  17. package/dist/export/html-blocks.js +170 -0
  18. package/dist/export/html.d.ts +8 -0
  19. package/dist/export/html.js +31 -0
  20. package/dist/export/image-size.d.ts +8 -0
  21. package/dist/export/image-size.js +83 -0
  22. package/dist/export/pdf.d.ts +28 -0
  23. package/dist/export/pdf.js +84 -0
  24. package/dist/export/pptx.d.ts +5 -0
  25. package/dist/export/pptx.js +334 -0
  26. package/dist/export.d.ts +19 -0
  27. package/dist/export.js +39 -0
  28. package/dist/font-widths.d.ts +8 -0
  29. package/dist/font-widths.js +3430 -0
  30. package/dist/gotenberg.d.ts +8 -0
  31. package/dist/gotenberg.js +39 -0
  32. package/dist/import/documents.d.ts +12 -0
  33. package/dist/import/documents.js +44 -0
  34. package/dist/import/html-to-blocks.d.ts +6 -0
  35. package/dist/import/html-to-blocks.js +58 -0
  36. package/dist/import/images.d.ts +10 -0
  37. package/dist/import/images.js +23 -0
  38. package/dist/import/pdf-lines.d.ts +4 -0
  39. package/dist/import/pdf-lines.js +206 -0
  40. package/dist/import/pdf-slides.d.ts +7 -0
  41. package/dist/import/pdf-slides.js +21 -0
  42. package/dist/import/pptx-geometry.d.ts +11 -0
  43. package/dist/import/pptx-geometry.js +74 -0
  44. package/dist/import/pptx-package.d.ts +22 -0
  45. package/dist/import/pptx-package.js +61 -0
  46. package/dist/import/pptx-placeholders.d.ts +11 -0
  47. package/dist/import/pptx-placeholders.js +44 -0
  48. package/dist/import/pptx-slide.d.ts +19 -0
  49. package/dist/import/pptx-slide.js +348 -0
  50. package/dist/import/pptx-theme.d.ts +19 -0
  51. package/dist/import/pptx-theme.js +160 -0
  52. package/dist/import/pptx-xml.d.ts +23 -0
  53. package/dist/import/pptx-xml.js +27 -0
  54. package/dist/import/pptx.d.ts +9 -0
  55. package/dist/import/pptx.js +126 -0
  56. package/dist/import/titles.d.ts +3 -0
  57. package/dist/import/titles.js +6 -0
  58. package/dist/import.d.ts +16 -0
  59. package/dist/import.js +68 -0
  60. package/dist/libreoffice.d.ts +8 -0
  61. package/dist/libreoffice.js +39 -0
  62. package/dist/text-measure.d.ts +5 -0
  63. package/dist/text-measure.js +102 -0
  64. package/package.json +50 -3
@@ -0,0 +1,44 @@
1
+ import { all, attr, first, kid, kids } from "./pptx-xml.js";
2
+ //#region src/import/pptx-placeholders.ts
3
+ const TITLE_TYPES = ["title", "ctrTitle"];
4
+ const MASTER_TYPES = {
5
+ ctrTitle: "title",
6
+ subTitle: "body",
7
+ obj: "body"
8
+ };
9
+ const isDefined = (value) => value !== void 0;
10
+ const placeholderType = (shape) => attr(first(shape, "p:ph"), "type") ?? "body";
11
+ const masterType = (type) => MASTER_TYPES[type] ?? type;
12
+ const placeholdersOf = (part) => part?.tree === void 0 ? [] : all(part.tree, "p:sp").filter((shape) => first(shape, "p:ph") !== void 0);
13
+ const placeholderChain = (shape, placeholder, parts) => {
14
+ const type = attr(placeholder, "type") ?? "body";
15
+ const idx = attr(placeholder, "idx");
16
+ const layoutShapes = placeholdersOf(parts.layout);
17
+ const inLayout = (idx === void 0 ? void 0 : layoutShapes.find((candidate) => attr(first(candidate, "p:ph"), "idx") === idx)) ?? layoutShapes.find((candidate) => placeholderType(candidate) === type);
18
+ const wanted = masterType(inLayout === void 0 ? type : placeholderType(inLayout));
19
+ return [
20
+ shape,
21
+ inLayout,
22
+ placeholdersOf(parts.master).find((candidate) => masterType(placeholderType(candidate)) === wanted)
23
+ ].filter(isDefined);
24
+ };
25
+ const levelOf = (shape) => kid(kid(kid(shape, "p:txBody"), "a:lstStyle"), "a:lvl1pPr");
26
+ const masterLevel = (parts, type) => {
27
+ if (type === null || parts.master === null) return void 0;
28
+ const style = TITLE_TYPES.includes(type) ? "p:titleStyle" : masterType(type) === "body" ? "p:bodyStyle" : "p:otherStyle";
29
+ return kid(first(parts.master.root, style), "a:lvl1pPr");
30
+ };
31
+ const bulletsByDefault = (levels) => {
32
+ for (const level of levels) {
33
+ if (kids(level, "a:buNone").length > 0) return false;
34
+ if (kids(level, "a:buChar").length > 0 || kids(level, "a:buAutoNum").length > 0) return true;
35
+ }
36
+ return false;
37
+ };
38
+ const showsMasterShapes = (part) => attr(part.root, "showMasterSp") !== "0";
39
+ const decorOf = (parts) => {
40
+ if (!showsMasterShapes(parts.slide) || parts.layout === null) return [];
41
+ return parts.master !== null && showsMasterShapes(parts.layout) ? [parts.master, parts.layout] : [parts.layout];
42
+ };
43
+ //#endregion
44
+ export { TITLE_TYPES, bulletsByDefault, decorOf, isDefined, levelOf, masterLevel, placeholderChain };
@@ -0,0 +1,19 @@
1
+ import { SlideParts } from "./pptx-package.js";
2
+ import { DeckTheme, Slide } from "@pollium/vyva-core/content";
3
+ //#region src/import/pptx-slide.d.ts
4
+ export interface SlideReaderOptions {
5
+ scale: number;
6
+ fontScale: number;
7
+ theme: DeckTheme;
8
+ image: (path: string) => Promise<string | null>;
9
+ }
10
+ export interface ReadSlide {
11
+ slide: Slide;
12
+ title: string;
13
+ }
14
+ export default class SlideReader {
15
+ #private;
16
+ constructor(options: SlideReaderOptions);
17
+ read(parts: SlideParts): Promise<ReadSlide>;
18
+ }
19
+ //#endregion
@@ -0,0 +1,348 @@
1
+ import { cleanHtml } from "../clean-html.js";
2
+ import { unwrappedWidth } from "../text-measure.js";
3
+ import { all, attr, first, kid, kids, num, tags, textOf } from "./pptx-xml.js";
4
+ import { TITLE_TYPES, bulletsByDefault, decorOf, isDefined, levelOf, masterLevel, placeholderChain } from "./pptx-placeholders.js";
5
+ import { outlineOf } from "./pptx-geometry.js";
6
+ import { MAX_CELL_TEXT, MAX_TABLE_COLUMNS, MAX_TABLE_ROWS } from "@pollium/vyva-core/content";
7
+ import { newId } from "@pollium/vyva-core/ids";
8
+ import { escapeHtml } from "@pollium/vyva-core/escape";
9
+ //#region src/import/pptx-slide.ts
10
+ const MAX_ELEMENTS = 300;
11
+ const ANGLE_UNIT = 6e4;
12
+ const DEFAULT_LINE_WIDTH = 12700;
13
+ const MAX_STROKE = 80;
14
+ const PX_PER_POINT = 2;
15
+ const TITLE_SIZE = 64;
16
+ const BODY_SIZE = 32;
17
+ const BULLET_TYPES = ["body", "obj"];
18
+ const ALIGNS = {
19
+ l: "left",
20
+ ctr: "center",
21
+ r: "right",
22
+ just: "justify"
23
+ };
24
+ const IDENTITY = {
25
+ offX: 0,
26
+ offY: 0,
27
+ chOffX: 0,
28
+ chOffY: 0,
29
+ scaleX: 1,
30
+ scaleY: 1
31
+ };
32
+ const NO_LOOK = {
33
+ size: null,
34
+ color: null,
35
+ isBold: null,
36
+ align: null
37
+ };
38
+ const mergeLooks = (looks) => looks.reduce((merged, look) => ({
39
+ size: merged.size ?? look.size,
40
+ color: merged.color ?? look.color,
41
+ isBold: merged.isBold ?? look.isBold,
42
+ align: merged.align ?? look.align
43
+ }), NO_LOOK);
44
+ const innerGroup = (node, group) => {
45
+ const xfrm = first(kid(node, "p:grpSpPr") ?? node, "a:xfrm");
46
+ const off = kid(xfrm, "a:off");
47
+ const ext = kid(xfrm, "a:ext");
48
+ const childOff = kid(xfrm, "a:chOff");
49
+ const childExt = kid(xfrm, "a:chExt");
50
+ return {
51
+ offX: group.offX + (num(attr(off, "x")) - group.chOffX) * group.scaleX,
52
+ offY: group.offY + (num(attr(off, "y")) - group.chOffY) * group.scaleY,
53
+ chOffX: num(attr(childOff, "x")),
54
+ chOffY: num(attr(childOff, "y")),
55
+ scaleX: group.scaleX * num(attr(ext, "cx"), 1) / Math.max(1, num(attr(childExt, "cx"), 1)),
56
+ scaleY: group.scaleY * num(attr(ext, "cy"), 1) / Math.max(1, num(attr(childExt, "cy"), 1))
57
+ };
58
+ };
59
+ const shapeKindOf = (geometry) => geometry === "roundRect" ? "rounded" : geometry === "ellipse" ? "ellipse" : geometry === "triangle" ? "triangle" : geometry === "line" || geometry === "straightConnector1" ? "line" : "rect";
60
+ const styleColor = (shape, reference, palette) => {
61
+ const ref = kid(kid(shape, "p:style"), reference);
62
+ return ref === void 0 || attr(ref, "idx") === "0" ? null : palette.color(ref);
63
+ };
64
+ var SlideReader = class {
65
+ #options;
66
+ constructor(options) {
67
+ this.#options = options;
68
+ }
69
+ async read(parts) {
70
+ const elements = [];
71
+ const titles = [];
72
+ for (const part of decorOf(parts)) if (part.tree !== void 0) await this.#visit({
73
+ part,
74
+ parts,
75
+ isDecor: true,
76
+ elements,
77
+ titles
78
+ }, part.tree, IDENTITY);
79
+ if (parts.slide.tree !== void 0) await this.#visit({
80
+ part: parts.slide,
81
+ parts,
82
+ isDecor: false,
83
+ elements,
84
+ titles
85
+ }, parts.slide.tree, IDENTITY);
86
+ return {
87
+ slide: {
88
+ id: newId(),
89
+ background: await this.#background(parts),
90
+ elements: elements.slice(0, MAX_ELEMENTS)
91
+ },
92
+ title: titles[0] ?? ""
93
+ };
94
+ }
95
+ async #visit(visit, container, group) {
96
+ for (const node of tags(container)) if (node.name === "p:sp") this.#shapeInto(visit, node, group);
97
+ else if (node.name === "p:pic") await this.#pictureInto(visit, node, group);
98
+ else if (node.name === "p:graphicFrame") this.#tableInto(visit, node, group);
99
+ else if (node.name === "p:grpSp") await this.#visit(visit, node, innerGroup(node, group));
100
+ }
101
+ #transformOf(shapeProps, group) {
102
+ const xfrm = kid(shapeProps, "a:xfrm") ?? kid(shapeProps, "p:xfrm");
103
+ const off = kid(xfrm, "a:off");
104
+ const ext = kid(xfrm, "a:ext");
105
+ if (off === void 0 || ext === void 0) return null;
106
+ const { scale } = this.#options;
107
+ const x = group.offX + (num(attr(off, "x")) - group.chOffX) * group.scaleX;
108
+ const y = group.offY + (num(attr(off, "y")) - group.chOffY) * group.scaleY;
109
+ return {
110
+ x: Math.round(x * scale),
111
+ y: Math.round(y * scale),
112
+ w: Math.max(1, Math.round(num(attr(ext, "cx")) * group.scaleX * scale)),
113
+ h: Math.max(1, Math.round(num(attr(ext, "cy")) * group.scaleY * scale)),
114
+ rotation: Math.round(num(attr(xfrm, "rot")) / ANGLE_UNIT) % 360
115
+ };
116
+ }
117
+ #frame(transform, isLocked) {
118
+ return {
119
+ id: newId(),
120
+ ...transform,
121
+ opacity: 1,
122
+ isLocked
123
+ };
124
+ }
125
+ #unwrapped(frame, html, style) {
126
+ const w = unwrappedWidth(html, style, this.#options.theme.font);
127
+ if (w <= frame.w) return frame;
128
+ const shift = {
129
+ left: 0,
130
+ justify: 0,
131
+ center: (w - frame.w) / 2,
132
+ right: w - frame.w
133
+ }[style.align];
134
+ return {
135
+ ...frame,
136
+ x: Math.round(frame.x - shift),
137
+ w
138
+ };
139
+ }
140
+ #pixels(size) {
141
+ const hundredths = num(size);
142
+ return hundredths > 0 ? Math.round(hundredths / 100 * PX_PER_POINT * this.#options.fontScale) : null;
143
+ }
144
+ #runLook(props, palette) {
145
+ const bold = attr(props, "b");
146
+ return {
147
+ size: this.#pixels(attr(props, "sz")),
148
+ color: palette.color(kid(props, "a:solidFill")),
149
+ isBold: bold === void 0 ? null : bold === "1"
150
+ };
151
+ }
152
+ #levelLook(level, palette) {
153
+ return {
154
+ ...this.#runLook(kid(level, "a:defRPr"), palette),
155
+ align: ALIGNS[attr(level, "algn") ?? ""] ?? null
156
+ };
157
+ }
158
+ #runHtml(run, box, palette) {
159
+ const props = kid(run, "a:rPr");
160
+ const text = escapeHtml(all(run, "a:t").map(textOf).join(""));
161
+ if (text === "") return "";
162
+ const look = this.#runLook(props, palette);
163
+ const styles = [...look.color === null || look.color.toLowerCase() === box.color.toLowerCase() ? [] : [`color:${look.color}`], ...look.size === null || look.size === box.fontSize ? [] : [`font-size:${look.size}px`]];
164
+ let html = styles.length === 0 ? text : `<span style="${styles.join(";")}">${text}</span>`;
165
+ if (look.isBold === true) html = `<strong>${html}</strong>`;
166
+ if (attr(props, "i") === "1") html = `<em>${html}</em>`;
167
+ if (attr(props, "u") !== void 0 && attr(props, "u") !== "none") html = `<u>${html}</u>`;
168
+ return html;
169
+ }
170
+ #textHtml(body, box, hasBullets, palette) {
171
+ const parts = [];
172
+ let isListOpen = false;
173
+ for (const paragraph of kids(body, "a:p")) {
174
+ const props = kid(paragraph, "a:pPr");
175
+ const own = ALIGNS[attr(props, "algn") ?? ""];
176
+ const inner = tags(paragraph).filter((node) => node.name === "a:r" || node.name === "a:br" || node.name === "a:fld").map((run) => run.name === "a:br" ? "<br>" : this.#runHtml(run, box, palette)).join("");
177
+ if ((props === void 0 ? hasBullets : kids(props, "a:buChar").length > 0 || kids(props, "a:buAutoNum").length > 0 || hasBullets && kids(props, "a:buNone").length === 0) && inner !== "") {
178
+ if (!isListOpen) parts.push("<ul>");
179
+ isListOpen = true;
180
+ parts.push(`<li>${inner}</li>`);
181
+ continue;
182
+ }
183
+ if (isListOpen) parts.push("</ul>");
184
+ isListOpen = false;
185
+ parts.push(`<p${own === void 0 || own === box.align ? "" : ` style="text-align:${own}"`}>${inner}</p>`);
186
+ }
187
+ if (isListOpen) parts.push("</ul>");
188
+ return cleanHtml(parts.join(""));
189
+ }
190
+ #textStyle(visit, chain, type, body) {
191
+ const palette = visit.part.palette;
192
+ const firstRun = all(body, "a:r").map((run) => kid(run, "a:rPr")).find(isDefined);
193
+ const firstAlign = ALIGNS[attr(kid(kids(body, "a:p")[0], "a:pPr"), "algn") ?? ""] ?? null;
194
+ const inherited = mergeLooks([
195
+ ...chain.map((shape) => this.#levelLook(levelOf(shape), palette)),
196
+ this.#levelLook(masterLevel(visit.parts, type), palette),
197
+ {
198
+ ...NO_LOOK,
199
+ color: styleColor(chain[0], "a:fontRef", palette)
200
+ }
201
+ ]);
202
+ const look = mergeLooks([{
203
+ ...this.#runLook(firstRun, palette),
204
+ align: firstAlign
205
+ }, inherited]);
206
+ const isBold = all(body, "a:r").every((run) => {
207
+ const bold = attr(kid(run, "a:rPr"), "b");
208
+ return bold === void 0 ? inherited.isBold === true : bold === "1";
209
+ });
210
+ const isTitle = TITLE_TYPES.includes(type ?? "");
211
+ const anchor = chain.map((shape) => attr(kid(kid(shape, "p:txBody"), "a:bodyPr"), "anchor")).find(isDefined);
212
+ const { theme } = this.#options;
213
+ return {
214
+ fontSize: look.size ?? (isTitle ? TITLE_SIZE : BODY_SIZE),
215
+ color: look.color ?? theme.text,
216
+ align: look.align ?? "left",
217
+ verticalAlign: anchor === "ctr" ? "middle" : anchor === "b" ? "bottom" : "top",
218
+ lineHeight: 1.2,
219
+ isBold
220
+ };
221
+ }
222
+ #shapeInto(visit, shape, group) {
223
+ const placeholder = first(shape, "p:ph");
224
+ if (placeholder !== void 0 && visit.isDecor) return;
225
+ const element = this.#shape(visit, shape, placeholder, group);
226
+ if (element === null) return;
227
+ visit.elements.push(element);
228
+ if (element.type === "text" && TITLE_TYPES.includes(attr(placeholder, "type") ?? "")) visit.titles.push(element.html.replace(/<[^>]+>/g, "").trim());
229
+ }
230
+ #shape(visit, shape, placeholder, group) {
231
+ const chain = placeholder === void 0 ? [shape] : placeholderChain(shape, placeholder, visit.parts);
232
+ const props = chain.map((link) => kid(link, "p:spPr"));
233
+ const transform = this.#transformOf(props[0], group) ?? props.slice(1).map((inherited) => this.#transformOf(inherited, IDENTITY)).find((found) => found !== null) ?? null;
234
+ if (transform === null) return null;
235
+ const { palette } = visit.part;
236
+ const kind = shapeKindOf(attr(chain.map((link) => first(link, "a:prstGeom")).find(isDefined), "prst") ?? "rect");
237
+ const filled = props.find((candidate) => palette.hasFill(candidate));
238
+ const fill = filled === void 0 ? styleColor(shape, "a:fillRef", palette) : palette.fill(filled);
239
+ const line = kid(props[0], "a:ln");
240
+ const stroke = line !== void 0 && palette.hasFill(line) ? palette.fill(line) : styleColor(shape, "a:lnRef", palette);
241
+ const strokeWidth = stroke === null ? 0 : Math.min(MAX_STROKE, Math.max(1, Math.round(num(attr(line, "w"), DEFAULT_LINE_WIDTH) * this.#options.scale)));
242
+ const body = kid(shape, "p:txBody");
243
+ const frame = this.#frame(transform, visit.isDecor);
244
+ if (body !== void 0 && all(body, "a:t").some((node) => textOf(node).trim() !== "")) {
245
+ const type = placeholder === void 0 ? null : attr(placeholder, "type") ?? "body";
246
+ const style = this.#textStyle(visit, chain, type, body);
247
+ const levels = [...chain.map(levelOf), masterLevel(visit.parts, type)].filter(isDefined);
248
+ const html = this.#textHtml(body, style, type !== null && BULLET_TYPES.includes(type) && bulletsByDefault(levels), palette);
249
+ if (placeholder === void 0 && kind !== "line" && (kind !== "rect" || stroke !== null)) return {
250
+ ...frame,
251
+ type: "shape",
252
+ shape: kind,
253
+ fill,
254
+ stroke,
255
+ strokeWidth,
256
+ text: {
257
+ html,
258
+ style
259
+ }
260
+ };
261
+ return {
262
+ ...chain.map((link) => attr(kid(kid(link, "p:txBody"), "a:bodyPr"), "wrap")).find(isDefined) === "none" ? this.#unwrapped(frame, html, style) : frame,
263
+ type: "text",
264
+ html,
265
+ fill,
266
+ style
267
+ };
268
+ }
269
+ if (fill === null && stroke === null) return null;
270
+ const geometry = chain.map((link) => first(link, "a:custGeom")).find(isDefined);
271
+ const outline = geometry === void 0 ? null : outlineOf(geometry, kid(props[0], "a:xfrm"));
272
+ return {
273
+ ...frame,
274
+ type: "shape",
275
+ shape: line !== void 0 && first(line, "a:tailEnd") !== void 0 && kind === "line" ? "arrow" : outline?.kind ?? kind,
276
+ fill: kind === "line" ? null : fill,
277
+ stroke,
278
+ strokeWidth,
279
+ ...outline?.kind === "polygon" ? { points: outline.points } : {}
280
+ };
281
+ }
282
+ async #pictureInto(visit, node, group) {
283
+ if (visit.isDecor && first(node, "p:ph") !== void 0) return;
284
+ const embed = attr(first(node, "a:blip"), "r:embed");
285
+ const transform = this.#transformOf(kid(node, "p:spPr"), group);
286
+ const target = embed === void 0 ? void 0 : visit.part.relations[embed]?.target;
287
+ if (transform === null || target === void 0) return;
288
+ const fileId = await this.#options.image(target);
289
+ if (fileId === null) return;
290
+ const image = {
291
+ ...this.#frame(transform, visit.isDecor),
292
+ type: "image",
293
+ fileId,
294
+ fit: "cover",
295
+ radius: 0,
296
+ alt: attr(first(node, "p:cNvPr"), "descr") ?? "",
297
+ credit: null
298
+ };
299
+ visit.elements.push(image);
300
+ }
301
+ #tableInto(visit, frame, group) {
302
+ const table = first(frame, "a:tbl");
303
+ const transform = this.#transformOf(frame, group);
304
+ if (table === void 0 || transform === null) return;
305
+ const rows = all(table, "a:tr").slice(0, MAX_TABLE_ROWS).map((row) => all(row, "a:tc").slice(0, MAX_TABLE_COLUMNS).map((cell) => all(cell, "a:t").map(textOf).join("").slice(0, MAX_CELL_TEXT)));
306
+ if (rows.length === 0) return;
307
+ const element = {
308
+ ...this.#frame(transform, visit.isDecor),
309
+ type: "table",
310
+ rows,
311
+ hasHeader: attr(first(table, "a:tblPr"), "firstRow") === "1",
312
+ fontSize: Math.max(12, Math.round(Math.min(40, transform.h / Math.max(rows.length, 1) * .4))),
313
+ headerFill: this.#options.theme.accent,
314
+ borderColor: this.#options.theme.muted
315
+ };
316
+ visit.elements.push(element);
317
+ }
318
+ async #background(parts) {
319
+ for (const part of [
320
+ parts.slide,
321
+ parts.layout,
322
+ parts.master
323
+ ]) {
324
+ const background = part === null ? void 0 : first(part.root, "p:bg");
325
+ if (part !== null && background !== void 0) return this.#backgroundOf(background, part);
326
+ }
327
+ return null;
328
+ }
329
+ async #backgroundOf(background, part) {
330
+ const props = kid(background, "p:bgPr");
331
+ const embed = attr(props === void 0 ? void 0 : first(props, "a:blip"), "r:embed");
332
+ const target = embed === void 0 ? void 0 : part.relations[embed]?.target;
333
+ const fileId = target === void 0 ? null : await this.#options.image(target);
334
+ if (fileId !== null) return {
335
+ type: "image",
336
+ fileId,
337
+ fit: "cover",
338
+ dim: 0
339
+ };
340
+ const color = props === void 0 ? part.palette.color(kid(background, "p:bgRef")) : part.palette.fill(props);
341
+ return color === null ? null : {
342
+ type: "color",
343
+ color
344
+ };
345
+ }
346
+ };
347
+ //#endregion
348
+ export { SlideReader as default };
@@ -0,0 +1,19 @@
1
+ import { XmlDocument, XmlElement } from "./pptx-xml.js";
2
+ import { DeckTheme, FontFamily } from "@pollium/vyva-core/content";
3
+ //#region src/import/pptx-theme.d.ts
4
+ export declare const familyOf: (name: string | null) => FontFamily | null;
5
+ export interface PptxTheme {
6
+ colors: Record<string, string>;
7
+ bodyFont: string | null;
8
+ }
9
+ export declare const themeOf: (document: XmlDocument | undefined) => PptxTheme;
10
+ export default class Palette {
11
+ #private;
12
+ constructor(theme: PptxTheme, colorMap: XmlElement | undefined);
13
+ color(root: XmlElement | undefined): string | null;
14
+ hasFill(props: XmlElement | undefined): boolean;
15
+ fill(props: XmlElement | undefined): string | null;
16
+ deckTheme(): DeckTheme;
17
+ }
18
+ export declare const colorMapOf: (master: XmlElement | undefined) => XmlElement | undefined;
19
+ //#endregion
@@ -0,0 +1,160 @@
1
+ import { attr, first, kid, kids, num, tags } from "./pptx-xml.js";
2
+ import { FONT_FAMILIES } from "@pollium/vyva-core/content";
3
+ import { DomUtils } from "htmlparser2";
4
+ import { DECK_THEME } from "@pollium/vyva-core/themes";
5
+ //#region src/import/pptx-theme.ts
6
+ const PERCENT = 1e5;
7
+ const HEX = /^[0-9a-f]{6}$/i;
8
+ const COLOR_TAGS = /* @__PURE__ */ new Set([
9
+ "a:srgbClr",
10
+ "a:sysClr",
11
+ "a:schemeClr"
12
+ ]);
13
+ const FILL_TAGS = /* @__PURE__ */ new Set([
14
+ "a:solidFill",
15
+ "a:gradFill",
16
+ "a:noFill",
17
+ "a:blipFill",
18
+ "a:pattFill",
19
+ "a:grpFill"
20
+ ]);
21
+ const DEFAULT_COLOR_MAP = {
22
+ bg1: "lt1",
23
+ tx1: "dk1",
24
+ bg2: "lt2",
25
+ tx2: "dk2"
26
+ };
27
+ const FONT_SUBSTITUTES = {
28
+ arial: "Inter",
29
+ helvetica: "Inter",
30
+ "segoe ui": "Inter",
31
+ calibri: "Open Sans",
32
+ "calibri light": "Open Sans",
33
+ verdana: "Open Sans",
34
+ tahoma: "Open Sans",
35
+ "century gothic": "Montserrat",
36
+ "gill sans": "Montserrat",
37
+ "gill sans mt": "Montserrat",
38
+ georgia: "Merriweather",
39
+ cambria: "Lora",
40
+ "times new roman": "Lora",
41
+ garamond: "Lora",
42
+ "courier new": "JetBrains Mono",
43
+ consolas: "JetBrains Mono"
44
+ };
45
+ const familyOf = (name) => {
46
+ if (name === null) return null;
47
+ return FONT_FAMILIES.find((font) => font.toLowerCase() === name.toLowerCase()) ?? FONT_SUBSTITUTES[name.toLowerCase()] ?? null;
48
+ };
49
+ const rgbOf = (hex) => {
50
+ const channel = (at) => Number.parseInt(hex.slice(at, at + 2), 16) / 255;
51
+ return [
52
+ channel(0),
53
+ channel(2),
54
+ channel(4)
55
+ ];
56
+ };
57
+ const hexOf = (rgb) => `#${rgb.map((channel) => Math.round(Math.min(1, Math.max(0, channel)) * 255).toString(16).padStart(2, "0")).join("")}`;
58
+ const hueChannel = (p, q, at) => {
59
+ const t = at < 0 ? at + 1 : at > 1 ? at - 1 : at;
60
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
61
+ if (t < 1 / 2) return q;
62
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
63
+ return p;
64
+ };
65
+ const withLuminance = (hex, mod, off) => {
66
+ const [r, g, b] = rgbOf(hex);
67
+ const max = Math.max(r, g, b);
68
+ const min = Math.min(r, g, b);
69
+ const delta = max - min;
70
+ const lightness = Math.min(1, Math.max(0, (max + min) / 2 * mod + off));
71
+ if (delta === 0) return hexOf([
72
+ lightness,
73
+ lightness,
74
+ lightness
75
+ ]);
76
+ const saturation = (max + min) / 2 > .5 ? delta / (2 - max - min) : delta / (max + min);
77
+ const hue = (max === r ? (g - b) / delta + (g < b ? 6 : 0) : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4) / 6;
78
+ const q = lightness < .5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
79
+ const p = 2 * lightness - q;
80
+ return hexOf([
81
+ hueChannel(p, q, hue + 1 / 3),
82
+ hueChannel(p, q, hue),
83
+ hueChannel(p, q, hue - 1 / 3)
84
+ ]);
85
+ };
86
+ const literalOf = (color) => {
87
+ const value = color?.name === "a:sysClr" ? attr(color, "lastClr") : color?.name === "a:srgbClr" ? attr(color, "val") : void 0;
88
+ return value !== void 0 && HEX.test(value) ? value.toLowerCase() : null;
89
+ };
90
+ const fontName = (document, scheme) => {
91
+ const font = first(document, scheme);
92
+ return attr(font === void 0 ? void 0 : kid(font, "a:latin"), "typeface") ?? null;
93
+ };
94
+ const themeOf = (document) => {
95
+ if (document === void 0) return {
96
+ colors: {},
97
+ bodyFont: null
98
+ };
99
+ const scheme = first(document, "a:clrScheme");
100
+ return {
101
+ colors: scheme === void 0 ? {} : Object.fromEntries(tags(scheme).flatMap((slot) => {
102
+ const value = literalOf(tags(slot)[0]);
103
+ return value === null ? [] : [[slot.name.replace(/^a:/, ""), value]];
104
+ })),
105
+ bodyFont: fontName(document, "a:minorFont")
106
+ };
107
+ };
108
+ var Palette = class {
109
+ #theme;
110
+ #colorMap;
111
+ constructor(theme, colorMap) {
112
+ this.#theme = theme;
113
+ this.#colorMap = {
114
+ ...DEFAULT_COLOR_MAP,
115
+ ...colorMap?.attribs
116
+ };
117
+ }
118
+ color(root) {
119
+ if (root === void 0) return null;
120
+ const node = COLOR_TAGS.has(root.name) ? root : DomUtils.findOne((element) => COLOR_TAGS.has(element.name), root.children, true);
121
+ if (node === null) return null;
122
+ const base = node.name === "a:schemeClr" ? this.#scheme(attr(node, "val") ?? "") : literalOf(node);
123
+ if (base === null) return null;
124
+ const mod = kid(node, "a:lumMod");
125
+ const off = kid(node, "a:lumOff");
126
+ if (mod === void 0 && off === void 0) return `#${base}`;
127
+ return withLuminance(base, num(attr(mod, "val"), PERCENT) / PERCENT, num(attr(off, "val")) / PERCENT);
128
+ }
129
+ hasFill(props) {
130
+ return props !== void 0 && tags(props).some((node) => FILL_TAGS.has(node.name));
131
+ }
132
+ fill(props) {
133
+ if (props === void 0) return null;
134
+ const solid = kid(props, "a:solidFill");
135
+ if (solid !== void 0) return this.color(solid);
136
+ const gradient = kid(props, "a:gradFill");
137
+ const stop = gradient === void 0 ? void 0 : first(gradient, "a:gs");
138
+ return stop === void 0 ? null : this.color(stop);
139
+ }
140
+ deckTheme() {
141
+ const fallback = DECK_THEME;
142
+ const slot = (name) => {
143
+ const value = this.#scheme(name);
144
+ return value === null ? null : `#${value}`;
145
+ };
146
+ return {
147
+ font: familyOf(this.#theme.bodyFont) ?? fallback.font,
148
+ background: slot("bg1") ?? fallback.background,
149
+ text: slot("tx1") ?? fallback.text,
150
+ accent: slot("accent1") ?? fallback.accent,
151
+ muted: slot("tx2") ?? fallback.muted
152
+ };
153
+ }
154
+ #scheme(name) {
155
+ return this.#theme.colors[this.#colorMap[name] ?? name] ?? null;
156
+ }
157
+ };
158
+ const colorMapOf = (master) => master === void 0 ? void 0 : kids(master, "p:clrMap")[0];
159
+ //#endregion
160
+ export { colorMapOf, Palette as default, familyOf, themeOf };
@@ -0,0 +1,23 @@
1
+ import { DomUtils, parseDocument } from "htmlparser2";
2
+ //#region src/import/pptx-xml.d.ts
3
+ export type XmlElement = Parameters<typeof DomUtils.getAttributeValue>[0];
4
+ export type XmlDocument = ReturnType<typeof parseDocument>;
5
+ type XmlRoot = XmlElement | XmlDocument;
6
+ export type PackageFiles = Record<string, Uint8Array>;
7
+ export declare const xml: (text: string) => XmlDocument;
8
+ export declare const all: (root: XmlRoot, name: string) => XmlElement[];
9
+ export declare const first: (root: XmlRoot, name: string) => XmlElement | undefined;
10
+ export declare const tags: (root: XmlRoot) => XmlElement[];
11
+ export declare const kids: (root: XmlElement, name: string) => XmlElement[];
12
+ export declare const kid: (root: XmlElement | undefined, name: string) => XmlElement | undefined;
13
+ export declare const attr: (element: XmlElement | undefined, name: string) => string | undefined;
14
+ export declare const textOf: (element: XmlElement) => string;
15
+ export declare const num: (value: string | undefined, fallback?: number) => number;
16
+ export interface Relation {
17
+ target: string;
18
+ type: string;
19
+ }
20
+ export type Relations = Record<string, Relation>;
21
+ export declare const relationsOf: (files: PackageFiles, part: string) => Relations;
22
+ export declare const relatedPart: (relations: Relations, type: string) => string | null;
23
+ //#endregion
@@ -0,0 +1,27 @@
1
+ import { DomUtils, parseDocument } from "htmlparser2";
2
+ import { strFromU8 } from "fflate";
3
+ import { posix } from "node:path";
4
+ //#region src/import/pptx-xml.ts
5
+ const xml = (text) => parseDocument(text, { xmlMode: true });
6
+ const all = (root, name) => DomUtils.getElementsByTagName(name, root, true);
7
+ const first = (root, name) => all(root, name)[0];
8
+ const tags = (root) => root.children.filter((node) => DomUtils.isTag(node));
9
+ const kids = (root, name) => tags(root).filter((node) => node.name === name);
10
+ const kid = (root, name) => root === void 0 ? void 0 : kids(root, name)[0];
11
+ const attr = (element, name) => element?.attribs[name];
12
+ const textOf = (element) => DomUtils.textContent(element);
13
+ const num = (value, fallback = 0) => {
14
+ const parsed = Number(value);
15
+ return Number.isFinite(parsed) ? parsed : fallback;
16
+ };
17
+ const relationsOf = (files, part) => {
18
+ const data = files[posix.join(posix.dirname(part), "_rels", `${posix.basename(part)}.rels`)];
19
+ if (data === void 0) return {};
20
+ return Object.fromEntries(all(xml(strFromU8(data)), "Relationship").map((relation) => [attr(relation, "Id") ?? "", {
21
+ target: posix.normalize(posix.join(posix.dirname(part), attr(relation, "Target") ?? "")),
22
+ type: attr(relation, "Type") ?? ""
23
+ }]));
24
+ };
25
+ const relatedPart = (relations, type) => Object.values(relations).find((relation) => relation.type.endsWith(`/${type}`))?.target ?? null;
26
+ //#endregion
27
+ export { all, attr, first, kid, kids, num, relatedPart, relationsOf, tags, textOf, xml };
@@ -0,0 +1,9 @@
1
+ import { SaveImage } from "./images.js";
2
+ import { DeckContent } from "@pollium/vyva-core/content";
3
+ //#region src/import/pptx.d.ts
4
+ export interface ImportedDeck {
5
+ title: string;
6
+ content: DeckContent;
7
+ }
8
+ export declare const importPptx: (buffer: Buffer, filename: string, saveImage: SaveImage) => Promise<ImportedDeck>;
9
+ //#endregion