@pollium/vyva-convert 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +88 -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 +58 -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 +310 -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/pptx-package.d.ts +22 -0
  39. package/dist/import/pptx-package.js +61 -0
  40. package/dist/import/pptx-placeholders.d.ts +11 -0
  41. package/dist/import/pptx-placeholders.js +44 -0
  42. package/dist/import/pptx-slide.d.ts +19 -0
  43. package/dist/import/pptx-slide.js +324 -0
  44. package/dist/import/pptx-theme.d.ts +18 -0
  45. package/dist/import/pptx-theme.js +161 -0
  46. package/dist/import/pptx-xml.d.ts +23 -0
  47. package/dist/import/pptx-xml.js +27 -0
  48. package/dist/import/pptx.d.ts +9 -0
  49. package/dist/import/pptx.js +112 -0
  50. package/dist/import/titles.d.ts +3 -0
  51. package/dist/import/titles.js +6 -0
  52. package/dist/import.d.ts +14 -0
  53. package/dist/import.js +66 -0
  54. package/dist/text-measure.d.ts +4 -0
  55. package/dist/text-measure.js +98 -0
  56. package/package.json +49 -3
@@ -0,0 +1,324 @@
1
+ import { cleanHtml } from "../clean-html.js";
2
+ import { all, attr, first, kid, kids, num, tags, textOf } from "./pptx-xml.js";
3
+ import { TITLE_TYPES, bulletsByDefault, decorOf, isDefined, levelOf, masterLevel, placeholderChain } from "./pptx-placeholders.js";
4
+ import { MAX_CELL_TEXT, MAX_TABLE_COLUMNS, MAX_TABLE_ROWS } from "@pollium/vyva-core/content";
5
+ import { newId } from "@pollium/vyva-core/ids";
6
+ import { escapeHtml } from "@pollium/vyva-core/escape";
7
+ //#region src/import/pptx-slide.ts
8
+ const MAX_ELEMENTS = 300;
9
+ const ANGLE_UNIT = 6e4;
10
+ const DEFAULT_LINE_WIDTH = 12700;
11
+ const MAX_STROKE = 80;
12
+ const PX_PER_POINT = 2;
13
+ const TITLE_SIZE = 64;
14
+ const BODY_SIZE = 32;
15
+ const BULLET_TYPES = ["body", "obj"];
16
+ const ALIGNS = {
17
+ l: "left",
18
+ ctr: "center",
19
+ r: "right",
20
+ just: "justify"
21
+ };
22
+ const IDENTITY = {
23
+ offX: 0,
24
+ offY: 0,
25
+ chOffX: 0,
26
+ chOffY: 0,
27
+ scaleX: 1,
28
+ scaleY: 1
29
+ };
30
+ const NO_LOOK = {
31
+ size: null,
32
+ color: null,
33
+ isBold: null,
34
+ align: null
35
+ };
36
+ const mergeLooks = (looks) => looks.reduce((merged, look) => ({
37
+ size: merged.size ?? look.size,
38
+ color: merged.color ?? look.color,
39
+ isBold: merged.isBold ?? look.isBold,
40
+ align: merged.align ?? look.align
41
+ }), NO_LOOK);
42
+ const innerGroup = (node, group) => {
43
+ const xfrm = first(kid(node, "p:grpSpPr") ?? node, "a:xfrm");
44
+ const off = kid(xfrm, "a:off");
45
+ const ext = kid(xfrm, "a:ext");
46
+ const childOff = kid(xfrm, "a:chOff");
47
+ const childExt = kid(xfrm, "a:chExt");
48
+ return {
49
+ offX: group.offX + (num(attr(off, "x")) - group.chOffX) * group.scaleX,
50
+ offY: group.offY + (num(attr(off, "y")) - group.chOffY) * group.scaleY,
51
+ chOffX: num(attr(childOff, "x")),
52
+ chOffY: num(attr(childOff, "y")),
53
+ scaleX: group.scaleX * num(attr(ext, "cx"), 1) / Math.max(1, num(attr(childExt, "cx"), 1)),
54
+ scaleY: group.scaleY * num(attr(ext, "cy"), 1) / Math.max(1, num(attr(childExt, "cy"), 1))
55
+ };
56
+ };
57
+ const shapeKindOf = (geometry) => geometry === "roundRect" ? "rounded" : geometry === "ellipse" ? "ellipse" : geometry === "triangle" ? "triangle" : geometry === "line" || geometry === "straightConnector1" ? "line" : "rect";
58
+ const styleColor = (shape, reference, palette) => {
59
+ const ref = kid(kid(shape, "p:style"), reference);
60
+ return ref === void 0 || attr(ref, "idx") === "0" ? null : palette.color(ref);
61
+ };
62
+ var SlideReader = class {
63
+ #options;
64
+ constructor(options) {
65
+ this.#options = options;
66
+ }
67
+ async read(parts) {
68
+ const elements = [];
69
+ const titles = [];
70
+ for (const part of decorOf(parts)) if (part.tree !== void 0) await this.#visit({
71
+ part,
72
+ parts,
73
+ isDecor: true,
74
+ elements,
75
+ titles
76
+ }, part.tree, IDENTITY);
77
+ if (parts.slide.tree !== void 0) await this.#visit({
78
+ part: parts.slide,
79
+ parts,
80
+ isDecor: false,
81
+ elements,
82
+ titles
83
+ }, parts.slide.tree, IDENTITY);
84
+ return {
85
+ slide: {
86
+ id: newId(),
87
+ background: await this.#background(parts),
88
+ elements: elements.slice(0, MAX_ELEMENTS)
89
+ },
90
+ title: titles[0] ?? ""
91
+ };
92
+ }
93
+ async #visit(visit, container, group) {
94
+ for (const node of tags(container)) if (node.name === "p:sp") this.#shapeInto(visit, node, group);
95
+ else if (node.name === "p:pic") await this.#pictureInto(visit, node, group);
96
+ else if (node.name === "p:graphicFrame") this.#tableInto(visit, node, group);
97
+ else if (node.name === "p:grpSp") await this.#visit(visit, node, innerGroup(node, group));
98
+ }
99
+ #transformOf(shapeProps, group) {
100
+ const xfrm = kid(shapeProps, "a:xfrm") ?? kid(shapeProps, "p:xfrm");
101
+ const off = kid(xfrm, "a:off");
102
+ const ext = kid(xfrm, "a:ext");
103
+ if (off === void 0 || ext === void 0) return null;
104
+ const { scale } = this.#options;
105
+ const x = group.offX + (num(attr(off, "x")) - group.chOffX) * group.scaleX;
106
+ const y = group.offY + (num(attr(off, "y")) - group.chOffY) * group.scaleY;
107
+ return {
108
+ x: Math.round(x * scale),
109
+ y: Math.round(y * scale),
110
+ w: Math.max(1, Math.round(num(attr(ext, "cx")) * group.scaleX * scale)),
111
+ h: Math.max(1, Math.round(num(attr(ext, "cy")) * group.scaleY * scale)),
112
+ rotation: Math.round(num(attr(xfrm, "rot")) / ANGLE_UNIT) % 360
113
+ };
114
+ }
115
+ #frame(transform, isLocked) {
116
+ return {
117
+ id: newId(),
118
+ ...transform,
119
+ opacity: 1,
120
+ isLocked
121
+ };
122
+ }
123
+ #pixels(size) {
124
+ const hundredths = num(size);
125
+ return hundredths > 0 ? Math.round(hundredths / 100 * PX_PER_POINT * this.#options.fontScale) : null;
126
+ }
127
+ #runLook(props, palette) {
128
+ const bold = attr(props, "b");
129
+ return {
130
+ size: this.#pixels(attr(props, "sz")),
131
+ color: palette.color(kid(props, "a:solidFill")),
132
+ isBold: bold === void 0 ? null : bold === "1"
133
+ };
134
+ }
135
+ #levelLook(level, palette) {
136
+ return {
137
+ ...this.#runLook(kid(level, "a:defRPr"), palette),
138
+ align: ALIGNS[attr(level, "algn") ?? ""] ?? null
139
+ };
140
+ }
141
+ #runHtml(run, box, palette) {
142
+ const props = kid(run, "a:rPr");
143
+ const text = escapeHtml(all(run, "a:t").map(textOf).join(""));
144
+ if (text === "") return "";
145
+ const look = this.#runLook(props, palette);
146
+ 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`]];
147
+ let html = styles.length === 0 ? text : `<span style="${styles.join(";")}">${text}</span>`;
148
+ if (look.isBold === true) html = `<strong>${html}</strong>`;
149
+ if (attr(props, "i") === "1") html = `<em>${html}</em>`;
150
+ if (attr(props, "u") !== void 0 && attr(props, "u") !== "none") html = `<u>${html}</u>`;
151
+ return html;
152
+ }
153
+ #textHtml(body, box, hasBullets, palette) {
154
+ const parts = [];
155
+ let isListOpen = false;
156
+ for (const paragraph of kids(body, "a:p")) {
157
+ const props = kid(paragraph, "a:pPr");
158
+ const own = ALIGNS[attr(props, "algn") ?? ""];
159
+ 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("");
160
+ 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 !== "") {
161
+ if (!isListOpen) parts.push("<ul>");
162
+ isListOpen = true;
163
+ parts.push(`<li>${inner}</li>`);
164
+ continue;
165
+ }
166
+ if (isListOpen) parts.push("</ul>");
167
+ isListOpen = false;
168
+ parts.push(`<p${own === void 0 || own === box.align ? "" : ` style="text-align:${own}"`}>${inner}</p>`);
169
+ }
170
+ if (isListOpen) parts.push("</ul>");
171
+ return cleanHtml(parts.join(""));
172
+ }
173
+ #textStyle(visit, chain, type, body) {
174
+ const palette = visit.part.palette;
175
+ const firstRun = all(body, "a:r").map((run) => kid(run, "a:rPr")).find(isDefined);
176
+ const firstAlign = ALIGNS[attr(kid(kids(body, "a:p")[0], "a:pPr"), "algn") ?? ""] ?? null;
177
+ const look = mergeLooks([
178
+ {
179
+ ...this.#runLook(firstRun, palette),
180
+ align: firstAlign
181
+ },
182
+ ...chain.map((shape) => this.#levelLook(levelOf(shape), palette)),
183
+ this.#levelLook(masterLevel(visit.parts, type), palette),
184
+ {
185
+ ...NO_LOOK,
186
+ color: styleColor(chain[0], "a:fontRef", palette)
187
+ }
188
+ ]);
189
+ const isTitle = TITLE_TYPES.includes(type ?? "");
190
+ const anchor = chain.map((shape) => attr(kid(kid(shape, "p:txBody"), "a:bodyPr"), "anchor")).find(isDefined);
191
+ const { theme } = this.#options;
192
+ return {
193
+ fontSize: look.size ?? (isTitle ? TITLE_SIZE : BODY_SIZE),
194
+ color: look.color ?? theme.text,
195
+ align: look.align ?? "left",
196
+ verticalAlign: anchor === "ctr" ? "middle" : anchor === "b" ? "bottom" : "top",
197
+ lineHeight: 1.2,
198
+ isBold: look.isBold ?? false
199
+ };
200
+ }
201
+ #shapeInto(visit, shape, group) {
202
+ const placeholder = first(shape, "p:ph");
203
+ if (placeholder !== void 0 && visit.isDecor) return;
204
+ const element = this.#shape(visit, shape, placeholder, group);
205
+ if (element === null) return;
206
+ visit.elements.push(element);
207
+ if (element.type === "text" && TITLE_TYPES.includes(attr(placeholder, "type") ?? "")) visit.titles.push(element.html.replace(/<[^>]+>/g, "").trim());
208
+ }
209
+ #shape(visit, shape, placeholder, group) {
210
+ const chain = placeholder === void 0 ? [shape] : placeholderChain(shape, placeholder, visit.parts);
211
+ const props = chain.map((link) => kid(link, "p:spPr"));
212
+ const transform = this.#transformOf(props[0], group) ?? props.slice(1).map((inherited) => this.#transformOf(inherited, IDENTITY)).find((found) => found !== null) ?? null;
213
+ if (transform === null) return null;
214
+ const { palette } = visit.part;
215
+ const kind = shapeKindOf(attr(chain.map((link) => first(link, "a:prstGeom")).find(isDefined), "prst") ?? "rect");
216
+ const filled = props.find((candidate) => palette.hasFill(candidate));
217
+ const fill = filled === void 0 ? styleColor(shape, "a:fillRef", palette) : palette.fill(filled);
218
+ const line = kid(props[0], "a:ln");
219
+ const stroke = line !== void 0 && palette.hasFill(line) ? palette.fill(line) : styleColor(shape, "a:lnRef", palette);
220
+ const strokeWidth = stroke === null ? 0 : Math.min(MAX_STROKE, Math.max(1, Math.round(num(attr(line, "w"), DEFAULT_LINE_WIDTH) * this.#options.scale)));
221
+ const body = kid(shape, "p:txBody");
222
+ const frame = this.#frame(transform, visit.isDecor);
223
+ if (body !== void 0 && all(body, "a:t").some((node) => textOf(node).trim() !== "")) {
224
+ const type = placeholder === void 0 ? null : attr(placeholder, "type") ?? "body";
225
+ const style = this.#textStyle(visit, chain, type, body);
226
+ const levels = [...chain.map(levelOf), masterLevel(visit.parts, type)].filter(isDefined);
227
+ const html = this.#textHtml(body, style, type !== null && BULLET_TYPES.includes(type) && bulletsByDefault(levels), palette);
228
+ if (placeholder === void 0 && kind !== "line" && (kind !== "rect" || stroke !== null)) return {
229
+ ...frame,
230
+ type: "shape",
231
+ shape: kind,
232
+ fill,
233
+ stroke,
234
+ strokeWidth,
235
+ text: {
236
+ html,
237
+ style
238
+ }
239
+ };
240
+ return {
241
+ ...frame,
242
+ type: "text",
243
+ html,
244
+ fill,
245
+ style
246
+ };
247
+ }
248
+ if (fill === null && stroke === null) return null;
249
+ return {
250
+ ...frame,
251
+ type: "shape",
252
+ shape: line !== void 0 && first(line, "a:tailEnd") !== void 0 && kind === "line" ? "arrow" : kind,
253
+ fill: kind === "line" ? null : fill,
254
+ stroke,
255
+ strokeWidth
256
+ };
257
+ }
258
+ async #pictureInto(visit, node, group) {
259
+ if (visit.isDecor && first(node, "p:ph") !== void 0) return;
260
+ const embed = attr(first(node, "a:blip"), "r:embed");
261
+ const transform = this.#transformOf(kid(node, "p:spPr"), group);
262
+ const target = embed === void 0 ? void 0 : visit.part.relations[embed]?.target;
263
+ if (transform === null || target === void 0) return;
264
+ const fileId = await this.#options.image(target);
265
+ if (fileId === null) return;
266
+ const image = {
267
+ ...this.#frame(transform, visit.isDecor),
268
+ type: "image",
269
+ fileId,
270
+ fit: "cover",
271
+ radius: 0,
272
+ alt: attr(first(node, "p:cNvPr"), "descr") ?? "",
273
+ credit: null
274
+ };
275
+ visit.elements.push(image);
276
+ }
277
+ #tableInto(visit, frame, group) {
278
+ const table = first(frame, "a:tbl");
279
+ const transform = this.#transformOf(frame, group);
280
+ if (table === void 0 || transform === null) return;
281
+ 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)));
282
+ if (rows.length === 0) return;
283
+ const element = {
284
+ ...this.#frame(transform, visit.isDecor),
285
+ type: "table",
286
+ rows,
287
+ hasHeader: attr(first(table, "a:tblPr"), "firstRow") === "1",
288
+ fontSize: Math.max(12, Math.round(Math.min(40, transform.h / Math.max(rows.length, 1) * .4))),
289
+ headerFill: this.#options.theme.accent,
290
+ borderColor: this.#options.theme.muted
291
+ };
292
+ visit.elements.push(element);
293
+ }
294
+ async #background(parts) {
295
+ for (const part of [
296
+ parts.slide,
297
+ parts.layout,
298
+ parts.master
299
+ ]) {
300
+ const background = part === null ? void 0 : first(part.root, "p:bg");
301
+ if (part !== null && background !== void 0) return this.#backgroundOf(background, part);
302
+ }
303
+ return null;
304
+ }
305
+ async #backgroundOf(background, part) {
306
+ const props = kid(background, "p:bgPr");
307
+ const embed = attr(props === void 0 ? void 0 : first(props, "a:blip"), "r:embed");
308
+ const target = embed === void 0 ? void 0 : part.relations[embed]?.target;
309
+ const fileId = target === void 0 ? null : await this.#options.image(target);
310
+ if (fileId !== null) return {
311
+ type: "image",
312
+ fileId,
313
+ fit: "cover",
314
+ dim: 0
315
+ };
316
+ const color = props === void 0 ? part.palette.color(kid(background, "p:bgRef")) : part.palette.fill(props);
317
+ return color === null ? null : {
318
+ type: "color",
319
+ color
320
+ };
321
+ }
322
+ };
323
+ //#endregion
324
+ export { SlideReader as default };
@@ -0,0 +1,18 @@
1
+ import { XmlDocument, XmlElement } from "./pptx-xml.js";
2
+ import { DeckTheme } from "@pollium/vyva-core/content";
3
+ //#region src/import/pptx-theme.d.ts
4
+ export interface PptxTheme {
5
+ colors: Record<string, string>;
6
+ bodyFont: string | null;
7
+ }
8
+ export declare const themeOf: (document: XmlDocument | undefined) => PptxTheme;
9
+ export default class Palette {
10
+ #private;
11
+ constructor(theme: PptxTheme, colorMap: XmlElement | undefined);
12
+ color(root: XmlElement | undefined): string | null;
13
+ hasFill(props: XmlElement | undefined): boolean;
14
+ fill(props: XmlElement | undefined): string | null;
15
+ deckTheme(): DeckTheme;
16
+ }
17
+ export declare const colorMapOf: (master: XmlElement | undefined) => XmlElement | undefined;
18
+ //#endregion
@@ -0,0 +1,161 @@
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 rgbOf = (hex) => {
46
+ const channel = (at) => Number.parseInt(hex.slice(at, at + 2), 16) / 255;
47
+ return [
48
+ channel(0),
49
+ channel(2),
50
+ channel(4)
51
+ ];
52
+ };
53
+ const hexOf = (rgb) => `#${rgb.map((channel) => Math.round(Math.min(1, Math.max(0, channel)) * 255).toString(16).padStart(2, "0")).join("")}`;
54
+ const hueChannel = (p, q, at) => {
55
+ const t = at < 0 ? at + 1 : at > 1 ? at - 1 : at;
56
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
57
+ if (t < 1 / 2) return q;
58
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
59
+ return p;
60
+ };
61
+ const withLuminance = (hex, mod, off) => {
62
+ const [r, g, b] = rgbOf(hex);
63
+ const max = Math.max(r, g, b);
64
+ const min = Math.min(r, g, b);
65
+ const delta = max - min;
66
+ const lightness = Math.min(1, Math.max(0, (max + min) / 2 * mod + off));
67
+ if (delta === 0) return hexOf([
68
+ lightness,
69
+ lightness,
70
+ lightness
71
+ ]);
72
+ const saturation = (max + min) / 2 > .5 ? delta / (2 - max - min) : delta / (max + min);
73
+ const hue = (max === r ? (g - b) / delta + (g < b ? 6 : 0) : max === g ? (b - r) / delta + 2 : (r - g) / delta + 4) / 6;
74
+ const q = lightness < .5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation;
75
+ const p = 2 * lightness - q;
76
+ return hexOf([
77
+ hueChannel(p, q, hue + 1 / 3),
78
+ hueChannel(p, q, hue),
79
+ hueChannel(p, q, hue - 1 / 3)
80
+ ]);
81
+ };
82
+ const literalOf = (color) => {
83
+ const value = color?.name === "a:sysClr" ? attr(color, "lastClr") : color?.name === "a:srgbClr" ? attr(color, "val") : void 0;
84
+ return value !== void 0 && HEX.test(value) ? value.toLowerCase() : null;
85
+ };
86
+ const fontName = (document, scheme) => {
87
+ const font = first(document, scheme);
88
+ return attr(font === void 0 ? void 0 : kid(font, "a:latin"), "typeface") ?? null;
89
+ };
90
+ const themeOf = (document) => {
91
+ if (document === void 0) return {
92
+ colors: {},
93
+ bodyFont: null
94
+ };
95
+ const scheme = first(document, "a:clrScheme");
96
+ return {
97
+ colors: scheme === void 0 ? {} : Object.fromEntries(tags(scheme).flatMap((slot) => {
98
+ const value = literalOf(tags(slot)[0]);
99
+ return value === null ? [] : [[slot.name.replace(/^a:/, ""), value]];
100
+ })),
101
+ bodyFont: fontName(document, "a:minorFont")
102
+ };
103
+ };
104
+ var Palette = class {
105
+ #theme;
106
+ #colorMap;
107
+ constructor(theme, colorMap) {
108
+ this.#theme = theme;
109
+ this.#colorMap = {
110
+ ...DEFAULT_COLOR_MAP,
111
+ ...colorMap?.attribs
112
+ };
113
+ }
114
+ color(root) {
115
+ if (root === void 0) return null;
116
+ const node = COLOR_TAGS.has(root.name) ? root : DomUtils.findOne((element) => COLOR_TAGS.has(element.name), root.children, true);
117
+ if (node === null) return null;
118
+ const base = node.name === "a:schemeClr" ? this.#scheme(attr(node, "val") ?? "") : literalOf(node);
119
+ if (base === null) return null;
120
+ const mod = kid(node, "a:lumMod");
121
+ const off = kid(node, "a:lumOff");
122
+ if (mod === void 0 && off === void 0) return `#${base}`;
123
+ return withLuminance(base, num(attr(mod, "val"), PERCENT) / PERCENT, num(attr(off, "val")) / PERCENT);
124
+ }
125
+ hasFill(props) {
126
+ return props !== void 0 && tags(props).some((node) => FILL_TAGS.has(node.name));
127
+ }
128
+ fill(props) {
129
+ if (props === void 0) return null;
130
+ const solid = kid(props, "a:solidFill");
131
+ if (solid !== void 0) return this.color(solid);
132
+ const gradient = kid(props, "a:gradFill");
133
+ const stop = gradient === void 0 ? void 0 : first(gradient, "a:gs");
134
+ return stop === void 0 ? null : this.color(stop);
135
+ }
136
+ #font() {
137
+ const name = this.#theme.bodyFont;
138
+ if (name === null) return null;
139
+ return FONT_FAMILIES.find((font) => font.toLowerCase() === name.toLowerCase()) ?? FONT_SUBSTITUTES[name.toLowerCase()] ?? null;
140
+ }
141
+ deckTheme() {
142
+ const fallback = DECK_THEME;
143
+ const slot = (name) => {
144
+ const value = this.#scheme(name);
145
+ return value === null ? null : `#${value}`;
146
+ };
147
+ return {
148
+ font: this.#font() ?? fallback.font,
149
+ background: slot("bg1") ?? fallback.background,
150
+ text: slot("tx1") ?? fallback.text,
151
+ accent: slot("accent1") ?? fallback.accent,
152
+ muted: slot("tx2") ?? fallback.muted
153
+ };
154
+ }
155
+ #scheme(name) {
156
+ return this.#theme.colors[this.#colorMap[name] ?? name] ?? null;
157
+ }
158
+ };
159
+ const colorMapOf = (master) => master === void 0 ? void 0 : kids(master, "p:clrMap")[0];
160
+ //#endregion
161
+ export { colorMapOf, Palette as default, 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
@@ -0,0 +1,112 @@
1
+ import { ConvertError } from "../errors.js";
2
+ import { titleOf } from "./titles.js";
3
+ import { imageSaver } from "./images.js";
4
+ import { all, attr, first, num } from "./pptx-xml.js";
5
+ import PptxPackage from "./pptx-package.js";
6
+ import SlideReader from "./pptx-slide.js";
7
+ import { DECK_SIZES } from "@pollium/vyva-core/content";
8
+ import { unzipSync } from "fflate";
9
+ import { posix } from "node:path";
10
+ //#region src/import/pptx.ts
11
+ const EMU_PER_INCH = 914400;
12
+ const PX_PER_INCH = 144;
13
+ const DEFAULT_WIDTH = 12192e3;
14
+ const DEFAULT_HEIGHT = 6858e3;
15
+ const MAX_SLIDES = 300;
16
+ const MAX_IMAGES = 80;
17
+ const MAX_UNZIPPED_BYTES = 125829120;
18
+ const IMAGE_TYPES = {
19
+ png: "image/png",
20
+ jpg: "image/jpeg",
21
+ jpeg: "image/jpeg",
22
+ gif: "image/gif",
23
+ webp: "image/webp"
24
+ };
25
+ const FORMATS = [
26
+ {
27
+ format: "16:9",
28
+ ratio: 16 / 9
29
+ },
30
+ {
31
+ format: "4:3",
32
+ ratio: 4 / 3
33
+ },
34
+ {
35
+ format: "1:1",
36
+ ratio: 1
37
+ },
38
+ {
39
+ format: "a4-landscape",
40
+ ratio: 1527 / 1080
41
+ },
42
+ {
43
+ format: "a4-portrait",
44
+ ratio: 1080 / 1527
45
+ }
46
+ ];
47
+ const formatFor = (cx, cy) => {
48
+ const ratio = cx / Math.max(cy, 1);
49
+ return FORMATS.reduce((best, candidate) => Math.abs(candidate.ratio - ratio) < Math.abs(best.ratio - ratio) ? candidate : best).format;
50
+ };
51
+ const plainText = (html) => html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
52
+ const deckTitle = (titles, slides, filename) => {
53
+ const biggest = slides[0]?.elements.filter((element) => element.type === "text" && !element.isLocked).sort((a, b) => b.style.fontSize - a.style.fontSize)[0];
54
+ const title = titles.find((candidate) => candidate !== "") ?? (biggest === void 0 ? "" : plainText(biggest.html));
55
+ return titleOf(title, filename);
56
+ };
57
+ const unzipped = (buffer) => {
58
+ let total = 0;
59
+ return unzipSync(buffer, { filter: (file) => {
60
+ total += file.originalSize;
61
+ if (total > MAX_UNZIPPED_BYTES) throw new ConvertError("too-large", `The presentation unzips to more than ${MAX_UNZIPPED_BYTES} bytes`);
62
+ return file.name.startsWith("ppt/") || file.name === "[Content_Types].xml" || file.name.startsWith("docProps/");
63
+ } });
64
+ };
65
+ const importPptx = async (buffer, filename, saveImage) => {
66
+ const files = unzipped(buffer);
67
+ const pptx = new PptxPackage(files);
68
+ const presentation = pptx.document("ppt/presentation.xml");
69
+ if (presentation === void 0) throw new ConvertError("unsupported", `${filename} has no presentation in it`);
70
+ const size = first(presentation, "p:sldSz");
71
+ const cx = num(attr(size, "cx"), DEFAULT_WIDTH);
72
+ const cy = num(attr(size, "cy"), DEFAULT_HEIGHT);
73
+ const format = formatFor(cx, cy);
74
+ const relations = pptx.relationsOf("ppt/presentation.xml");
75
+ const allParts = all(presentation, "p:sldId").map((slide) => relations[attr(slide, "r:id") ?? ""]?.target).filter((path) => path !== void 0).slice(0, MAX_SLIDES).map((path) => pptx.parts(path)).filter((parts) => parts !== null);
76
+ const theme = allParts[0]?.slide.palette.deckTheme();
77
+ if (theme === void 0) throw new ConvertError("empty", `${filename} has no slides`);
78
+ const save = imageSaver(saveImage, filename, MAX_IMAGES);
79
+ const saved = /* @__PURE__ */ new Map();
80
+ const reader = new SlideReader({
81
+ scale: DECK_SIZES[format].width / cx,
82
+ fontScale: DECK_SIZES[format].width / (cx / EMU_PER_INCH * PX_PER_INCH),
83
+ theme,
84
+ image: (path) => {
85
+ const known = saved.get(path);
86
+ if (known !== void 0) return known;
87
+ const data = files[path];
88
+ const contentType = IMAGE_TYPES[posix.extname(path).slice(1).toLowerCase()];
89
+ const fileId = data === void 0 || contentType === void 0 ? Promise.resolve(null) : save(data, contentType);
90
+ saved.set(path, fileId);
91
+ return fileId;
92
+ }
93
+ });
94
+ const slides = [];
95
+ const titles = [];
96
+ for (const parts of allParts) {
97
+ const read = await reader.read(parts);
98
+ slides.push(read.slide);
99
+ titles.push(read.title);
100
+ }
101
+ return {
102
+ title: deckTitle(titles, slides, filename),
103
+ content: {
104
+ kind: "deck",
105
+ format,
106
+ theme,
107
+ slides
108
+ }
109
+ };
110
+ };
111
+ //#endregion
112
+ export { importPptx };
@@ -0,0 +1,3 @@
1
+ //#region src/import/titles.d.ts
2
+ export declare const titleOf: (text: string, filename: string) => string;
3
+ //#endregion