@pollium/vyva-convert 0.1.0 → 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.
- package/README.md +14 -2
- package/dist/clean-content.js +6 -1
- package/dist/errors.d.ts +1 -1
- package/dist/export/pptx.js +26 -2
- package/dist/import/pdf-lines.d.ts +4 -0
- package/dist/import/pdf-lines.js +206 -0
- package/dist/import/pdf-slides.d.ts +7 -0
- package/dist/import/pdf-slides.js +21 -0
- package/dist/import/pptx-geometry.d.ts +11 -0
- package/dist/import/pptx-geometry.js +74 -0
- package/dist/import/pptx-slide.js +33 -9
- package/dist/import/pptx-theme.d.ts +2 -1
- package/dist/import/pptx-theme.js +6 -7
- package/dist/import/pptx.js +17 -3
- package/dist/import.d.ts +3 -1
- package/dist/import.js +6 -4
- package/dist/libreoffice.d.ts +8 -0
- package/dist/libreoffice.js +39 -0
- package/dist/text-measure.d.ts +1 -0
- package/dist/text-measure.js +5 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -70,8 +70,19 @@ const { title, content } = await importFile(upload.bytes, upload.filename, {
|
|
|
70
70
|
by what their zip holds, and PDF by its signature. Only Markdown (`.md`) and text (`.txt`) are
|
|
71
71
|
recognized by their extension, and only when they are valid UTF-8.
|
|
72
72
|
- Word, PDF, Markdown and text files become documents, and PowerPoint files become decks. A deck
|
|
73
|
-
keeps its theme colors and font. What its slide master draws comes in
|
|
74
|
-
own elements.
|
|
73
|
+
keeps its theme colors and the font most of its text uses. What its slide master draws comes in
|
|
74
|
+
locked, behind the slide's own elements, and freeform shapes come in as polygons.
|
|
75
|
+
- With `pdfSlides`, a PDF whose pages are landscape becomes a deck instead: each page a slide, with
|
|
76
|
+
its pictures and shapes where they were, and the loose lines a PDF keeps joined back into
|
|
77
|
+
paragraphs and lists. A portrait PDF stays a document. `pdfSlides` takes the PDF and returns it as
|
|
78
|
+
a PowerPoint file; `libreOffice()` from `@pollium/vyva-convert/libreoffice` does that with the
|
|
79
|
+
`soffice` on the machine (`command` changes the path), one conversion per call:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { libreOffice } from '@pollium/vyva-convert/libreoffice';
|
|
83
|
+
|
|
84
|
+
await importFile(upload.bytes, upload.filename, { saveImage, pdfSlides: libreOffice() });
|
|
85
|
+
```
|
|
75
86
|
- `saveImage` stores each image and returns its file id, which must be a UUID. Return `null` to
|
|
76
87
|
leave the image out. If it throws, the import fails with your error.
|
|
77
88
|
- Files over 25 MB (`maxBytes` changes the limit) and files it cannot read are refused.
|
|
@@ -87,3 +98,4 @@ a bug or a broken file, so log it.
|
|
|
87
98
|
| `too-large` | The file, or what it unzips to, is over the limit |
|
|
88
99
|
| `empty` | The file has nothing to import |
|
|
89
100
|
| `pdf-failed` | The PDF engine could not be reached or answered with an error |
|
|
101
|
+
| `slides-failed` | `pdfSlides` could not turn a PDF into slides |
|
package/dist/clean-content.js
CHANGED
|
@@ -28,7 +28,12 @@ const cleanElement = (element, font) => {
|
|
|
28
28
|
html
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
-
if (element.type !== "shape"
|
|
31
|
+
if (element.type !== "shape") return element;
|
|
32
|
+
if (element.points !== void 0 && element.shape !== "polygon") {
|
|
33
|
+
const { points: _points, ...plain } = element;
|
|
34
|
+
return cleanElement(plain, font);
|
|
35
|
+
}
|
|
36
|
+
if (element.text === void 0) return element;
|
|
32
37
|
const { text, ...bare } = element;
|
|
33
38
|
const html = TEXT_SHAPES.includes(element.shape) ? cleanHtml(text.html) : "";
|
|
34
39
|
return html === "" ? bare : {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/errors.d.ts
|
|
2
|
-
export type ConvertErrorCode = 'too-large' | 'unsupported' | 'empty' | 'pdf-failed';
|
|
2
|
+
export type ConvertErrorCode = 'too-large' | 'unsupported' | 'empty' | 'pdf-failed' | 'slides-failed';
|
|
3
3
|
export declare class ConvertError extends Error {
|
|
4
4
|
readonly code: ConvertErrorCode;
|
|
5
5
|
constructor(code: ConvertErrorCode, message: string, options?: ErrorOptions);
|
package/dist/export/pptx.js
CHANGED
|
@@ -110,8 +110,31 @@ const SHAPES = {
|
|
|
110
110
|
ellipse: "ellipse",
|
|
111
111
|
triangle: "triangle",
|
|
112
112
|
line: "line",
|
|
113
|
-
arrow: "line"
|
|
113
|
+
arrow: "line",
|
|
114
|
+
polygon: "custGeom"
|
|
114
115
|
};
|
|
116
|
+
const BOX_CORNERS = [
|
|
117
|
+
{
|
|
118
|
+
x: 0,
|
|
119
|
+
y: 0
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
x: 1,
|
|
123
|
+
y: 0
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
x: 1,
|
|
127
|
+
y: 1
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
x: 0,
|
|
131
|
+
y: 1
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
const outline = (element) => [...(element.points ?? BOX_CORNERS).map((point) => ({
|
|
135
|
+
x: inch(point.x * element.w),
|
|
136
|
+
y: inch(point.y * element.h)
|
|
137
|
+
})), { close: true }];
|
|
115
138
|
const addShape = (slide, element, deck) => {
|
|
116
139
|
const isLine = element.shape === "line" || element.shape === "arrow";
|
|
117
140
|
const lineColor = element.stroke ?? element.fill;
|
|
@@ -132,7 +155,8 @@ const addShape = (slide, element, deck) => {
|
|
|
132
155
|
width: Math.max(.5, element.strokeWidth * PT_PER_PX),
|
|
133
156
|
...element.shape === "arrow" ? { endArrowType: "triangle" } : {}
|
|
134
157
|
} },
|
|
135
|
-
...element.shape === "rounded" ? { rectRadius: .18 } : {}
|
|
158
|
+
...element.shape === "rounded" ? { rectRadius: .18 } : {},
|
|
159
|
+
...element.shape === "polygon" ? { points: outline(element) } : {}
|
|
136
160
|
};
|
|
137
161
|
if (element.text !== void 0 && TEXT_SHAPES.includes(element.shape) && element.text.html.trim() !== "") {
|
|
138
162
|
slide.addText(textRuns(element.text.html, element.text.style), {
|
|
@@ -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(/ /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 };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { cleanHtml } from "../clean-html.js";
|
|
2
|
+
import { unwrappedWidth } from "../text-measure.js";
|
|
2
3
|
import { all, attr, first, kid, kids, num, tags, textOf } from "./pptx-xml.js";
|
|
3
4
|
import { TITLE_TYPES, bulletsByDefault, decorOf, isDefined, levelOf, masterLevel, placeholderChain } from "./pptx-placeholders.js";
|
|
5
|
+
import { outlineOf } from "./pptx-geometry.js";
|
|
4
6
|
import { MAX_CELL_TEXT, MAX_TABLE_COLUMNS, MAX_TABLE_ROWS } from "@pollium/vyva-core/content";
|
|
5
7
|
import { newId } from "@pollium/vyva-core/ids";
|
|
6
8
|
import { escapeHtml } from "@pollium/vyva-core/escape";
|
|
@@ -120,6 +122,21 @@ var SlideReader = class {
|
|
|
120
122
|
isLocked
|
|
121
123
|
};
|
|
122
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
|
+
}
|
|
123
140
|
#pixels(size) {
|
|
124
141
|
const hundredths = num(size);
|
|
125
142
|
return hundredths > 0 ? Math.round(hundredths / 100 * PX_PER_POINT * this.#options.fontScale) : null;
|
|
@@ -174,11 +191,7 @@ var SlideReader = class {
|
|
|
174
191
|
const palette = visit.part.palette;
|
|
175
192
|
const firstRun = all(body, "a:r").map((run) => kid(run, "a:rPr")).find(isDefined);
|
|
176
193
|
const firstAlign = ALIGNS[attr(kid(kids(body, "a:p")[0], "a:pPr"), "algn") ?? ""] ?? null;
|
|
177
|
-
const
|
|
178
|
-
{
|
|
179
|
-
...this.#runLook(firstRun, palette),
|
|
180
|
-
align: firstAlign
|
|
181
|
-
},
|
|
194
|
+
const inherited = mergeLooks([
|
|
182
195
|
...chain.map((shape) => this.#levelLook(levelOf(shape), palette)),
|
|
183
196
|
this.#levelLook(masterLevel(visit.parts, type), palette),
|
|
184
197
|
{
|
|
@@ -186,6 +199,14 @@ var SlideReader = class {
|
|
|
186
199
|
color: styleColor(chain[0], "a:fontRef", palette)
|
|
187
200
|
}
|
|
188
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
|
+
});
|
|
189
210
|
const isTitle = TITLE_TYPES.includes(type ?? "");
|
|
190
211
|
const anchor = chain.map((shape) => attr(kid(kid(shape, "p:txBody"), "a:bodyPr"), "anchor")).find(isDefined);
|
|
191
212
|
const { theme } = this.#options;
|
|
@@ -195,7 +216,7 @@ var SlideReader = class {
|
|
|
195
216
|
align: look.align ?? "left",
|
|
196
217
|
verticalAlign: anchor === "ctr" ? "middle" : anchor === "b" ? "bottom" : "top",
|
|
197
218
|
lineHeight: 1.2,
|
|
198
|
-
isBold
|
|
219
|
+
isBold
|
|
199
220
|
};
|
|
200
221
|
}
|
|
201
222
|
#shapeInto(visit, shape, group) {
|
|
@@ -238,7 +259,7 @@ var SlideReader = class {
|
|
|
238
259
|
}
|
|
239
260
|
};
|
|
240
261
|
return {
|
|
241
|
-
...frame,
|
|
262
|
+
...chain.map((link) => attr(kid(kid(link, "p:txBody"), "a:bodyPr"), "wrap")).find(isDefined) === "none" ? this.#unwrapped(frame, html, style) : frame,
|
|
242
263
|
type: "text",
|
|
243
264
|
html,
|
|
244
265
|
fill,
|
|
@@ -246,13 +267,16 @@ var SlideReader = class {
|
|
|
246
267
|
};
|
|
247
268
|
}
|
|
248
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"));
|
|
249
272
|
return {
|
|
250
273
|
...frame,
|
|
251
274
|
type: "shape",
|
|
252
|
-
shape: line !== void 0 && first(line, "a:tailEnd") !== void 0 && kind === "line" ? "arrow" : kind,
|
|
275
|
+
shape: line !== void 0 && first(line, "a:tailEnd") !== void 0 && kind === "line" ? "arrow" : outline?.kind ?? kind,
|
|
253
276
|
fill: kind === "line" ? null : fill,
|
|
254
277
|
stroke,
|
|
255
|
-
strokeWidth
|
|
278
|
+
strokeWidth,
|
|
279
|
+
...outline?.kind === "polygon" ? { points: outline.points } : {}
|
|
256
280
|
};
|
|
257
281
|
}
|
|
258
282
|
async #pictureInto(visit, node, group) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { XmlDocument, XmlElement } from "./pptx-xml.js";
|
|
2
|
-
import { DeckTheme } from "@pollium/vyva-core/content";
|
|
2
|
+
import { DeckTheme, FontFamily } from "@pollium/vyva-core/content";
|
|
3
3
|
//#region src/import/pptx-theme.d.ts
|
|
4
|
+
export declare const familyOf: (name: string | null) => FontFamily | null;
|
|
4
5
|
export interface PptxTheme {
|
|
5
6
|
colors: Record<string, string>;
|
|
6
7
|
bodyFont: string | null;
|
|
@@ -42,6 +42,10 @@ const FONT_SUBSTITUTES = {
|
|
|
42
42
|
"courier new": "JetBrains Mono",
|
|
43
43
|
consolas: "JetBrains Mono"
|
|
44
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
|
+
};
|
|
45
49
|
const rgbOf = (hex) => {
|
|
46
50
|
const channel = (at) => Number.parseInt(hex.slice(at, at + 2), 16) / 255;
|
|
47
51
|
return [
|
|
@@ -133,11 +137,6 @@ var Palette = class {
|
|
|
133
137
|
const stop = gradient === void 0 ? void 0 : first(gradient, "a:gs");
|
|
134
138
|
return stop === void 0 ? null : this.color(stop);
|
|
135
139
|
}
|
|
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
140
|
deckTheme() {
|
|
142
141
|
const fallback = DECK_THEME;
|
|
143
142
|
const slot = (name) => {
|
|
@@ -145,7 +144,7 @@ var Palette = class {
|
|
|
145
144
|
return value === null ? null : `#${value}`;
|
|
146
145
|
};
|
|
147
146
|
return {
|
|
148
|
-
font: this.#
|
|
147
|
+
font: familyOf(this.#theme.bodyFont) ?? fallback.font,
|
|
149
148
|
background: slot("bg1") ?? fallback.background,
|
|
150
149
|
text: slot("tx1") ?? fallback.text,
|
|
151
150
|
accent: slot("accent1") ?? fallback.accent,
|
|
@@ -158,4 +157,4 @@ var Palette = class {
|
|
|
158
157
|
};
|
|
159
158
|
const colorMapOf = (master) => master === void 0 ? void 0 : kids(master, "p:clrMap")[0];
|
|
160
159
|
//#endregion
|
|
161
|
-
export { colorMapOf, Palette as default, themeOf };
|
|
160
|
+
export { colorMapOf, Palette as default, familyOf, themeOf };
|
package/dist/import/pptx.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ConvertError } from "../errors.js";
|
|
2
2
|
import { titleOf } from "./titles.js";
|
|
3
3
|
import { imageSaver } from "./images.js";
|
|
4
|
-
import { all, attr, first, num } from "./pptx-xml.js";
|
|
4
|
+
import { all, attr, first, num, textOf } from "./pptx-xml.js";
|
|
5
|
+
import { familyOf } from "./pptx-theme.js";
|
|
5
6
|
import PptxPackage from "./pptx-package.js";
|
|
6
7
|
import SlideReader from "./pptx-slide.js";
|
|
7
8
|
import { DECK_SIZES } from "@pollium/vyva-core/content";
|
|
@@ -54,6 +55,15 @@ const deckTitle = (titles, slides, filename) => {
|
|
|
54
55
|
const title = titles.find((candidate) => candidate !== "") ?? (biggest === void 0 ? "" : plainText(biggest.html));
|
|
55
56
|
return titleOf(title, filename);
|
|
56
57
|
};
|
|
58
|
+
const dominantFont = (parts, themeFont) => {
|
|
59
|
+
const counts = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const { slide } of parts) for (const run of all(slide.root, "a:r")) {
|
|
61
|
+
const typeface = attr(first(run, "a:latin"), "typeface");
|
|
62
|
+
const family = typeface === void 0 || typeface.startsWith("+") ? themeFont : familyOf(typeface);
|
|
63
|
+
if (family !== null) counts.set(family, (counts.get(family) ?? 0) + all(run, "a:t").map(textOf).join("").length);
|
|
64
|
+
}
|
|
65
|
+
return [...counts].sort((a, b) => b[1] - a[1])[0]?.[0] ?? themeFont;
|
|
66
|
+
};
|
|
57
67
|
const unzipped = (buffer) => {
|
|
58
68
|
let total = 0;
|
|
59
69
|
return unzipSync(buffer, { filter: (file) => {
|
|
@@ -73,8 +83,12 @@ const importPptx = async (buffer, filename, saveImage) => {
|
|
|
73
83
|
const format = formatFor(cx, cy);
|
|
74
84
|
const relations = pptx.relationsOf("ppt/presentation.xml");
|
|
75
85
|
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
|
|
77
|
-
if (
|
|
86
|
+
const themed = allParts[0]?.slide.palette.deckTheme();
|
|
87
|
+
if (themed === void 0) throw new ConvertError("empty", `${filename} has no slides`);
|
|
88
|
+
const theme = {
|
|
89
|
+
...themed,
|
|
90
|
+
font: dominantFont(allParts, themed.font)
|
|
91
|
+
};
|
|
78
92
|
const save = imageSaver(saveImage, filename, MAX_IMAGES);
|
|
79
93
|
const saved = /* @__PURE__ */ new Map();
|
|
80
94
|
const reader = new SlideReader({
|
package/dist/import.d.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { SaveImage } from "./import/images.js";
|
|
2
|
+
import { PdfSlides } from "./import/pdf-slides.js";
|
|
2
3
|
import { Content } from "@pollium/vyva-core/content";
|
|
3
4
|
//#region src/import.d.ts
|
|
4
5
|
export declare const MAX_IMPORT_BYTES: number;
|
|
5
6
|
export interface ImportOptions {
|
|
6
7
|
saveImage: SaveImage;
|
|
7
8
|
maxBytes?: number;
|
|
9
|
+
pdfSlides?: PdfSlides;
|
|
8
10
|
}
|
|
9
11
|
export interface ImportedContent {
|
|
10
12
|
title: string;
|
|
11
13
|
content: Content;
|
|
12
14
|
}
|
|
13
|
-
export declare const importFile: (data: Uint8Array, filename: string,
|
|
15
|
+
export declare const importFile: (data: Uint8Array, filename: string, options: ImportOptions) => Promise<ImportedContent>;
|
|
14
16
|
//#endregion
|
package/dist/import.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ConvertError } from "./errors.js";
|
|
2
2
|
import { importDocx, importMarkdown, importPdf, importPlainText } from "./import/documents.js";
|
|
3
3
|
import { importPptx } from "./import/pptx.js";
|
|
4
|
+
import { importPdfSlides, isLandscapePdf } from "./import/pdf-slides.js";
|
|
4
5
|
import { unzipSync } from "fflate";
|
|
5
6
|
//#region src/import.ts
|
|
6
7
|
const MAX_IMPORT_BYTES = 26214400;
|
|
@@ -44,21 +45,22 @@ const detect = (buffer, filename) => {
|
|
|
44
45
|
if (extension === "txt") return "text";
|
|
45
46
|
return null;
|
|
46
47
|
};
|
|
47
|
-
const convert = async (detected, buffer, filename, saveImage) => {
|
|
48
|
+
const convert = async (detected, buffer, filename, { saveImage, pdfSlides }) => {
|
|
48
49
|
if (detected === "docx") return importDocx(buffer, filename, saveImage);
|
|
49
50
|
if (detected === "pptx") return importPptx(buffer, filename, saveImage);
|
|
50
|
-
if (detected === "pdf") return importPdf(buffer, filename);
|
|
51
|
+
if (detected === "pdf") return pdfSlides !== void 0 && await isLandscapePdf(buffer) ? importPdfSlides(buffer, filename, saveImage, pdfSlides) : importPdf(buffer, filename);
|
|
51
52
|
const text = buffer.toString("utf8").replace(BOM, "");
|
|
52
53
|
return detected === "markdown" ? importMarkdown(text, filename) : importPlainText(text, filename);
|
|
53
54
|
};
|
|
54
55
|
const isEmpty = (content) => content.kind === "document" ? content.blocks.length === 0 : content.slides.length === 0;
|
|
55
|
-
const importFile = async (data, filename,
|
|
56
|
+
const importFile = async (data, filename, options) => {
|
|
57
|
+
const { maxBytes = MAX_IMPORT_BYTES } = options;
|
|
56
58
|
if (data.length > maxBytes) throw new ConvertError("too-large", `The file is larger than ${maxBytes} bytes`);
|
|
57
59
|
const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
58
60
|
const name = filename.replace(/[\\/]/g, "_").slice(-200);
|
|
59
61
|
const detected = detect(buffer, name);
|
|
60
62
|
if (detected === null) throw new ConvertError("unsupported", `${name} is not a file Vyva can import`);
|
|
61
|
-
const imported = await convert(detected, buffer, name,
|
|
63
|
+
const imported = await convert(detected, buffer, name, options);
|
|
62
64
|
if (isEmpty(imported.content)) throw new ConvertError("empty", `${name} has nothing to import`);
|
|
63
65
|
return imported;
|
|
64
66
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PdfSlides } from "./import/pdf-slides.js";
|
|
2
|
+
//#region src/libreoffice.d.ts
|
|
3
|
+
export interface LibreOfficeOptions {
|
|
4
|
+
command?: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const libreOffice: ({ command, timeoutMs }?: LibreOfficeOptions) => PdfSlides;
|
|
8
|
+
//#endregion
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ConvertError } from "./errors.js";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
//#region src/libreoffice.ts
|
|
9
|
+
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
10
|
+
const run = promisify(execFile);
|
|
11
|
+
const libreOffice = ({ command = "soffice", timeoutMs = DEFAULT_TIMEOUT_MS } = {}) => async (pdf) => {
|
|
12
|
+
const folder = await mkdtemp(join(tmpdir(), "vyva-slides-"));
|
|
13
|
+
const source = join(folder, "source.pdf");
|
|
14
|
+
try {
|
|
15
|
+
await writeFile(source, pdf);
|
|
16
|
+
await run(command, [
|
|
17
|
+
`-env:UserInstallation=${pathToFileURL(join(folder, "profile")).href}`,
|
|
18
|
+
"--headless",
|
|
19
|
+
"--infilter=impress_pdf_import",
|
|
20
|
+
"--convert-to",
|
|
21
|
+
"pptx",
|
|
22
|
+
"--outdir",
|
|
23
|
+
folder,
|
|
24
|
+
source
|
|
25
|
+
], { timeout: timeoutMs }).catch((error) => {
|
|
26
|
+
throw new ConvertError("slides-failed", `LibreOffice at ${command} could not turn the PDF into slides`, { cause: error });
|
|
27
|
+
});
|
|
28
|
+
return new Uint8Array(await readFile(join(folder, "source.pptx")).catch((error) => {
|
|
29
|
+
throw new ConvertError("slides-failed", "LibreOffice finished without writing the slides", { cause: error });
|
|
30
|
+
}));
|
|
31
|
+
} finally {
|
|
32
|
+
await rm(folder, {
|
|
33
|
+
recursive: true,
|
|
34
|
+
force: true
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { libreOffice };
|
package/dist/text-measure.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FontFamily, TextStyle } from "@pollium/vyva-core/content";
|
|
2
2
|
//#region src/text-measure.d.ts
|
|
3
3
|
export declare const textHeight: (html: string, style: TextStyle, font: FontFamily, width: number) => number;
|
|
4
|
+
export declare const unwrappedWidth: (html: string, style: TextStyle, font: FontFamily) => number;
|
|
4
5
|
//#endregion
|
package/dist/text-measure.js
CHANGED
|
@@ -94,5 +94,9 @@ const textHeight = (html, style, font, width) => {
|
|
|
94
94
|
return height + linesOf(block.glyphs, style.fontSize, (width - indent) * WIDTH_SAFETY) * style.fontSize * style.lineHeight + gap;
|
|
95
95
|
}, 0);
|
|
96
96
|
};
|
|
97
|
+
const unwrappedWidth = (html, style, font) => {
|
|
98
|
+
const widths = blocksOf(html, font, style.isBold).map((block) => block.glyphs.reduce((width, glyph) => width + widthOf(glyph, style.fontSize), block.isItem ? LIST_INDENT_EMS * style.fontSize : 0));
|
|
99
|
+
return Math.ceil(Math.max(0, ...widths) / WIDTH_SAFETY) + 1;
|
|
100
|
+
};
|
|
97
101
|
//#endregion
|
|
98
|
-
export { textHeight };
|
|
102
|
+
export { textHeight, unwrappedWidth };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pollium/vyva-convert",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Server-side tools for Vyva content: clean what clients send, import Word, PowerPoint, PDF and Markdown files, and export to PDF, Word, PowerPoint and HTML.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"pptxgenjs": "^4.0.1",
|
|
38
38
|
"sanitize-html": "^2.18.0",
|
|
39
39
|
"unpdf": "^1.8.1",
|
|
40
|
-
"@pollium/vyva-core": "0.
|
|
40
|
+
"@pollium/vyva-core": "0.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^26.6.4",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"build": "tsdown",
|
|
48
48
|
"typecheck": "tsc -p tsconfig.json",
|
|
49
49
|
"lint": "oxlint --deny-warnings",
|
|
50
|
-
"test": "vitest run"
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"font-widths": "node scripts/font-widths.ts"
|
|
51
52
|
}
|
|
52
53
|
}
|