@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,8 @@
1
+ import { PdfEngine } from "./export/pdf.js";
2
+ //#region src/gotenberg.d.ts
3
+ export interface GotenbergOptions {
4
+ url: string;
5
+ timeoutMs?: number;
6
+ }
7
+ export declare const gotenberg: ({ url, timeoutMs }: GotenbergOptions) => PdfEngine;
8
+ //#endregion
@@ -0,0 +1,39 @@
1
+ import { ConvertError } from "./errors.js";
2
+ //#region src/gotenberg.ts
3
+ const DEFAULT_TIMEOUT_MS = 6e4;
4
+ const CONVERT_PATH = "/forms/chromium/convert/html";
5
+ const inches = (value) => String(Number(value.toFixed(3)));
6
+ const formOf = ({ html, files, footerHtml, page }) => {
7
+ const form = new FormData();
8
+ form.append("files", new Blob([html], { type: "text/html" }), "index.html");
9
+ for (const file of files) form.append("files", new Blob([new Uint8Array(file.data)], { type: file.contentType }), file.name);
10
+ if (footerHtml !== null) form.append("files", new Blob([footerHtml], { type: "text/html" }), "footer.html");
11
+ const { margins } = page;
12
+ const sizes = {
13
+ paperWidth: page.width,
14
+ paperHeight: page.height,
15
+ marginTop: margins.top,
16
+ marginBottom: margins.bottom,
17
+ marginLeft: margins.left,
18
+ marginRight: margins.right
19
+ };
20
+ for (const [field, value] of Object.entries(sizes)) form.append(field, inches(value));
21
+ form.append("printBackground", "true");
22
+ form.append("preferCssPageSize", "false");
23
+ form.append("waitDelay", "0.5s");
24
+ return form;
25
+ };
26
+ const gotenberg = ({ url, timeoutMs = DEFAULT_TIMEOUT_MS }) => async (job) => {
27
+ const endpoint = `${url.replace(/\/+$/, "")}${CONVERT_PATH}`;
28
+ const response = await fetch(endpoint, {
29
+ method: "POST",
30
+ body: formOf(job),
31
+ signal: AbortSignal.timeout(timeoutMs)
32
+ }).catch((error) => {
33
+ throw new ConvertError("pdf-failed", `Gotenberg at ${url} could not be reached`, { cause: error });
34
+ });
35
+ if (!response.ok) throw new ConvertError("pdf-failed", `Gotenberg answered ${response.status}: ${await response.text().catch(() => "")}`);
36
+ return new Uint8Array(await response.arrayBuffer());
37
+ };
38
+ //#endregion
39
+ export { gotenberg };
@@ -0,0 +1,12 @@
1
+ import { SaveImage } from "./images.js";
2
+ import { DocumentContent } from "@pollium/vyva-core/content";
3
+ //#region src/import/documents.d.ts
4
+ export interface ImportedDocument {
5
+ title: string;
6
+ content: DocumentContent;
7
+ }
8
+ export declare const importDocx: (buffer: Buffer, filename: string, saveImage: SaveImage) => Promise<ImportedDocument>;
9
+ export declare const importMarkdown: (text: string, filename: string) => ImportedDocument;
10
+ export declare const importPlainText: (text: string, filename: string) => ImportedDocument;
11
+ export declare const importPdf: (buffer: Buffer, filename: string) => Promise<ImportedDocument>;
12
+ //#endregion
@@ -0,0 +1,44 @@
1
+ import { htmlToBlocks, paragraphsHtml, titleFrom } from "./html-to-blocks.js";
2
+ import { imageSaver } from "./images.js";
3
+ import { markdownToBlocks } from "@pollium/vyva-core/markdown-blocks";
4
+ import { DEFAULT_DOCUMENT_DESIGN } from "@pollium/vyva-core/themes";
5
+ import mammoth from "mammoth";
6
+ import { extractText, getDocumentProxy } from "unpdf";
7
+ //#region src/import/documents.ts
8
+ const MAX_IMAGES = 60;
9
+ const PAGE_BREAK = "<div data-type=\"page-break\"></div>";
10
+ const documentOf = (blocks, filename) => ({
11
+ title: titleFrom(blocks, filename),
12
+ content: {
13
+ kind: "document",
14
+ design: DEFAULT_DOCUMENT_DESIGN,
15
+ blocks
16
+ }
17
+ });
18
+ const importDocx = async (buffer, filename, saveImage) => {
19
+ const save = imageSaver(saveImage, filename, MAX_IMAGES);
20
+ const failures = [];
21
+ const result = await mammoth.convertToHtml({ buffer }, { convertImage: mammoth.images.imgElement(async (image) => {
22
+ const data = await image.readAsBuffer();
23
+ const fileId = await save(data, image.contentType).catch((error) => {
24
+ failures.push(error);
25
+ return null;
26
+ });
27
+ return fileId === null ? { src: "" } : {
28
+ src: "",
29
+ "data-file-id": fileId,
30
+ alt: ""
31
+ };
32
+ }) });
33
+ if (failures.length > 0) throw failures[0];
34
+ return documentOf(htmlToBlocks(result.value), filename);
35
+ };
36
+ const importMarkdown = (text, filename) => documentOf(markdownToBlocks(text), filename);
37
+ const importPlainText = (text, filename) => documentOf(htmlToBlocks(paragraphsHtml(text)), filename);
38
+ const importPdf = async (buffer, filename) => {
39
+ const { text } = await extractText(await getDocumentProxy(new Uint8Array(buffer)), { mergePages: false });
40
+ const html = text.map((page) => paragraphsHtml(page)).filter((page) => page !== "").join(PAGE_BREAK);
41
+ return documentOf(htmlToBlocks(html), filename);
42
+ };
43
+ //#endregion
44
+ export { importDocx, importMarkdown, importPdf, importPlainText };
@@ -0,0 +1,6 @@
1
+ import { DocumentBlock } from "@pollium/vyva-core/content";
2
+ //#region src/import/html-to-blocks.d.ts
3
+ export declare const htmlToBlocks: (html: string) => DocumentBlock[];
4
+ export declare const titleFrom: (blocks: DocumentBlock[], filename: string) => string;
5
+ export declare const paragraphsHtml: (text: string) => string;
6
+ //#endregion
@@ -0,0 +1,58 @@
1
+ import { cleanHtml } from "../clean-html.js";
2
+ import { titleOf } from "./titles.js";
3
+ import { newId } from "@pollium/vyva-core/ids";
4
+ import { DomUtils, parseDocument } from "htmlparser2";
5
+ //#region src/import/html-to-blocks.ts
6
+ const BLOCK_TAGS = /* @__PURE__ */ new Set([
7
+ "p",
8
+ "h1",
9
+ "h2",
10
+ "h3",
11
+ "h4",
12
+ "ul",
13
+ "ol",
14
+ "table",
15
+ "pre",
16
+ "blockquote",
17
+ "hr",
18
+ "div",
19
+ "img"
20
+ ]);
21
+ const HEADINGS = /* @__PURE__ */ new Set(["h5", "h6"]);
22
+ const ENTITIES = {
23
+ "&amp;": "&",
24
+ "&lt;": "<",
25
+ "&gt;": ">",
26
+ "&quot;": "\"",
27
+ "&#39;": "'"
28
+ };
29
+ const htmlToBlocks = (html) => {
30
+ const blocks = [];
31
+ let loose = [];
32
+ const push = (outer) => {
33
+ const cleaned = cleanHtml(outer);
34
+ if (cleaned !== "" && cleaned !== "<p></p>") blocks.push({
35
+ id: newId(),
36
+ html: cleaned
37
+ });
38
+ };
39
+ const flush = () => {
40
+ const inner = loose.map((node) => DomUtils.getOuterHTML(node)).join("").trim();
41
+ loose = [];
42
+ if (inner !== "") push(`<p>${inner}</p>`);
43
+ };
44
+ for (const node of parseDocument(html).children) if (DomUtils.isTag(node) && (BLOCK_TAGS.has(node.name) || HEADINGS.has(node.name))) {
45
+ flush();
46
+ push(HEADINGS.has(node.name) ? `<h4>${DomUtils.getInnerHTML(node)}</h4>` : DomUtils.getOuterHTML(node));
47
+ } else loose.push(node);
48
+ flush();
49
+ return blocks;
50
+ };
51
+ const titleFrom = (blocks, filename) => {
52
+ const heading = blocks.find((block) => /^<h[1-3]/.test(block.html));
53
+ const text = heading === void 0 ? "" : heading.html.replace(/<[^>]+>/g, "").replace(/&(?:amp|lt|gt|quot|#39);/g, (entity) => ENTITIES[entity] ?? entity);
54
+ return titleOf(text, filename);
55
+ };
56
+ const paragraphsHtml = (text) => text.replace(/\r\n?/g, "\n").split(/\n{2,}/).map((part) => part.trim()).filter((part) => part !== "").map((part) => `<p>${part.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")}</p>`).join("");
57
+ //#endregion
58
+ export { htmlToBlocks, paragraphsHtml, titleFrom };
@@ -0,0 +1,10 @@
1
+ //#region src/import/images.d.ts
2
+ export interface NewImage {
3
+ name: string;
4
+ data: Uint8Array;
5
+ contentType: string;
6
+ }
7
+ export type SaveImage = (image: NewImage) => Promise<string | null>;
8
+ export type ImageSaver = (data: Uint8Array, contentType: string) => Promise<string | null>;
9
+ export declare const imageSaver: (saveImage: SaveImage, filename: string, limit: number) => ImageSaver;
10
+ //#endregion
@@ -0,0 +1,23 @@
1
+ //#region src/import/images.ts
2
+ const EXTENSIONS = {
3
+ "image/png": "png",
4
+ "image/jpeg": "jpg",
5
+ "image/gif": "gif",
6
+ "image/webp": "webp"
7
+ };
8
+ const imageSaver = (saveImage, filename, limit) => {
9
+ const base = filename.replace(/\.[a-z0-9]+$/i, "");
10
+ let taken = 0;
11
+ return async (data, contentType) => {
12
+ const extension = EXTENSIONS[contentType];
13
+ if (extension === void 0 || taken >= limit) return null;
14
+ taken += 1;
15
+ return saveImage({
16
+ name: `${base} (image ${taken}).${extension}`,
17
+ data,
18
+ contentType
19
+ });
20
+ };
21
+ };
22
+ //#endregion
23
+ export { imageSaver };
@@ -0,0 +1,4 @@
1
+ import { DeckContent } from "@pollium/vyva-core/content";
2
+ //#region src/import/pdf-lines.d.ts
3
+ export declare const joinLines: (deck: DeckContent) => DeckContent;
4
+ //#endregion
@@ -0,0 +1,206 @@
1
+ import { textHeight, unwrappedWidth } from "../text-measure.js";
2
+ import { DECK_SIZES } from "@pollium/vyva-core/content";
3
+ import { newId } from "@pollium/vyva-core/ids";
4
+ //#region src/import/pdf-lines.ts
5
+ const BULLET = /^[•▪◦●○■□➢►▸✓✔\-–*]$/;
6
+ const NUMBER = /^(\d{1,3}|[a-zA-Z])[.)]$/;
7
+ const LEADING_BULLET = /^((?:<[^>]+>)*)\s*[•▪◦●○■□➢►▸✓✔\-–*]\s+(?=\S)/u;
8
+ const LEADING_NUMBER = /^((?:<[^>]+>)*)\s*(?:\d{1,3}|[a-z])[.)]\s+(?=\S)/u;
9
+ const MAX_CONTAINER_SHARE = .5;
10
+ const CONTAINER_SLACK = 2;
11
+ const PARAGRAPH = /^<p(?: style="[^"]*")?>([\s\S]*)<\/p>$/;
12
+ const MARKER_REACH_EMS = 3;
13
+ const SAME_EDGE_EMS = .5;
14
+ const SAME_SIZE = .1;
15
+ const MAX_LINE_GAP = 1;
16
+ const PARAGRAPH_GAP = .3;
17
+ const LIST_INDENT_EMS = 1.15;
18
+ const SLIDE_MARGIN = 16;
19
+ const SIDE_SHARE = .3;
20
+ const SPACED_END = /\s(?:<\/[^>]+>)*$/;
21
+ const plain = (html) => html.replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").trim();
22
+ const middle = (element) => element.y + element.h / 2;
23
+ const right = (element) => element.x + element.w;
24
+ const center = (element) => element.x + element.w / 2;
25
+ const area = (element) => element.w * element.h;
26
+ const containerOf = (element, boxes) => boxes.filter((box) => box.x - CONTAINER_SLACK <= element.x && box.y - CONTAINER_SLACK <= element.y && box.x + box.w + CONTAINER_SLACK >= right(element) && box.y + box.h + CONTAINER_SLACK >= element.y + element.h).sort((a, b) => area(a) - area(b))[0] ?? null;
27
+ const lineOf = (element, boxes) => {
28
+ if (element.type !== "text" || element.isLocked || element.rotation !== 0 || element.fill !== null) return null;
29
+ const html = PARAGRAPH.exec(element.html)?.[1];
30
+ if (html === void 0 || html.includes("<p")) return null;
31
+ const marker = LEADING_BULLET.test(html) ? "bullet" : LEADING_NUMBER.test(html) ? "number" : null;
32
+ const inner = marker === null ? html : html.replace(marker === "bullet" ? LEADING_BULLET : LEADING_NUMBER, "$1");
33
+ return {
34
+ element,
35
+ inner,
36
+ text: plain(inner),
37
+ marker,
38
+ left: element.x,
39
+ container: containerOf(element, boxes)
40
+ };
41
+ };
42
+ const markerOf = (line) => BULLET.test(line.text) ? "bullet" : NUMBER.test(line.text) ? "number" : null;
43
+ const ownerOf = (marker, lines) => lines.find((line) => {
44
+ const { element } = line;
45
+ const reach = MARKER_REACH_EMS * element.style.fontSize;
46
+ const gap = element.x - right(marker.element);
47
+ return line.marker === null && markerOf(line) === null && gap > -element.style.fontSize && gap < reach && Math.abs(middle(element) - middle(marker.element)) < element.h * .6;
48
+ });
49
+ const isSameSize = (a, b) => Math.abs(a.fontSize - b.fontSize) <= Math.max(a.fontSize, b.fontSize) * SAME_SIZE;
50
+ const alignWith = (block, line) => {
51
+ const last = block.lines.at(-1)?.element;
52
+ if (last === void 0) return null;
53
+ const { element } = line;
54
+ const edge = SAME_EDGE_EMS * element.style.fontSize;
55
+ const gap = element.y - (last.y + last.h);
56
+ if (line.container !== block.lines[0].container) return null;
57
+ if (!isSameSize(last.style, element.style) || element.y <= last.y || gap > last.h * MAX_LINE_GAP || gap < -last.h * .5) return null;
58
+ if (gap > last.h * PARAGRAPH_GAP && line.marker === null !== (block.lines[0].marker === null)) return null;
59
+ if (Math.abs(element.x - block.lines[0].element.x) < edge) return block.lines.length === 1 || block.align === "left" ? "left" : null;
60
+ if (line.marker === null && Math.abs(center(element) - center(last)) < edge) return block.lines.length === 1 || block.align === "center" ? "center" : null;
61
+ return null;
62
+ };
63
+ const joined = (lines) => lines.reduce((html, line, index) => {
64
+ if (index === 0) return line.inner;
65
+ const previous = lines[index - 1].text;
66
+ if (/\p{L}-$/u.test(previous) && /^\p{Ll}/u.test(line.text)) return `${html.replace(/-((?:<\/[^>]+>)*)$/, "$1")}${line.inner}`;
67
+ return SPACED_END.test(html) || /^\s/.test(line.inner) ? `${html}${line.inner}` : `${html} ${line.inner}`;
68
+ }, "");
69
+ const styled = (line, base) => {
70
+ const { style } = line.element;
71
+ return style.color.toLowerCase() === base.color.toLowerCase() ? line : {
72
+ ...line,
73
+ inner: `<span style="color:${style.color}">${line.inner}</span>`
74
+ };
75
+ };
76
+ const paragraphsOf = (lines) => lines.reduce((paragraphs, line, index) => {
77
+ const previous = lines[index - 1]?.element;
78
+ const current = paragraphs.at(-1);
79
+ if (current === void 0 || previous === void 0 || line.marker !== null || line.element.y - (previous.y + previous.h) > previous.h * PARAGRAPH_GAP || current === void 0) paragraphs.push([line]);
80
+ else current.push(line);
81
+ return paragraphs;
82
+ }, []);
83
+ const listTag = (marker) => marker === "bullet" ? "ul" : "ol";
84
+ const htmlOf = (paragraphs) => {
85
+ let html = "";
86
+ let open = null;
87
+ for (const paragraph of paragraphs) {
88
+ const marker = paragraph[0].marker;
89
+ if (open !== null && open !== marker) {
90
+ html += `</${listTag(open)}>`;
91
+ open = null;
92
+ }
93
+ if (marker === null) {
94
+ html += `<p>${joined(paragraph)}</p>`;
95
+ continue;
96
+ }
97
+ if (open === null) html += `<${listTag(marker)}>`;
98
+ open = marker;
99
+ html += `<li>${joined(paragraph)}</li>`;
100
+ }
101
+ return open === null ? html : `${html}</${listTag(open)}>`;
102
+ };
103
+ const lineHeightOf = (lines, fontSize, fallback) => {
104
+ const pitches = lines.slice(1).map((line, index) => line.element.y - lines[index].element.y).filter((pitch) => pitch < fontSize * 3);
105
+ if (pitches.length === 0) return fallback;
106
+ const sorted = pitches.sort((a, b) => a - b);
107
+ return Math.min(3, Math.max(.8, Number((sorted[Math.floor(sorted.length / 2)] / fontSize).toFixed(2))));
108
+ };
109
+ const merged = (block, neighbours, font, slideWidth) => {
110
+ const [first] = block.lines;
111
+ const base = {
112
+ ...first.element.style,
113
+ isBold: block.lines.every((line) => line.element.style.isBold),
114
+ align: block.align,
115
+ verticalAlign: "top"
116
+ };
117
+ const lines = block.lines.map((line) => styled(line, base));
118
+ const paragraphs = paragraphsOf(lines);
119
+ const html = htmlOf(paragraphs);
120
+ const indent = LIST_INDENT_EMS * base.fontSize;
121
+ const left = Math.max(0, Math.min(...block.lines.map((line) => line.left)));
122
+ const last = block.lines[block.lines.length - 1].element;
123
+ const wanted = Math.max(...block.lines.map((line) => Math.max(right(line.element), left + (line.marker === null ? 0 : indent) + unwrappedWidth(`<p>${line.inner}</p>`, base, font)))) - left;
124
+ const limit = Math.min(slideWidth - SLIDE_MARGIN, ...neighbours.filter((element) => isInTheWay(element, left, wanted, first.element.y, last.y + last.h)).map((element) => element.x - SLIDE_MARGIN));
125
+ const w = Math.max(1, Math.round(Math.min(wanted, limit - left)));
126
+ const style = {
127
+ ...base,
128
+ lineHeight: lineHeightOf(block.lines, base.fontSize, base.lineHeight)
129
+ };
130
+ const height = textHeight(html, style, font, w);
131
+ return {
132
+ id: newId(),
133
+ type: "text",
134
+ x: Math.round(left),
135
+ y: first.element.y,
136
+ w,
137
+ h: Math.max(1, Math.round(Number.isFinite(height) ? height : first.element.h * block.lines.length)),
138
+ rotation: 0,
139
+ opacity: first.element.opacity,
140
+ isLocked: false,
141
+ fill: null,
142
+ html,
143
+ style
144
+ };
145
+ };
146
+ const isInTheWay = (element, left, width, top, bottom) => element.x > left + width * SIDE_SHARE && element.x < left + width && element.y < bottom && element.y + element.h > top;
147
+ const blocksOf = (lines) => {
148
+ const blocks = [];
149
+ const ordered = [...lines].sort((a, b) => a.element.y - b.element.y || a.element.x - b.element.x);
150
+ for (const line of ordered) {
151
+ const match = blocks.map((block) => ({
152
+ block,
153
+ align: alignWith(block, line)
154
+ })).find(({ align }) => align !== null);
155
+ if (match === void 0 || match.align === null) {
156
+ blocks.push({
157
+ lines: [line],
158
+ align: "left"
159
+ });
160
+ continue;
161
+ }
162
+ match.block.lines.push(line);
163
+ match.block.align = match.align;
164
+ }
165
+ return blocks;
166
+ };
167
+ const joinSlide = (slide, font, slideWidth, slideHeight) => {
168
+ const boxes = slide.elements.filter((element) => element.type === "shape" && element.fill !== null && element.shape !== "line" && area(element) < slideWidth * slideHeight * MAX_CONTAINER_SHARE);
169
+ const lines = slide.elements.map((element) => lineOf(element, boxes)).filter((line) => line !== null);
170
+ const markers = lines.filter((line) => markerOf(line) !== null);
171
+ const owned = /* @__PURE__ */ new Set();
172
+ for (const marker of markers) {
173
+ const owner = ownerOf(marker, lines);
174
+ if (owner === void 0) continue;
175
+ owner.marker = markerOf(marker);
176
+ owner.left = marker.element.x;
177
+ owned.add(marker);
178
+ }
179
+ const blocks = blocksOf(lines.filter((line) => !owned.has(line)));
180
+ const replaced = /* @__PURE__ */ new Map();
181
+ for (const block of blocks) {
182
+ if (block.lines.length === 1 && block.lines[0].marker === null) continue;
183
+ const [head, ...rest] = block.lines;
184
+ const own = /* @__PURE__ */ new Set([...block.lines.map((line) => line.element), ...[...owned].map((marker) => marker.element)]);
185
+ replaced.set(head.element, merged(block, slide.elements.filter((element) => !own.has(element)), font, slideWidth));
186
+ for (const line of rest) replaced.set(line.element, null);
187
+ }
188
+ for (const marker of owned) replaced.set(marker.element, null);
189
+ const elements = slide.elements.flatMap((element) => {
190
+ const replacement = replaced.get(element);
191
+ return replacement === void 0 ? [element] : replacement === null ? [] : [replacement];
192
+ });
193
+ return {
194
+ ...slide,
195
+ elements
196
+ };
197
+ };
198
+ const joinLines = (deck) => {
199
+ const { width, height } = DECK_SIZES[deck.format];
200
+ return {
201
+ ...deck,
202
+ slides: deck.slides.map((slide) => joinSlide(slide, deck.theme.font, width, height))
203
+ };
204
+ };
205
+ //#endregion
206
+ export { joinLines };
@@ -0,0 +1,7 @@
1
+ import { SaveImage } from "./images.js";
2
+ import { ImportedDeck } from "./pptx.js";
3
+ //#region src/import/pdf-slides.d.ts
4
+ export type PdfSlides = (pdf: Uint8Array) => Promise<Uint8Array>;
5
+ export declare const isLandscapePdf: (buffer: Buffer) => Promise<boolean>;
6
+ export declare const importPdfSlides: (buffer: Buffer, filename: string, saveImage: SaveImage, pdfSlides: PdfSlides) => Promise<ImportedDeck>;
7
+ //#endregion
@@ -0,0 +1,21 @@
1
+ import { ConvertError } from "../errors.js";
2
+ import { joinLines } from "./pdf-lines.js";
3
+ import { importPptx } from "./pptx.js";
4
+ import { getDocumentProxy } from "unpdf";
5
+ //#region src/import/pdf-slides.ts
6
+ const isLandscapePdf = async (buffer) => {
7
+ const { width, height } = (await (await getDocumentProxy(new Uint8Array(buffer))).getPage(1)).getViewport({ scale: 1 });
8
+ return width > height;
9
+ };
10
+ const importPdfSlides = async (buffer, filename, saveImage, pdfSlides) => {
11
+ const pptx = await pdfSlides(new Uint8Array(buffer)).catch((error) => {
12
+ throw error instanceof ConvertError ? error : new ConvertError("slides-failed", `${filename} could not be turned into slides`, { cause: error });
13
+ });
14
+ const { title, content } = await importPptx(Buffer.from(pptx), filename, saveImage);
15
+ return {
16
+ title,
17
+ content: joinLines(content)
18
+ };
19
+ };
20
+ //#endregion
21
+ export { importPdfSlides, isLandscapePdf };
@@ -0,0 +1,11 @@
1
+ import { XmlElement } from "./pptx-xml.js";
2
+ import { ShapePoint } from "@pollium/vyva-core/content";
3
+ //#region src/import/pptx-geometry.d.ts
4
+ export type Outline = {
5
+ kind: 'rect';
6
+ } | {
7
+ kind: 'polygon';
8
+ points: ShapePoint[];
9
+ };
10
+ export declare const outlineOf: (geometry: XmlElement, xfrm: XmlElement | undefined) => Outline | null;
11
+ //#endregion
@@ -0,0 +1,74 @@
1
+ import { all, attr, num, tags } from "./pptx-xml.js";
2
+ import { MAX_SHAPE_POINTS } from "@pollium/vyva-core/content";
3
+ //#region src/import/pptx-geometry.ts
4
+ const CURVE_STEPS = [
5
+ .25,
6
+ .5,
7
+ .75,
8
+ 1
9
+ ];
10
+ const SAME = .001;
11
+ const cubic = (from, [c1, c2, to], t) => {
12
+ const u = 1 - t;
13
+ const at = (axis) => u * u * u * from[axis] + 3 * u * u * t * c1[axis] + 3 * u * t * t * c2[axis] + t * t * t * to[axis];
14
+ return [at(0), at(1)];
15
+ };
16
+ const quadratic = (from, [control, to], t) => {
17
+ const u = 1 - t;
18
+ const at = (axis) => u * u * from[axis] + 2 * u * t * control[axis] + t * t * to[axis];
19
+ return [at(0), at(1)];
20
+ };
21
+ const pointsOf = (step) => all(step, "a:pt").map((point) => [num(attr(point, "x")), num(attr(point, "y"))]);
22
+ const traced = (path) => {
23
+ const points = [];
24
+ let moves = 0;
25
+ for (const step of tags(path)) {
26
+ const given = pointsOf(step);
27
+ const from = points.at(-1) ?? [0, 0];
28
+ if (step.name === "a:moveTo") moves += 1;
29
+ if (step.name === "a:moveTo" || step.name === "a:lnTo") points.push(...given.slice(-1));
30
+ else if (step.name === "a:cubicBezTo" && given.length === 3) points.push(...CURVE_STEPS.map((t) => cubic(from, given, t)));
31
+ else if (step.name === "a:quadBezTo" && given.length === 2) points.push(...CURVE_STEPS.map((t) => quadratic(from, given, t)));
32
+ else if (step.name !== "a:close") return null;
33
+ }
34
+ return moves === 1 ? points : null;
35
+ };
36
+ const isBox = (points) => points.length === 4 && points.every((point) => [point.x, point.y].every((value) => value < SAME || value > .999));
37
+ const thinned = (points) => {
38
+ const distinct = points.filter((point, index) => {
39
+ const previous = points[index - 1];
40
+ return previous === void 0 || Math.abs(point.x - previous.x) > SAME || Math.abs(point.y - previous.y) > SAME;
41
+ });
42
+ const [head] = distinct;
43
+ const tail = distinct.at(-1);
44
+ const open = distinct.length > 1 && head !== void 0 && tail !== void 0 && Math.abs(head.x - tail.x) < SAME && Math.abs(head.y - tail.y) < SAME ? distinct.slice(0, -1) : distinct;
45
+ if (open.length <= MAX_SHAPE_POINTS) return open;
46
+ const stride = open.length / MAX_SHAPE_POINTS;
47
+ return Array.from({ length: MAX_SHAPE_POINTS }, (_, index) => open[Math.floor(index * stride)]);
48
+ };
49
+ const outlineOf = (geometry, xfrm) => {
50
+ const paths = all(geometry, "a:path");
51
+ const [path] = paths;
52
+ if (paths.length !== 1 || path === void 0) return null;
53
+ const w = num(attr(path, "w"));
54
+ const h = num(attr(path, "h"));
55
+ const steps = traced(path);
56
+ if (w <= 0 || h <= 0 || steps === null) return null;
57
+ const flipX = attr(xfrm, "flipH") === "1";
58
+ const flipY = attr(xfrm, "flipV") === "1";
59
+ const unit = (value, extent, isFlipped) => {
60
+ const share = Math.min(1, Math.max(0, value / extent));
61
+ return Number((isFlipped ? 1 - share : share).toFixed(4));
62
+ };
63
+ const points = thinned(steps.map(([x, y]) => ({
64
+ x: unit(x, w, flipX),
65
+ y: unit(y, h, flipY)
66
+ })));
67
+ if (points.length < 3) return null;
68
+ return isBox(points) ? { kind: "rect" } : {
69
+ kind: "polygon",
70
+ points
71
+ };
72
+ };
73
+ //#endregion
74
+ export { outlineOf };
@@ -0,0 +1,22 @@
1
+ import { PackageFiles, Relations, XmlDocument, XmlElement } from "./pptx-xml.js";
2
+ import Palette from "./pptx-theme.js";
3
+ //#region src/import/pptx-package.d.ts
4
+ export interface SlidePart {
5
+ root: XmlElement;
6
+ tree: XmlElement | undefined;
7
+ relations: Relations;
8
+ palette: Palette;
9
+ }
10
+ export interface SlideParts {
11
+ slide: SlidePart;
12
+ layout: SlidePart | null;
13
+ master: SlidePart | null;
14
+ }
15
+ export default class PptxPackage {
16
+ #private;
17
+ constructor(files: PackageFiles);
18
+ document(path: string): XmlDocument | undefined;
19
+ relationsOf(path: string): Relations;
20
+ parts(slidePath: string): SlideParts | null;
21
+ }
22
+ //#endregion
@@ -0,0 +1,61 @@
1
+ import { first, relatedPart, relationsOf, xml } from "./pptx-xml.js";
2
+ import Palette, { colorMapOf, themeOf } from "./pptx-theme.js";
3
+ import { DomUtils } from "htmlparser2";
4
+ import { strFromU8 } from "fflate";
5
+ //#region src/import/pptx-package.ts
6
+ var PptxPackage = class {
7
+ #files;
8
+ #documents = /* @__PURE__ */ new Map();
9
+ #palettes = /* @__PURE__ */ new Map();
10
+ constructor(files) {
11
+ this.#files = files;
12
+ }
13
+ document(path) {
14
+ const known = this.#documents.get(path);
15
+ if (known !== void 0) return known;
16
+ const data = this.#files[path];
17
+ if (data === void 0) return void 0;
18
+ const document = xml(strFromU8(data));
19
+ this.#documents.set(path, document);
20
+ return document;
21
+ }
22
+ relationsOf(path) {
23
+ return relationsOf(this.#files, path);
24
+ }
25
+ parts(slidePath) {
26
+ const slideRelations = relationsOf(this.#files, slidePath);
27
+ const layoutPath = relatedPart(slideRelations, "slideLayout");
28
+ const layoutRelations = layoutPath === null ? {} : relationsOf(this.#files, layoutPath);
29
+ const masterPath = layoutPath === null ? null : relatedPart(layoutRelations, "slideMaster");
30
+ const palette = this.#palette(masterPath);
31
+ const slide = this.#part(slidePath, slideRelations, palette);
32
+ if (slide === null) return null;
33
+ return {
34
+ slide,
35
+ layout: layoutPath === null ? null : this.#part(layoutPath, layoutRelations, palette),
36
+ master: masterPath === null ? null : this.#part(masterPath, relationsOf(this.#files, masterPath), palette)
37
+ };
38
+ }
39
+ #part(path, relations, palette) {
40
+ const root = this.document(path)?.children.find((node) => DomUtils.isTag(node));
41
+ if (root === void 0) return null;
42
+ return {
43
+ root,
44
+ tree: first(root, "p:spTree"),
45
+ relations,
46
+ palette
47
+ };
48
+ }
49
+ #palette(masterPath) {
50
+ const key = masterPath ?? "";
51
+ const known = this.#palettes.get(key);
52
+ if (known !== void 0) return known;
53
+ const masterRoot = (masterPath === null ? void 0 : this.document(masterPath))?.children.find((node) => DomUtils.isTag(node));
54
+ const themePath = masterPath === null ? null : relatedPart(relationsOf(this.#files, masterPath), "theme");
55
+ const palette = new Palette(themeOf(themePath === null ? void 0 : this.document(themePath)), colorMapOf(masterRoot));
56
+ this.#palettes.set(key, palette);
57
+ return palette;
58
+ }
59
+ };
60
+ //#endregion
61
+ export { PptxPackage as default };
@@ -0,0 +1,11 @@
1
+ import { XmlElement } from "./pptx-xml.js";
2
+ import { SlidePart, SlideParts } from "./pptx-package.js";
3
+ //#region src/import/pptx-placeholders.d.ts
4
+ export declare const TITLE_TYPES: string[];
5
+ export declare const isDefined: <T>(value: T | undefined) => value is T;
6
+ export declare const placeholderChain: (shape: XmlElement, placeholder: XmlElement, parts: SlideParts) => XmlElement[];
7
+ export declare const levelOf: (shape: XmlElement) => XmlElement | undefined;
8
+ export declare const masterLevel: (parts: SlideParts, type: string | null) => XmlElement | undefined;
9
+ export declare const bulletsByDefault: (levels: XmlElement[]) => boolean;
10
+ export declare const decorOf: (parts: SlideParts) => SlidePart[];
11
+ //#endregion