@pollium/vyva-convert 0.2.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 +30 -12
- package/dist/errors.d.ts +1 -1
- 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 +4 -0
- package/dist/import/deck-format.js +12 -0
- package/dist/import/documents.d.ts +0 -1
- package/dist/import/documents.js +1 -8
- package/dist/import/image.d.ts +7 -0
- package/dist/import/image.js +58 -0
- package/dist/import/office.d.ts +10 -0
- package/dist/import/office.js +10 -0
- 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.js +2 -28
- package/dist/import.d.ts +2 -2
- package/dist/import.js +65 -16
- package/dist/libreoffice.d.ts +2 -2
- package/dist/libreoffice.js +8 -9
- package/package.json +4 -3
- 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 -7
- package/dist/import/pdf-slides.js +0 -21
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @pollium/vyva-convert
|
|
2
2
|
|
|
3
3
|
Server-side tools for Vyva content. It cleans what clients send, imports Word, PowerPoint, PDF,
|
|
4
|
-
Markdown and
|
|
4
|
+
OpenDocument, RTF, Markdown, text and image files, and exports to PDF, Word, PowerPoint and HTML.
|
|
5
|
+
It runs in Node.
|
|
5
6
|
|
|
6
7
|
```sh
|
|
7
8
|
npm install @pollium/vyva-convert
|
|
@@ -46,6 +47,9 @@ reply.header('content-disposition', contentDisposition('Subnetting guide', file.
|
|
|
46
47
|
|---|---|
|
|
47
48
|
| Document | `pdf`, `docx`, `html` |
|
|
48
49
|
| Deck | `pptx`, `pdf` |
|
|
50
|
+
| Deck of PDF pages | `pdf` |
|
|
51
|
+
|
|
52
|
+
`exportFormatsOf(content)` lists them, and `canExport(content, format)` checks one.
|
|
49
53
|
|
|
50
54
|
- `readImage` is called once for every image the content uses. Return `null` and the export goes
|
|
51
55
|
on without that image.
|
|
@@ -55,6 +59,9 @@ reply.header('content-disposition', contentDisposition('Subnetting guide', file.
|
|
|
55
59
|
page size in inches) and returns the PDF. Use `gotenberg` or write your own with Puppeteer or
|
|
56
60
|
Playwright.
|
|
57
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`.
|
|
58
65
|
|
|
59
66
|
## Importing
|
|
60
67
|
|
|
@@ -66,22 +73,33 @@ const { title, content } = await importFile(upload.bytes, upload.filename, {
|
|
|
66
73
|
});
|
|
67
74
|
```
|
|
68
75
|
|
|
69
|
-
- The file is recognized by its content, not its name. Word and
|
|
70
|
-
by what their zip holds,
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
- The file is recognized by its content, not its name. Word, PowerPoint and OpenDocument files
|
|
77
|
+
are told apart by what their zip holds, Word 97 and PowerPoint 97 files by the streams they hold,
|
|
78
|
+
and PDF, RTF, PNG, JPEG, GIF and WebP by their signatures. Only Markdown (`.md`) and text
|
|
79
|
+
(`.txt`) are recognized by their extension, and only when they are valid UTF-8.
|
|
80
|
+
- Word, Markdown and text files become documents, and PowerPoint files become decks. A deck
|
|
73
81
|
keeps its theme colors and the font most of its text uses. What its slide master draws comes in
|
|
74
82
|
locked, behind the slide's own elements, and freeform shapes come in as polygons.
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
`
|
|
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`.
|
|
88
|
+
- A picture becomes a deck of one slide, in the format closest to its shape, with the picture
|
|
89
|
+
filling it. `saveImage` stores it like any other image; return `null` and the import is refused
|
|
90
|
+
as `empty`.
|
|
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`.
|
|
94
|
+
|
|
95
|
+
`officeEngine` takes `{ file, from, to }` and returns the converted file. `libreOffice()` from
|
|
96
|
+
`@pollium/vyva-convert/libreoffice` does that with the `soffice` on the machine (`command` changes
|
|
97
|
+
the path), one conversion per call:
|
|
80
98
|
|
|
81
99
|
```ts
|
|
82
100
|
import { libreOffice } from '@pollium/vyva-convert/libreoffice';
|
|
83
101
|
|
|
84
|
-
await importFile(upload.bytes, upload.filename, { saveImage,
|
|
102
|
+
await importFile(upload.bytes, upload.filename, { saveImage, officeEngine: libreOffice() });
|
|
85
103
|
```
|
|
86
104
|
- `saveImage` stores each image and returns its file id, which must be a UUID. Return `null` to
|
|
87
105
|
leave the image out. If it throws, the import fails with your error.
|
|
@@ -98,4 +116,4 @@ a bug or a broken file, so log it.
|
|
|
98
116
|
| `too-large` | The file, or what it unzips to, is over the limit |
|
|
99
117
|
| `empty` | The file has nothing to import |
|
|
100
118
|
| `pdf-failed` | The PDF engine could not be reached or answered with an error |
|
|
101
|
-
| `
|
|
119
|
+
| `office-failed` | `officeEngine` could not convert the file, or answered with something else |
|
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' | 'office-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/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 };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { DECK_SIZES } from "@pollium/vyva-core/content";
|
|
2
|
+
//#region src/import/deck-format.ts
|
|
3
|
+
const FORMATS = Object.entries(DECK_SIZES).map(([format, size]) => ({
|
|
4
|
+
format,
|
|
5
|
+
ratio: size.width / size.height
|
|
6
|
+
}));
|
|
7
|
+
const formatFor = (width, height) => {
|
|
8
|
+
const ratio = width / Math.max(height, 1);
|
|
9
|
+
return FORMATS.reduce((best, candidate) => Math.abs(candidate.ratio - ratio) < Math.abs(best.ratio - ratio) ? candidate : best).format;
|
|
10
|
+
};
|
|
11
|
+
//#endregion
|
|
12
|
+
export { formatFor };
|
|
@@ -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 };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { SaveImage } from "./images.js";
|
|
2
|
+
import { ImportedDeck } from "./pptx.js";
|
|
3
|
+
//#region src/import/image.d.ts
|
|
4
|
+
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
|
|
5
|
+
export declare const imageTypeOf: (data: Buffer) => ImageType | null;
|
|
6
|
+
export declare const importImage: (buffer: Buffer, filename: string, contentType: ImageType, saveImage: SaveImage) => Promise<ImportedDeck>;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ConvertError } from "../errors.js";
|
|
2
|
+
import { imageSize } from "../export/image-size.js";
|
|
3
|
+
import { titleOf } from "./titles.js";
|
|
4
|
+
import { imageSaver } from "./images.js";
|
|
5
|
+
import { formatFor } from "./deck-format.js";
|
|
6
|
+
import { PLAIN_THEME } from "./plain-theme.js";
|
|
7
|
+
import { DECK_SIZES } from "@pollium/vyva-core/content";
|
|
8
|
+
import { newId } from "@pollium/vyva-core/ids";
|
|
9
|
+
//#region src/import/image.ts
|
|
10
|
+
const PNG_SIGNATURE = Buffer.from("89504e470d0a1a0a", "hex");
|
|
11
|
+
const JPEG_SIGNATURE = Buffer.from("ffd8ff", "hex");
|
|
12
|
+
const imageTypeOf = (data) => {
|
|
13
|
+
if (data.subarray(0, PNG_SIGNATURE.length).equals(PNG_SIGNATURE)) return "image/png";
|
|
14
|
+
if (data.subarray(0, JPEG_SIGNATURE.length).equals(JPEG_SIGNATURE)) return "image/jpeg";
|
|
15
|
+
const head = data.toString("latin1", 0, 12);
|
|
16
|
+
if (head.startsWith("GIF87a") || head.startsWith("GIF89a")) return "image/gif";
|
|
17
|
+
return head.startsWith("RIFF") && head.slice(8) === "WEBP" ? "image/webp" : null;
|
|
18
|
+
};
|
|
19
|
+
const importImage = async (buffer, filename, contentType, saveImage) => {
|
|
20
|
+
const fileId = await imageSaver(saveImage, filename, 1)(buffer, contentType);
|
|
21
|
+
if (fileId === null) throw new ConvertError("empty", `${filename} was not stored, so there is nothing to show`);
|
|
22
|
+
const size = imageSize(buffer, contentType);
|
|
23
|
+
const format = formatFor(size.width, size.height);
|
|
24
|
+
const slide = DECK_SIZES[format];
|
|
25
|
+
const scale = Math.min(slide.width / size.width, slide.height / size.height);
|
|
26
|
+
const w = Math.round(size.width * scale);
|
|
27
|
+
const h = Math.round(size.height * scale);
|
|
28
|
+
return {
|
|
29
|
+
title: titleOf("", filename),
|
|
30
|
+
content: {
|
|
31
|
+
kind: "deck",
|
|
32
|
+
format,
|
|
33
|
+
theme: PLAIN_THEME,
|
|
34
|
+
slides: [{
|
|
35
|
+
id: newId(),
|
|
36
|
+
background: null,
|
|
37
|
+
elements: [{
|
|
38
|
+
id: newId(),
|
|
39
|
+
type: "image",
|
|
40
|
+
x: Math.round((slide.width - w) / 2),
|
|
41
|
+
y: Math.round((slide.height - h) / 2),
|
|
42
|
+
w,
|
|
43
|
+
h,
|
|
44
|
+
rotation: 0,
|
|
45
|
+
opacity: 1,
|
|
46
|
+
isLocked: false,
|
|
47
|
+
fileId,
|
|
48
|
+
fit: "contain",
|
|
49
|
+
radius: 0,
|
|
50
|
+
alt: "",
|
|
51
|
+
credit: null
|
|
52
|
+
}]
|
|
53
|
+
}]
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
export { imageTypeOf, importImage };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/import/office.d.ts
|
|
2
|
+
export type LegacyOffice = 'doc' | 'ppt' | 'odt' | 'odp' | 'rtf';
|
|
3
|
+
export interface OfficeJob {
|
|
4
|
+
file: Uint8Array;
|
|
5
|
+
from: LegacyOffice;
|
|
6
|
+
to: 'docx' | 'pptx';
|
|
7
|
+
}
|
|
8
|
+
export type OfficeEngine = (job: OfficeJob) => Promise<Uint8Array>;
|
|
9
|
+
export declare const runOffice: (engine: OfficeEngine, job: OfficeJob, filename: string) => Promise<Buffer>;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ConvertError } from "../errors.js";
|
|
2
|
+
//#region src/import/office.ts
|
|
3
|
+
const runOffice = async (engine, job, filename) => {
|
|
4
|
+
const converted = await engine(job).catch((error) => {
|
|
5
|
+
throw error instanceof ConvertError ? error : new ConvertError("office-failed", `${filename} could not be turned into ${job.to}`, { cause: error });
|
|
6
|
+
});
|
|
7
|
+
return Buffer.from(converted);
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { runOffice };
|
|
@@ -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.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { ConvertError } from "../errors.js";
|
|
2
2
|
import { titleOf } from "./titles.js";
|
|
3
3
|
import { imageSaver } from "./images.js";
|
|
4
|
+
import { formatFor } from "./deck-format.js";
|
|
4
5
|
import { all, attr, first, num, textOf } from "./pptx-xml.js";
|
|
5
6
|
import { familyOf } from "./pptx-theme.js";
|
|
6
7
|
import PptxPackage from "./pptx-package.js";
|
|
7
8
|
import SlideReader from "./pptx-slide.js";
|
|
8
|
-
import { DECK_SIZES } from "@pollium/vyva-core/content";
|
|
9
|
+
import { DECK_SIZES, MAX_SLIDES } from "@pollium/vyva-core/content";
|
|
9
10
|
import { unzipSync } from "fflate";
|
|
10
11
|
import { posix } from "node:path";
|
|
11
12
|
//#region src/import/pptx.ts
|
|
@@ -13,7 +14,6 @@ const EMU_PER_INCH = 914400;
|
|
|
13
14
|
const PX_PER_INCH = 144;
|
|
14
15
|
const DEFAULT_WIDTH = 12192e3;
|
|
15
16
|
const DEFAULT_HEIGHT = 6858e3;
|
|
16
|
-
const MAX_SLIDES = 300;
|
|
17
17
|
const MAX_IMAGES = 80;
|
|
18
18
|
const MAX_UNZIPPED_BYTES = 125829120;
|
|
19
19
|
const IMAGE_TYPES = {
|
|
@@ -23,32 +23,6 @@ const IMAGE_TYPES = {
|
|
|
23
23
|
gif: "image/gif",
|
|
24
24
|
webp: "image/webp"
|
|
25
25
|
};
|
|
26
|
-
const FORMATS = [
|
|
27
|
-
{
|
|
28
|
-
format: "16:9",
|
|
29
|
-
ratio: 16 / 9
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
format: "4:3",
|
|
33
|
-
ratio: 4 / 3
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
format: "1:1",
|
|
37
|
-
ratio: 1
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
format: "a4-landscape",
|
|
41
|
-
ratio: 1527 / 1080
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
format: "a4-portrait",
|
|
45
|
-
ratio: 1080 / 1527
|
|
46
|
-
}
|
|
47
|
-
];
|
|
48
|
-
const formatFor = (cx, cy) => {
|
|
49
|
-
const ratio = cx / Math.max(cy, 1);
|
|
50
|
-
return FORMATS.reduce((best, candidate) => Math.abs(candidate.ratio - ratio) < Math.abs(best.ratio - ratio) ? candidate : best).format;
|
|
51
|
-
};
|
|
52
26
|
const plainText = (html) => html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
53
27
|
const deckTitle = (titles, slides, filename) => {
|
|
54
28
|
const biggest = slides[0]?.elements.filter((element) => element.type === "text" && !element.isLocked).sort((a, b) => b.style.fontSize - a.style.fontSize)[0];
|
package/dist/import.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { SaveImage } from "./import/images.js";
|
|
2
|
-
import {
|
|
2
|
+
import { OfficeEngine } from "./import/office.js";
|
|
3
3
|
import { Content } from "@pollium/vyva-core/content";
|
|
4
4
|
//#region src/import.d.ts
|
|
5
5
|
export declare const MAX_IMPORT_BYTES: number;
|
|
6
6
|
export interface ImportOptions {
|
|
7
7
|
saveImage: SaveImage;
|
|
8
8
|
maxBytes?: number;
|
|
9
|
-
|
|
9
|
+
officeEngine?: OfficeEngine;
|
|
10
10
|
}
|
|
11
11
|
export interface ImportedContent {
|
|
12
12
|
title: string;
|
package/dist/import.js
CHANGED
|
@@ -1,23 +1,44 @@
|
|
|
1
1
|
import { ConvertError } from "./errors.js";
|
|
2
|
-
import { importDocx, importMarkdown,
|
|
2
|
+
import { importDocx, importMarkdown, importPlainText } from "./import/documents.js";
|
|
3
|
+
import { imageTypeOf, importImage } from "./import/image.js";
|
|
4
|
+
import { runOffice } from "./import/office.js";
|
|
5
|
+
import { importPdf } from "./import/pdf.js";
|
|
3
6
|
import { importPptx } from "./import/pptx.js";
|
|
4
|
-
import {
|
|
5
|
-
import { unzipSync } from "fflate";
|
|
7
|
+
import { strFromU8, unzipSync } from "fflate";
|
|
6
8
|
//#region src/import.ts
|
|
7
9
|
const MAX_IMPORT_BYTES = 26214400;
|
|
8
10
|
const ZIP_SIGNATURE = 67324752;
|
|
9
11
|
const PDF_SIGNATURE = "%PDF-";
|
|
12
|
+
const RTF_SIGNATURE = "{\\rtf";
|
|
13
|
+
const OLE_SIGNATURE = Buffer.from("d0cf11e0a1b11ae1", "hex");
|
|
10
14
|
const TEXT_SAMPLE_BYTES = 65536;
|
|
15
|
+
const MAX_MIMETYPE_BYTES = 128;
|
|
11
16
|
const BOM = /^/;
|
|
17
|
+
const OPEN_DOCUMENTS = {
|
|
18
|
+
"application/vnd.oasis.opendocument.text": "odt",
|
|
19
|
+
"application/vnd.oasis.opendocument.text-template": "odt",
|
|
20
|
+
"application/vnd.oasis.opendocument.presentation": "odp",
|
|
21
|
+
"application/vnd.oasis.opendocument.presentation-template": "odp"
|
|
22
|
+
};
|
|
23
|
+
const OLE_STREAMS = [{
|
|
24
|
+
stream: Buffer.from("WordDocument", "utf16le"),
|
|
25
|
+
format: "doc"
|
|
26
|
+
}, {
|
|
27
|
+
stream: Buffer.from("PowerPoint Document", "utf16le"),
|
|
28
|
+
format: "ppt"
|
|
29
|
+
}];
|
|
12
30
|
const zipParts = (buffer) => {
|
|
13
31
|
if (buffer.length < 4 || buffer.readUInt32LE(0) !== ZIP_SIGNATURE) return null;
|
|
14
32
|
try {
|
|
15
33
|
const names = /* @__PURE__ */ new Set();
|
|
16
|
-
unzipSync(buffer, { filter: (file) => {
|
|
34
|
+
const read = unzipSync(buffer, { filter: (file) => {
|
|
17
35
|
names.add(file.name);
|
|
18
|
-
return
|
|
36
|
+
return file.name === "mimetype" && file.originalSize <= MAX_MIMETYPE_BYTES;
|
|
19
37
|
} });
|
|
20
|
-
return
|
|
38
|
+
return {
|
|
39
|
+
names,
|
|
40
|
+
mimetype: read.mimetype === void 0 ? "" : strFromU8(read.mimetype)
|
|
41
|
+
};
|
|
21
42
|
} catch {
|
|
22
43
|
return null;
|
|
23
44
|
}
|
|
@@ -33,24 +54,52 @@ const isUtf8Text = (buffer) => {
|
|
|
33
54
|
}
|
|
34
55
|
};
|
|
35
56
|
const detect = (buffer, filename) => {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
if (
|
|
39
|
-
|
|
57
|
+
const zip = zipParts(buffer);
|
|
58
|
+
if (zip !== null) {
|
|
59
|
+
if (zip.names.has("word/document.xml")) return "docx";
|
|
60
|
+
if (zip.names.has("ppt/presentation.xml")) return "pptx";
|
|
61
|
+
return OPEN_DOCUMENTS[zip.mimetype] ?? null;
|
|
40
62
|
}
|
|
41
63
|
if (buffer.subarray(0, 5).toString("latin1") === PDF_SIGNATURE) return "pdf";
|
|
64
|
+
if (buffer.subarray(0, OLE_SIGNATURE.length).equals(OLE_SIGNATURE)) return OLE_STREAMS.find(({ stream }) => buffer.includes(stream))?.format ?? null;
|
|
65
|
+
if (buffer.subarray(0, 5).toString("latin1") === RTF_SIGNATURE) return "rtf";
|
|
66
|
+
const image = imageTypeOf(buffer);
|
|
67
|
+
if (image !== null) return image;
|
|
42
68
|
if (!isUtf8Text(buffer)) return null;
|
|
43
69
|
const extension = filename.toLowerCase().split(".").at(-1) ?? "";
|
|
44
70
|
if (extension === "md" || extension === "markdown") return "markdown";
|
|
45
71
|
if (extension === "txt") return "text";
|
|
46
72
|
return null;
|
|
47
73
|
};
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
const textOf = (buffer) => buffer.toString("utf8").replace(BOM, "");
|
|
75
|
+
const upgraded = async (from, to, buffer, filename, officeEngine) => {
|
|
76
|
+
if (officeEngine === void 0) throw new ConvertError("unsupported", `${filename} is a ${from} file, which Vyva imports only through an office engine`);
|
|
77
|
+
const converted = await runOffice(officeEngine, {
|
|
78
|
+
file: buffer,
|
|
79
|
+
from,
|
|
80
|
+
to
|
|
81
|
+
}, filename);
|
|
82
|
+
if (detect(converted, filename) !== to) throw new ConvertError("office-failed", `The office engine did not turn ${filename} into ${to}`);
|
|
83
|
+
return converted;
|
|
84
|
+
};
|
|
85
|
+
const convert = async (detected, buffer, filename, options) => {
|
|
86
|
+
const { saveImage, officeEngine } = options;
|
|
87
|
+
switch (detected) {
|
|
88
|
+
case "docx": return importDocx(buffer, filename, saveImage);
|
|
89
|
+
case "pptx": return importPptx(buffer, filename, saveImage);
|
|
90
|
+
case "pdf": return importPdf(buffer, filename, saveImage);
|
|
91
|
+
case "markdown": return importMarkdown(textOf(buffer), filename);
|
|
92
|
+
case "text": return importPlainText(textOf(buffer), filename);
|
|
93
|
+
case "image/png":
|
|
94
|
+
case "image/jpeg":
|
|
95
|
+
case "image/gif":
|
|
96
|
+
case "image/webp": return importImage(buffer, filename, detected, saveImage);
|
|
97
|
+
case "doc":
|
|
98
|
+
case "odt":
|
|
99
|
+
case "rtf": return convert("docx", await upgraded(detected, "docx", buffer, filename, officeEngine), filename, options);
|
|
100
|
+
case "ppt":
|
|
101
|
+
case "odp": return convert("pptx", await upgraded(detected, "pptx", buffer, filename, officeEngine), filename, options);
|
|
102
|
+
}
|
|
54
103
|
};
|
|
55
104
|
const isEmpty = (content) => content.kind === "document" ? content.blocks.length === 0 : content.slides.length === 0;
|
|
56
105
|
const importFile = async (data, filename, options) => {
|
package/dist/libreoffice.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OfficeEngine } from "./import/office.js";
|
|
2
2
|
//#region src/libreoffice.d.ts
|
|
3
3
|
export interface LibreOfficeOptions {
|
|
4
4
|
command?: string;
|
|
5
5
|
timeoutMs?: number;
|
|
6
6
|
}
|
|
7
|
-
export declare const libreOffice: ({ command, timeoutMs }?: LibreOfficeOptions) =>
|
|
7
|
+
export declare const libreOffice: ({ command, timeoutMs }?: LibreOfficeOptions) => OfficeEngine;
|
|
8
8
|
//#endregion
|
package/dist/libreoffice.js
CHANGED
|
@@ -8,25 +8,24 @@ import { promisify } from "node:util";
|
|
|
8
8
|
//#region src/libreoffice.ts
|
|
9
9
|
const DEFAULT_TIMEOUT_MS = 12e4;
|
|
10
10
|
const run = promisify(execFile);
|
|
11
|
-
const libreOffice = ({ command = "soffice", timeoutMs = DEFAULT_TIMEOUT_MS } = {}) => async (
|
|
12
|
-
const folder = await mkdtemp(join(tmpdir(), "vyva-
|
|
13
|
-
const source = join(folder,
|
|
11
|
+
const libreOffice = ({ command = "soffice", timeoutMs = DEFAULT_TIMEOUT_MS } = {}) => async ({ file, from, to }) => {
|
|
12
|
+
const folder = await mkdtemp(join(tmpdir(), "vyva-office-"));
|
|
13
|
+
const source = join(folder, `source.${from}`);
|
|
14
14
|
try {
|
|
15
|
-
await writeFile(source,
|
|
15
|
+
await writeFile(source, file);
|
|
16
16
|
await run(command, [
|
|
17
17
|
`-env:UserInstallation=${pathToFileURL(join(folder, "profile")).href}`,
|
|
18
18
|
"--headless",
|
|
19
|
-
"--infilter=impress_pdf_import",
|
|
20
19
|
"--convert-to",
|
|
21
|
-
|
|
20
|
+
to,
|
|
22
21
|
"--outdir",
|
|
23
22
|
folder,
|
|
24
23
|
source
|
|
25
24
|
], { timeout: timeoutMs }).catch((error) => {
|
|
26
|
-
throw new ConvertError("
|
|
25
|
+
throw new ConvertError("office-failed", `LibreOffice at ${command} could not turn the ${from} file into ${to}`, { cause: error });
|
|
27
26
|
});
|
|
28
|
-
return new Uint8Array(await readFile(join(folder,
|
|
29
|
-
throw new ConvertError("
|
|
27
|
+
return new Uint8Array(await readFile(join(folder, `source.${to}`)).catch((error) => {
|
|
28
|
+
throw new ConvertError("office-failed", `LibreOffice finished without writing the ${to} file`, { cause: error });
|
|
30
29
|
}));
|
|
31
30
|
} finally {
|
|
32
31
|
await rm(folder, {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pollium/vyva-convert",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Server-side tools for Vyva content: clean what clients send, import Word, PowerPoint, PDF
|
|
3
|
+
"version": "0.4.0",
|
|
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,
|
|
7
7
|
"repository": {
|
|
@@ -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) 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,7 +0,0 @@
|
|
|
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
|
|
@@ -1,21 +0,0 @@
|
|
|
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 };
|