@pollium/vyva-convert 0.3.0 → 0.4.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 +15 -8
- package/dist/export/bundle.d.ts +1 -0
- package/dist/export/bundle.js +10 -4
- package/dist/export/html.js +2 -2
- package/dist/export/pdf-pages.d.ts +5 -0
- package/dist/export/pdf-pages.js +110 -0
- package/dist/export/pdf.js +21 -17
- package/dist/export/pptx.js +5 -5
- package/dist/export.d.ts +2 -1
- package/dist/export.js +5 -5
- package/dist/import/deck-format.d.ts +2 -2
- package/dist/import/documents.d.ts +0 -1
- package/dist/import/documents.js +1 -8
- package/dist/import/image.js +2 -3
- package/dist/import/office.d.ts +1 -1
- package/dist/import/pdf.d.ts +5 -0
- package/dist/import/pdf.js +49 -0
- package/dist/import/plain-theme.d.ts +4 -0
- package/dist/import/plain-theme.js +6 -0
- package/dist/import/pptx.d.ts +0 -1
- package/dist/import/pptx.js +3 -4
- package/dist/import.js +3 -3
- package/dist/libreoffice.js +0 -1
- package/package.json +3 -2
- package/dist/import/pdf-lines.d.ts +0 -4
- package/dist/import/pdf-lines.js +0 -206
- package/dist/import/pdf-slides.d.ts +0 -6
- package/dist/import/pdf-slides.js +0 -22
package/README.md
CHANGED
|
@@ -47,6 +47,9 @@ reply.header('content-disposition', contentDisposition('Subnetting guide', file.
|
|
|
47
47
|
|---|---|
|
|
48
48
|
| Document | `pdf`, `docx`, `html` |
|
|
49
49
|
| Deck | `pptx`, `pdf` |
|
|
50
|
+
| Deck of PDF pages | `pdf` |
|
|
51
|
+
|
|
52
|
+
`exportFormatsOf(content)` lists them, and `canExport(content, format)` checks one.
|
|
50
53
|
|
|
51
54
|
- `readImage` is called once for every image the content uses. Return `null` and the export goes
|
|
52
55
|
on without that image.
|
|
@@ -56,6 +59,9 @@ reply.header('content-disposition', contentDisposition('Subnetting guide', file.
|
|
|
56
59
|
page size in inches) and returns the PDF. Use `gotenberg` or write your own with Puppeteer or
|
|
57
60
|
Playwright.
|
|
58
61
|
- The `html` export is one self-contained file, with its images and fonts inline.
|
|
62
|
+
- A deck of PDF pages exports as the original PDF, read through `readImage`, with what the slides
|
|
63
|
+
add printed on top of each page. Pages with nothing added are copied as they are, and a deck
|
|
64
|
+
nobody drew on never reaches the PDF engine. An encrypted PDF is refused as `unsupported`.
|
|
59
65
|
|
|
60
66
|
## Importing
|
|
61
67
|
|
|
@@ -71,19 +77,20 @@ const { title, content } = await importFile(upload.bytes, upload.filename, {
|
|
|
71
77
|
are told apart by what their zip holds, Word 97 and PowerPoint 97 files by the streams they hold,
|
|
72
78
|
and PDF, RTF, PNG, JPEG, GIF and WebP by their signatures. Only Markdown (`.md`) and text
|
|
73
79
|
(`.txt`) are recognized by their extension, and only when they are valid UTF-8.
|
|
74
|
-
- Word,
|
|
80
|
+
- Word, Markdown and text files become documents, and PowerPoint files become decks. A deck
|
|
75
81
|
keeps its theme colors and the font most of its text uses. What its slide master draws comes in
|
|
76
82
|
locked, behind the slide's own elements, and freeform shapes come in as polygons.
|
|
83
|
+
- A PDF becomes a deck of its own pages: one slide for each page, at the size of the first page,
|
|
84
|
+
with that page behind it. Nothing is converted, so it looks exactly like the original.
|
|
85
|
+
`saveImage` stores the PDF once, as `application/pdf`; return `null` and the import is refused as
|
|
86
|
+
`empty`. A PDF Vyva cannot open is refused as `unsupported`, and one of more than 1000 pages as
|
|
87
|
+
`too-large`.
|
|
77
88
|
- A picture becomes a deck of one slide, in the format closest to its shape, with the picture
|
|
78
89
|
filling it. `saveImage` stores it like any other image; return `null` and the import is refused
|
|
79
90
|
as `empty`.
|
|
80
|
-
- With `officeEngine`, Vyva reads what it cannot read alone.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
refused as `too-large` before the engine runs. Word 97, OpenDocument text and RTF files are
|
|
84
|
-
turned into Word, and PowerPoint 97 and OpenDocument presentations into PowerPoint, then imported
|
|
85
|
-
as those. Without an engine a PDF becomes a document of its text, and the older formats are
|
|
86
|
-
refused as `unsupported`.
|
|
91
|
+
- With `officeEngine`, Vyva reads what it cannot read alone. Word 97, OpenDocument text and RTF
|
|
92
|
+
files are turned into Word, and PowerPoint 97 and OpenDocument presentations into PowerPoint,
|
|
93
|
+
then imported as those. Without an engine those formats are refused as `unsupported`.
|
|
87
94
|
|
|
88
95
|
`officeEngine` takes `{ file, from, to }` and returns the converted file. `libreOffice()` from
|
|
89
96
|
`@pollium/vyva-convert/libreoffice` does that with the `soffice` on the machine (`command` changes
|
package/dist/export/bundle.d.ts
CHANGED
package/dist/export/bundle.js
CHANGED
|
@@ -8,6 +8,7 @@ const EXTENSIONS = {
|
|
|
8
8
|
"image/gif": "gif",
|
|
9
9
|
"image/webp": "webp"
|
|
10
10
|
};
|
|
11
|
+
const PDF_TYPE = "application/pdf";
|
|
11
12
|
const fontsOf = (content) => {
|
|
12
13
|
if (content.kind === "document") return [content.design.font, CODE_FONT];
|
|
13
14
|
return content.slides.some((slide) => slide.elements.some((element) => element.type === "code")) ? [content.theme.font, CODE_FONT] : [content.theme.font];
|
|
@@ -27,13 +28,18 @@ const bundleImage = (id, stored) => {
|
|
|
27
28
|
const bundleOf = async (content, { title, language, readImage }) => {
|
|
28
29
|
const ids = fileIdsOf(content);
|
|
29
30
|
const stored = await Promise.all(ids.map((id) => readImage(id)));
|
|
31
|
+
const found = ids.flatMap((id, index) => {
|
|
32
|
+
const file = stored[index];
|
|
33
|
+
return file === null ? [] : [{
|
|
34
|
+
id,
|
|
35
|
+
file
|
|
36
|
+
}];
|
|
37
|
+
});
|
|
30
38
|
return {
|
|
31
39
|
title,
|
|
32
40
|
language,
|
|
33
|
-
images: new Map(
|
|
34
|
-
|
|
35
|
-
return image === null ? [] : [[id, bundleImage(id, image)]];
|
|
36
|
-
})),
|
|
41
|
+
images: new Map(found.filter(({ file }) => file.contentType !== PDF_TYPE).map(({ id, file }) => [id, bundleImage(id, file)])),
|
|
42
|
+
pdfs: new Map(found.filter(({ file }) => file.contentType === PDF_TYPE).map(({ id, file }) => [id, Buffer.from(file.data)])),
|
|
37
43
|
fonts: fontsOf(content)
|
|
38
44
|
};
|
|
39
45
|
};
|
package/dist/export/html.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fontFaces, fontFilesOf } from "./fonts.js";
|
|
2
|
-
import {
|
|
2
|
+
import { deckSize } from "@pollium/vyva-core/content";
|
|
3
3
|
import { CONTENT_CSS, renderDocument, renderSlide } from "@pollium/vyva-core/render";
|
|
4
4
|
import { escapeHtml } from "@pollium/vyva-core/escape";
|
|
5
5
|
//#region src/export/html.ts
|
|
@@ -9,7 +9,7 @@ const SCREEN_CSS = "body{margin:0;background:#f3f4f6;display:flex;justify-conten
|
|
|
9
9
|
const dataUrl = (contentType, data) => `data:${contentType};base64,${data.toString("base64")}`;
|
|
10
10
|
const bodyOf = (content, fileUrl) => {
|
|
11
11
|
if (content.kind === "document") return renderDocument(content, fileUrl);
|
|
12
|
-
const { width, height } =
|
|
12
|
+
const { width, height } = deckSize(content.format);
|
|
13
13
|
const zoom = SCREEN_PX_PER_INCH / SLIDE_PX_PER_INCH;
|
|
14
14
|
return content.slides.map((slide) => `<div class="vy-print-slide" style="width:${width * zoom}px;height:${height * zoom}px"><div style="width:${width}px;height:${height}px;transform:scale(${zoom});transform-origin:0 0">${renderSlide(slide, content, fileUrl)}</div></div>`).join("");
|
|
15
15
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ExportBundle } from "./bundle.js";
|
|
2
|
+
import { DeckContent } from "@pollium/vyva-core/content";
|
|
3
|
+
//#region src/export/pdf-pages.d.ts
|
|
4
|
+
export declare const onPdfPages: (deck: DeckContent, bundle: ExportBundle, print: () => Promise<Uint8Array>) => Promise<Uint8Array>;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ConvertError } from "../errors.js";
|
|
2
|
+
import { frameLayers } from "@pollium/vyva-core/deck-frame";
|
|
3
|
+
import { PDFDocument, concatTransformationMatrix, drawObject, popGraphicsState, pushGraphicsState } from "pdf-lib";
|
|
4
|
+
//#region src/export/pdf-pages.ts
|
|
5
|
+
const pdfPageOf = (slide) => slide.background?.type === "pdf-page" ? slide.background : null;
|
|
6
|
+
const drawsOver = (slide, deck) => {
|
|
7
|
+
const { under, over } = frameLayers(slide, deck);
|
|
8
|
+
return slide.elements.length > 0 || under !== "" || over !== "";
|
|
9
|
+
};
|
|
10
|
+
const loaded = (data, what) => PDFDocument.load(data).catch((error) => {
|
|
11
|
+
throw new ConvertError("unsupported", `${what} cannot be copied into the exported PDF`, { cause: error });
|
|
12
|
+
});
|
|
13
|
+
const placement = (page, overlay) => {
|
|
14
|
+
const { x, y, width, height } = page.getCropBox();
|
|
15
|
+
const rotation = (page.getRotation().angle % 360 + 360) % 360;
|
|
16
|
+
const isTurned = rotation === 90 || rotation === 270;
|
|
17
|
+
const visibleWidth = isTurned ? height : width;
|
|
18
|
+
const visibleHeight = isTurned ? width : height;
|
|
19
|
+
const scale = Math.min(visibleWidth / overlay.width, visibleHeight / overlay.height);
|
|
20
|
+
const left = (visibleWidth - overlay.width * scale) / 2;
|
|
21
|
+
const top = (visibleHeight - overlay.height * scale) / 2;
|
|
22
|
+
const tall = overlay.height * scale;
|
|
23
|
+
if (rotation === 90) return [
|
|
24
|
+
0,
|
|
25
|
+
scale,
|
|
26
|
+
-scale,
|
|
27
|
+
0,
|
|
28
|
+
x + top + tall,
|
|
29
|
+
y + left
|
|
30
|
+
];
|
|
31
|
+
if (rotation === 180) return [
|
|
32
|
+
-scale,
|
|
33
|
+
0,
|
|
34
|
+
0,
|
|
35
|
+
-scale,
|
|
36
|
+
x + width - left,
|
|
37
|
+
y + top + tall
|
|
38
|
+
];
|
|
39
|
+
if (rotation === 270) return [
|
|
40
|
+
0,
|
|
41
|
+
-scale,
|
|
42
|
+
scale,
|
|
43
|
+
0,
|
|
44
|
+
x + width - top - tall,
|
|
45
|
+
y + height - left
|
|
46
|
+
];
|
|
47
|
+
return [
|
|
48
|
+
scale,
|
|
49
|
+
0,
|
|
50
|
+
0,
|
|
51
|
+
scale,
|
|
52
|
+
x + left,
|
|
53
|
+
y + height - top - tall
|
|
54
|
+
];
|
|
55
|
+
};
|
|
56
|
+
const stamp = (page, overlay) => {
|
|
57
|
+
const { context } = page.doc;
|
|
58
|
+
page.node.normalize();
|
|
59
|
+
page.node.wrapContentStreams(context.register(context.contentStream([pushGraphicsState()])), context.register(context.contentStream([popGraphicsState()])));
|
|
60
|
+
const name = page.node.newXObject("VyvaOverlay", overlay.ref);
|
|
61
|
+
page.pushOperators(pushGraphicsState(), concatTransformationMatrix(...placement(page, overlay)), drawObject(name), popGraphicsState());
|
|
62
|
+
};
|
|
63
|
+
const sourcePagesOf = (deck, pdfs) => deck.slides.flatMap((slide, index) => {
|
|
64
|
+
const background = pdfPageOf(slide);
|
|
65
|
+
return background !== null && pdfs.has(background.fileId) ? [{
|
|
66
|
+
slide: index,
|
|
67
|
+
fileId: background.fileId,
|
|
68
|
+
page: background.page
|
|
69
|
+
}] : [];
|
|
70
|
+
});
|
|
71
|
+
const copySources = async (output, wanted, pdfs) => {
|
|
72
|
+
const placed = [];
|
|
73
|
+
for (const [fileId, data] of pdfs) {
|
|
74
|
+
const pages = wanted.filter((source) => source.fileId === fileId);
|
|
75
|
+
if (pages.length === 0) continue;
|
|
76
|
+
const source = await loaded(data, `The PDF ${fileId}`);
|
|
77
|
+
if (pages.some(({ page }) => page > source.getPageCount())) throw new ConvertError("unsupported", `The PDF ${fileId} has fewer pages than the deck shows`);
|
|
78
|
+
const copied = await output.copyPages(source, pages.map(({ page }) => page - 1));
|
|
79
|
+
placed.push(...copied.map((page, at) => ({
|
|
80
|
+
slide: pages[at].slide,
|
|
81
|
+
page
|
|
82
|
+
})));
|
|
83
|
+
}
|
|
84
|
+
return placed;
|
|
85
|
+
};
|
|
86
|
+
const copyPrinted = async (output, printed, slides) => {
|
|
87
|
+
return (await output.copyPages(printed, slides)).map((page, at) => ({
|
|
88
|
+
slide: slides[at],
|
|
89
|
+
page
|
|
90
|
+
}));
|
|
91
|
+
};
|
|
92
|
+
const stampAll = async (output, printed, sources, deck) => {
|
|
93
|
+
const stamped = sources.filter(({ slide }) => drawsOver(deck.slides[slide], deck));
|
|
94
|
+
const overlays = await output.embedPages(stamped.map(({ slide }) => printed.getPage(slide)));
|
|
95
|
+
stamped.forEach(({ page }, index) => stamp(page, overlays[index]));
|
|
96
|
+
};
|
|
97
|
+
const onPdfPages = async (deck, bundle, print) => {
|
|
98
|
+
const wanted = sourcePagesOf(deck, bundle.pdfs);
|
|
99
|
+
const fromSource = new Set(wanted.map(({ slide }) => slide));
|
|
100
|
+
const printed = deck.slides.some((slide, index) => !fromSource.has(index) || drawsOver(slide, deck)) ? await loaded(await print(), "The printed slides") : null;
|
|
101
|
+
const output = await PDFDocument.create();
|
|
102
|
+
output.setTitle(bundle.title);
|
|
103
|
+
const sources = await copySources(output, wanted, bundle.pdfs);
|
|
104
|
+
const others = printed === null ? [] : await copyPrinted(output, printed, deck.slides.map((_, index) => index).filter((index) => !fromSource.has(index)));
|
|
105
|
+
if (printed !== null) await stampAll(output, printed, sources, deck);
|
|
106
|
+
for (const { page } of [...sources, ...others].sort((a, b) => a.slide - b.slide)) output.addPage(page);
|
|
107
|
+
return output.save();
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
110
|
+
export { onPdfPages };
|
package/dist/export/pdf.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fontFaces, fontFilesOf } from "./fonts.js";
|
|
2
2
|
import { bodyOf, pageHtml } from "./html.js";
|
|
3
|
-
import {
|
|
3
|
+
import { onPdfPages } from "./pdf-pages.js";
|
|
4
|
+
import { deckSize, hasPdfPages } from "@pollium/vyva-core/content";
|
|
4
5
|
import { CONTENT_CSS } from "@pollium/vyva-core/render";
|
|
5
6
|
import { escapeHtml } from "@pollium/vyva-core/escape";
|
|
6
7
|
//#region src/export/pdf.ts
|
|
@@ -31,7 +32,7 @@ html,body{margin:0;padding:0;-webkit-print-color-adjust:exact;print-color-adjust
|
|
|
31
32
|
const inches = (mm) => mm / MM_PER_INCH;
|
|
32
33
|
const pageOf = (content) => {
|
|
33
34
|
if (content.kind === "deck") {
|
|
34
|
-
const { width, height } =
|
|
35
|
+
const { width, height } = deckSize(content.format);
|
|
35
36
|
return {
|
|
36
37
|
width: width / SLIDE_PX_PER_INCH,
|
|
37
38
|
height: height / SLIDE_PX_PER_INCH,
|
|
@@ -58,24 +59,27 @@ const footerOf = (content) => {
|
|
|
58
59
|
const side = (mm) => `${inches(mm).toFixed(3)}in`;
|
|
59
60
|
return `<html><head><style>.vy-page-footer{box-sizing:border-box;display:flex;justify-content:space-between;width:100%;padding:0 ${side(design.margins.right)} 0 ${side(design.margins.left)};font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:8pt;color:#6b7280}</style></head><body><div class="vy-page-footer"><span>${text}</span>${number}</div></body></html>`;
|
|
60
61
|
};
|
|
61
|
-
const
|
|
62
|
+
const print = async (content, bundle, engine) => {
|
|
62
63
|
const fonts = await fontFilesOf(bundle.fonts);
|
|
63
64
|
const css = `${fontFaces(fonts, (file) => file.name)}${CONTENT_CSS}${PRINT_CSS}`;
|
|
65
|
+
return engine({
|
|
66
|
+
html: pageHtml(bundle, css, bodyOf(content, (id) => bundle.images.get(id)?.name ?? null)),
|
|
67
|
+
files: [...[...bundle.images.values()].map(({ name, data, contentType }) => ({
|
|
68
|
+
name,
|
|
69
|
+
data,
|
|
70
|
+
contentType
|
|
71
|
+
})), ...fonts.map(({ name, data }) => ({
|
|
72
|
+
name,
|
|
73
|
+
data,
|
|
74
|
+
contentType: "font/woff2"
|
|
75
|
+
}))],
|
|
76
|
+
footerHtml: footerOf(content),
|
|
77
|
+
page: pageOf(content)
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
const buildPdf = async (content, bundle, engine) => {
|
|
64
81
|
return {
|
|
65
|
-
data: await engine(
|
|
66
|
-
html: pageHtml(bundle, css, bodyOf(content, (id) => bundle.images.get(id)?.name ?? null)),
|
|
67
|
-
files: [...[...bundle.images.values()].map(({ name, data, contentType }) => ({
|
|
68
|
-
name,
|
|
69
|
-
data,
|
|
70
|
-
contentType
|
|
71
|
-
})), ...fonts.map(({ name, data }) => ({
|
|
72
|
-
name,
|
|
73
|
-
data,
|
|
74
|
-
contentType: "font/woff2"
|
|
75
|
-
}))],
|
|
76
|
-
footerHtml: footerOf(content),
|
|
77
|
-
page: pageOf(content)
|
|
78
|
-
}),
|
|
82
|
+
data: content.kind === "deck" && hasPdfPages(content) ? await onPdfPages(content, bundle, () => print(content, bundle, engine)) : await print(content, bundle, engine),
|
|
79
83
|
contentType: "application/pdf",
|
|
80
84
|
extension: "pdf"
|
|
81
85
|
};
|
package/dist/export/pptx.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { blocksOfHtml } from "./html-blocks.js";
|
|
2
2
|
import { CODE_FONT, CODE_LINE_HEIGHT, CODE_PALETTES, codeLines } from "@pollium/vyva-core/code";
|
|
3
|
-
import {
|
|
3
|
+
import { TEXT_SHAPES, deckSize } from "@pollium/vyva-core/content";
|
|
4
4
|
import { bandHeight, footerHeight, footerSegments, hasBand, hasFooter, inkOn } from "@pollium/vyva-core/deck-frame";
|
|
5
5
|
import PptxGenJS from "pptxgenjs";
|
|
6
6
|
//#region src/export/pptx.ts
|
|
@@ -250,7 +250,7 @@ const addBackground = (slide, background, deck, bundle) => {
|
|
|
250
250
|
if (image !== void 0) {
|
|
251
251
|
slide.background = { data: dataUrl(image.contentType, image.data) };
|
|
252
252
|
if (background.dim > 0) {
|
|
253
|
-
const { width, height } =
|
|
253
|
+
const { width, height } = deckSize(deck.format);
|
|
254
254
|
slide.addShape("rect", {
|
|
255
255
|
x: 0,
|
|
256
256
|
y: 0,
|
|
@@ -270,7 +270,7 @@ const addBackground = (slide, background, deck, bundle) => {
|
|
|
270
270
|
};
|
|
271
271
|
const addBand = (slide, source, deck) => {
|
|
272
272
|
if (!hasBand(deck.theme, source)) return;
|
|
273
|
-
const { width } =
|
|
273
|
+
const { width } = deckSize(deck.format);
|
|
274
274
|
slide.addShape("rect", {
|
|
275
275
|
x: 0,
|
|
276
276
|
y: 0,
|
|
@@ -282,7 +282,7 @@ const addBand = (slide, source, deck) => {
|
|
|
282
282
|
};
|
|
283
283
|
const addFooter = (slide, source, deck) => {
|
|
284
284
|
if (!hasFooter(deck.theme, source)) return;
|
|
285
|
-
const { width, height } =
|
|
285
|
+
const { width, height } = deckSize(deck.format);
|
|
286
286
|
const tall = footerHeight(deck.format);
|
|
287
287
|
const third = width / 3;
|
|
288
288
|
footerSegments(source, deck).forEach((segment, index) => {
|
|
@@ -302,7 +302,7 @@ const addFooter = (slide, source, deck) => {
|
|
|
302
302
|
});
|
|
303
303
|
};
|
|
304
304
|
const buildPptx = async (deck, bundle) => {
|
|
305
|
-
const { width, height } =
|
|
305
|
+
const { width, height } = deckSize(deck.format);
|
|
306
306
|
const pptx = new PptxGenJS();
|
|
307
307
|
pptx.defineLayout({
|
|
308
308
|
name: LAYOUT,
|
package/dist/export.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface ExportOptions {
|
|
|
14
14
|
language?: string;
|
|
15
15
|
}
|
|
16
16
|
export declare const isExportFormat: (value: unknown) => value is ExportFormat;
|
|
17
|
-
export declare const
|
|
17
|
+
export declare const exportFormatsOf: (content: Content) => readonly ExportFormat[];
|
|
18
|
+
export declare const canExport: (content: Content, format: ExportFormat) => boolean;
|
|
18
19
|
export declare const exportContent: (content: Content, format: ExportFormat, { title, readImage, pdfEngine, language }: ExportOptions) => Promise<ExportedFile>;
|
|
19
20
|
//#endregion
|
package/dist/export.js
CHANGED
|
@@ -4,6 +4,7 @@ import { buildDocx } from "./export/docx.js";
|
|
|
4
4
|
import { buildHtml } from "./export/html.js";
|
|
5
5
|
import { buildPdf } from "./export/pdf.js";
|
|
6
6
|
import { buildPptx } from "./export/pptx.js";
|
|
7
|
+
import { hasPdfPages } from "@pollium/vyva-core/content";
|
|
7
8
|
//#region src/export.ts
|
|
8
9
|
const EXPORT_FORMATS = {
|
|
9
10
|
document: [
|
|
@@ -16,15 +17,14 @@ const EXPORT_FORMATS = {
|
|
|
16
17
|
const DEFAULT_LANGUAGE = "en";
|
|
17
18
|
const FORMATS = new Set(Object.values(EXPORT_FORMATS).flat());
|
|
18
19
|
const isExportFormat = (value) => typeof value === "string" && FORMATS.has(value);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
};
|
|
20
|
+
const exportFormatsOf = (content) => hasPdfPages(content) ? ["pdf"] : EXPORT_FORMATS[content.kind];
|
|
21
|
+
const canExport = (content, format) => exportFormatsOf(content).includes(format);
|
|
22
22
|
const engineOf = (engine) => {
|
|
23
23
|
if (engine === void 0) throw new TypeError("Exporting as pdf needs a pdfEngine");
|
|
24
24
|
return engine;
|
|
25
25
|
};
|
|
26
26
|
const exportContent = async (content, format, { title, readImage, pdfEngine, language = DEFAULT_LANGUAGE }) => {
|
|
27
|
-
if (!canExport(content
|
|
27
|
+
if (!canExport(content, format)) throw new ConvertError("unsupported", `This ${content.kind} cannot be exported as ${format}`);
|
|
28
28
|
const engine = format === "pdf" ? engineOf(pdfEngine) : null;
|
|
29
29
|
const bundle = await bundleOf(content, {
|
|
30
30
|
title,
|
|
@@ -36,4 +36,4 @@ const exportContent = async (content, format, { title, readImage, pdfEngine, lan
|
|
|
36
36
|
return content.kind === "document" ? buildDocx(content, bundle) : buildPptx(content, bundle);
|
|
37
37
|
};
|
|
38
38
|
//#endregion
|
|
39
|
-
export { EXPORT_FORMATS, canExport, exportContent, isExportFormat };
|
|
39
|
+
export { EXPORT_FORMATS, canExport, exportContent, exportFormatsOf, isExportFormat };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DeckPreset } from "@pollium/vyva-core/content";
|
|
2
2
|
//#region src/import/deck-format.d.ts
|
|
3
|
-
export declare const formatFor: (width: number, height: number) =>
|
|
3
|
+
export declare const formatFor: (width: number, height: number) => DeckPreset;
|
|
4
4
|
//#endregion
|
|
@@ -8,5 +8,4 @@ export interface ImportedDocument {
|
|
|
8
8
|
export declare const importDocx: (buffer: Buffer, filename: string, saveImage: SaveImage) => Promise<ImportedDocument>;
|
|
9
9
|
export declare const importMarkdown: (text: string, filename: string) => ImportedDocument;
|
|
10
10
|
export declare const importPlainText: (text: string, filename: string) => ImportedDocument;
|
|
11
|
-
export declare const importPdf: (buffer: Buffer, filename: string) => Promise<ImportedDocument>;
|
|
12
11
|
//#endregion
|
package/dist/import/documents.js
CHANGED
|
@@ -3,10 +3,8 @@ import { imageSaver } from "./images.js";
|
|
|
3
3
|
import { markdownToBlocks } from "@pollium/vyva-core/markdown-blocks";
|
|
4
4
|
import { DEFAULT_DOCUMENT_DESIGN } from "@pollium/vyva-core/themes";
|
|
5
5
|
import mammoth from "mammoth";
|
|
6
|
-
import { extractText, getDocumentProxy } from "unpdf";
|
|
7
6
|
//#region src/import/documents.ts
|
|
8
7
|
const MAX_IMAGES = 60;
|
|
9
|
-
const PAGE_BREAK = "<div data-type=\"page-break\"></div>";
|
|
10
8
|
const documentOf = (blocks, filename) => ({
|
|
11
9
|
title: titleFrom(blocks, filename),
|
|
12
10
|
content: {
|
|
@@ -35,10 +33,5 @@ const importDocx = async (buffer, filename, saveImage) => {
|
|
|
35
33
|
};
|
|
36
34
|
const importMarkdown = (text, filename) => documentOf(markdownToBlocks(text), filename);
|
|
37
35
|
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
36
|
//#endregion
|
|
44
|
-
export { importDocx, importMarkdown,
|
|
37
|
+
export { importDocx, importMarkdown, importPlainText };
|
package/dist/import/image.js
CHANGED
|
@@ -3,9 +3,9 @@ import { imageSize } from "../export/image-size.js";
|
|
|
3
3
|
import { titleOf } from "./titles.js";
|
|
4
4
|
import { imageSaver } from "./images.js";
|
|
5
5
|
import { formatFor } from "./deck-format.js";
|
|
6
|
+
import { PLAIN_THEME } from "./plain-theme.js";
|
|
6
7
|
import { DECK_SIZES } from "@pollium/vyva-core/content";
|
|
7
8
|
import { newId } from "@pollium/vyva-core/ids";
|
|
8
|
-
import { DECK_THEME } from "@pollium/vyva-core/themes";
|
|
9
9
|
//#region src/import/image.ts
|
|
10
10
|
const PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
|
|
11
11
|
const JPEG_SIGNATURE = Buffer.from("ffd8ff", "hex");
|
|
@@ -25,13 +25,12 @@ const importImage = async (buffer, filename, contentType, saveImage) => {
|
|
|
25
25
|
const scale = Math.min(slide.width / size.width, slide.height / size.height);
|
|
26
26
|
const w = Math.round(size.width * scale);
|
|
27
27
|
const h = Math.round(size.height * scale);
|
|
28
|
-
const { frame: _frame, ...theme } = DECK_THEME;
|
|
29
28
|
return {
|
|
30
29
|
title: titleOf("", filename),
|
|
31
30
|
content: {
|
|
32
31
|
kind: "deck",
|
|
33
32
|
format,
|
|
34
|
-
theme,
|
|
33
|
+
theme: PLAIN_THEME,
|
|
35
34
|
slides: [{
|
|
36
35
|
id: newId(),
|
|
37
36
|
background: null,
|
package/dist/import/office.d.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ConvertError } from "../errors.js";
|
|
2
|
+
import { titleOf } from "./titles.js";
|
|
3
|
+
import { PLAIN_THEME } from "./plain-theme.js";
|
|
4
|
+
import { MAX_EXTENT, MAX_SLIDES } from "@pollium/vyva-core/content";
|
|
5
|
+
import { newId } from "@pollium/vyva-core/ids";
|
|
6
|
+
import { getDocumentProxy } from "unpdf";
|
|
7
|
+
//#region src/import/pdf.ts
|
|
8
|
+
const PX_PER_POINT = 2;
|
|
9
|
+
const PDF_TYPE = "application/pdf";
|
|
10
|
+
const slideSizeOf = (width, height) => {
|
|
11
|
+
const scale = Math.min(PX_PER_POINT, MAX_EXTENT / Math.max(width, height));
|
|
12
|
+
return {
|
|
13
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
14
|
+
height: Math.max(1, Math.round(height * scale))
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
const opened = (buffer, filename) => getDocumentProxy(new Uint8Array(buffer)).catch((error) => {
|
|
18
|
+
throw new ConvertError("unsupported", `${filename} is a PDF Vyva cannot open`, { cause: error });
|
|
19
|
+
});
|
|
20
|
+
const importPdf = async (buffer, filename, saveImage) => {
|
|
21
|
+
const pdf = await opened(buffer, filename);
|
|
22
|
+
if (pdf.numPages > MAX_SLIDES) throw new ConvertError("too-large", `${filename} has ${pdf.numPages} pages, more than the ${MAX_SLIDES} slides a deck holds`);
|
|
23
|
+
const { width, height } = (await pdf.getPage(1)).getViewport({ scale: 1 });
|
|
24
|
+
const fileId = await saveImage({
|
|
25
|
+
name: filename,
|
|
26
|
+
data: buffer,
|
|
27
|
+
contentType: PDF_TYPE
|
|
28
|
+
});
|
|
29
|
+
if (fileId === null) throw new ConvertError("empty", `${filename} was not stored, so there is nothing to show`);
|
|
30
|
+
return {
|
|
31
|
+
title: titleOf("", filename),
|
|
32
|
+
content: {
|
|
33
|
+
kind: "deck",
|
|
34
|
+
format: slideSizeOf(width, height),
|
|
35
|
+
theme: PLAIN_THEME,
|
|
36
|
+
slides: Array.from({ length: pdf.numPages }, (_, index) => ({
|
|
37
|
+
id: newId(),
|
|
38
|
+
background: {
|
|
39
|
+
type: "pdf-page",
|
|
40
|
+
fileId,
|
|
41
|
+
page: index + 1
|
|
42
|
+
},
|
|
43
|
+
elements: []
|
|
44
|
+
}))
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { importPdf };
|
package/dist/import/pptx.d.ts
CHANGED
package/dist/import/pptx.js
CHANGED
|
@@ -6,7 +6,7 @@ import { all, attr, first, num, textOf } from "./pptx-xml.js";
|
|
|
6
6
|
import { familyOf } from "./pptx-theme.js";
|
|
7
7
|
import PptxPackage from "./pptx-package.js";
|
|
8
8
|
import SlideReader from "./pptx-slide.js";
|
|
9
|
-
import { DECK_SIZES } from "@pollium/vyva-core/content";
|
|
9
|
+
import { DECK_SIZES, MAX_SLIDES } from "@pollium/vyva-core/content";
|
|
10
10
|
import { unzipSync } from "fflate";
|
|
11
11
|
import { posix } from "node:path";
|
|
12
12
|
//#region src/import/pptx.ts
|
|
@@ -14,7 +14,6 @@ const EMU_PER_INCH = 914400;
|
|
|
14
14
|
const PX_PER_INCH = 144;
|
|
15
15
|
const DEFAULT_WIDTH = 12192e3;
|
|
16
16
|
const DEFAULT_HEIGHT = 6858e3;
|
|
17
|
-
const MAX_SLIDES = 300;
|
|
18
17
|
const MAX_IMAGES = 80;
|
|
19
18
|
const MAX_UNZIPPED_BYTES = 125829120;
|
|
20
19
|
const IMAGE_TYPES = {
|
|
@@ -57,7 +56,7 @@ const importPptx = async (buffer, filename, saveImage) => {
|
|
|
57
56
|
const cy = num(attr(size, "cy"), DEFAULT_HEIGHT);
|
|
58
57
|
const format = formatFor(cx, cy);
|
|
59
58
|
const relations = pptx.relationsOf("ppt/presentation.xml");
|
|
60
|
-
const allParts = all(presentation, "p:sldId").map((slide) => relations[attr(slide, "r:id") ?? ""]?.target).filter((path) => path !== void 0).slice(0,
|
|
59
|
+
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);
|
|
61
60
|
const themed = allParts[0]?.slide.palette.deckTheme();
|
|
62
61
|
if (themed === void 0) throw new ConvertError("empty", `${filename} has no slides`);
|
|
63
62
|
const theme = {
|
|
@@ -98,4 +97,4 @@ const importPptx = async (buffer, filename, saveImage) => {
|
|
|
98
97
|
};
|
|
99
98
|
};
|
|
100
99
|
//#endregion
|
|
101
|
-
export {
|
|
100
|
+
export { importPptx };
|
package/dist/import.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ConvertError } from "./errors.js";
|
|
2
|
-
import { importDocx, importMarkdown,
|
|
2
|
+
import { importDocx, importMarkdown, importPlainText } from "./import/documents.js";
|
|
3
3
|
import { imageTypeOf, importImage } from "./import/image.js";
|
|
4
4
|
import { runOffice } from "./import/office.js";
|
|
5
|
+
import { importPdf } from "./import/pdf.js";
|
|
5
6
|
import { importPptx } from "./import/pptx.js";
|
|
6
|
-
import { importPdfSlides } from "./import/pdf-slides.js";
|
|
7
7
|
import { strFromU8, unzipSync } from "fflate";
|
|
8
8
|
//#region src/import.ts
|
|
9
9
|
const MAX_IMPORT_BYTES = 26214400;
|
|
@@ -87,7 +87,7 @@ const convert = async (detected, buffer, filename, options) => {
|
|
|
87
87
|
switch (detected) {
|
|
88
88
|
case "docx": return importDocx(buffer, filename, saveImage);
|
|
89
89
|
case "pptx": return importPptx(buffer, filename, saveImage);
|
|
90
|
-
case "pdf": return
|
|
90
|
+
case "pdf": return importPdf(buffer, filename, saveImage);
|
|
91
91
|
case "markdown": return importMarkdown(textOf(buffer), filename);
|
|
92
92
|
case "text": return importPlainText(textOf(buffer), filename);
|
|
93
93
|
case "image/png":
|
package/dist/libreoffice.js
CHANGED
|
@@ -16,7 +16,6 @@ const libreOffice = ({ command = "soffice", timeoutMs = DEFAULT_TIMEOUT_MS } = {
|
|
|
16
16
|
await run(command, [
|
|
17
17
|
`-env:UserInstallation=${pathToFileURL(join(folder, "profile")).href}`,
|
|
18
18
|
"--headless",
|
|
19
|
-
...from === "pdf" ? ["--infilter=impress_pdf_import"] : [],
|
|
20
19
|
"--convert-to",
|
|
21
20
|
to,
|
|
22
21
|
"--outdir",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pollium/vyva-convert",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Server-side tools for Vyva content: clean what clients send, import Word, PowerPoint, PDF, OpenDocument, Markdown and image files, and export to PDF, Word, PowerPoint and HTML.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -34,10 +34,11 @@
|
|
|
34
34
|
"fflate": "^0.8.3",
|
|
35
35
|
"htmlparser2": "^10.1.0",
|
|
36
36
|
"mammoth": "^1.13.0",
|
|
37
|
+
"pdf-lib": "1.17.1",
|
|
37
38
|
"pptxgenjs": "^4.0.1",
|
|
38
39
|
"sanitize-html": "^2.18.0",
|
|
39
40
|
"unpdf": "^1.8.1",
|
|
40
|
-
"@pollium/vyva-core": "0.
|
|
41
|
+
"@pollium/vyva-core": "0.3.0"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/node": "^26.6.4",
|
package/dist/import/pdf-lines.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
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 && right(block.lines[0].element) <= slideWidth - SLIDE_MARGIN) 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 };
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { SaveImage } from "./images.js";
|
|
2
|
-
import { OfficeEngine } from "./office.js";
|
|
3
|
-
import { ImportedDeck } from "./pptx.js";
|
|
4
|
-
//#region src/import/pdf-slides.d.ts
|
|
5
|
-
export declare const importPdfSlides: (buffer: Buffer, filename: string, saveImage: SaveImage, officeEngine: OfficeEngine) => Promise<ImportedDeck>;
|
|
6
|
-
//#endregion
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { ConvertError } from "../errors.js";
|
|
2
|
-
import { runOffice } from "./office.js";
|
|
3
|
-
import { joinLines } from "./pdf-lines.js";
|
|
4
|
-
import { importPptx } from "./pptx.js";
|
|
5
|
-
import { getDocumentProxy } from "unpdf";
|
|
6
|
-
//#region src/import/pdf-slides.ts
|
|
7
|
-
const importPdfSlides = async (buffer, filename, saveImage, officeEngine) => {
|
|
8
|
-
const { numPages } = await getDocumentProxy(new Uint8Array(buffer));
|
|
9
|
-
if (numPages > 300) throw new ConvertError("too-large", `${filename} has ${numPages} pages, more than the 300 slides a deck holds`);
|
|
10
|
-
const pptx = await runOffice(officeEngine, {
|
|
11
|
-
file: buffer,
|
|
12
|
-
from: "pdf",
|
|
13
|
-
to: "pptx"
|
|
14
|
-
}, filename);
|
|
15
|
-
const { title, content } = await importPptx(pptx, filename, saveImage);
|
|
16
|
-
return {
|
|
17
|
-
title,
|
|
18
|
-
content: joinLines(content)
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
//#endregion
|
|
22
|
-
export { importPdfSlides };
|