@pollium/vyva-convert 0.2.0 → 0.3.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 +22 -11
- package/dist/errors.d.ts +1 -1
- package/dist/import/deck-format.d.ts +4 -0
- package/dist/import/deck-format.js +12 -0
- package/dist/import/image.d.ts +7 -0
- package/dist/import/image.js +59 -0
- package/dist/import/office.d.ts +10 -0
- package/dist/import/office.js +10 -0
- package/dist/import/pdf-lines.js +1 -1
- package/dist/import/pdf-slides.d.ts +2 -3
- package/dist/import/pdf-slides.js +11 -10
- package/dist/import/pptx.d.ts +1 -0
- package/dist/import/pptx.js +3 -28
- package/dist/import.d.ts +2 -2
- package/dist/import.js +64 -15
- package/dist/libreoffice.d.ts +2 -2
- package/dist/libreoffice.js +9 -9
- package/package.json +2 -2
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
|
|
@@ -66,22 +67,32 @@ const { title, content } = await importFile(upload.bytes, upload.filename, {
|
|
|
66
67
|
});
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
- The file is recognized by its content, not its name. Word and
|
|
70
|
-
by what their zip holds,
|
|
71
|
-
|
|
70
|
+
- The file is recognized by its content, not its name. Word, PowerPoint and OpenDocument files
|
|
71
|
+
are told apart by what their zip holds, Word 97 and PowerPoint 97 files by the streams they hold,
|
|
72
|
+
and PDF, RTF, PNG, JPEG, GIF and WebP by their signatures. Only Markdown (`.md`) and text
|
|
73
|
+
(`.txt`) are recognized by their extension, and only when they are valid UTF-8.
|
|
72
74
|
- Word, PDF, Markdown and text files become documents, and PowerPoint files become decks. A deck
|
|
73
75
|
keeps its theme colors and the font most of its text uses. What its slide master draws comes in
|
|
74
76
|
locked, behind the slide's own elements, and freeform shapes come in as polygons.
|
|
75
|
-
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
- A picture becomes a deck of one slide, in the format closest to its shape, with the picture
|
|
78
|
+
filling it. `saveImage` stores it like any other image; return `null` and the import is refused
|
|
79
|
+
as `empty`.
|
|
80
|
+
- With `officeEngine`, Vyva reads what it cannot read alone. Every PDF becomes a deck: each page a
|
|
81
|
+
slide, portrait pages on a portrait slide, with its pictures and shapes where they were, and the
|
|
82
|
+
loose lines a PDF keeps joined back into paragraphs and lists. A PDF of more than 300 pages is
|
|
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`.
|
|
87
|
+
|
|
88
|
+
`officeEngine` takes `{ file, from, to }` and returns the converted file. `libreOffice()` from
|
|
89
|
+
`@pollium/vyva-convert/libreoffice` does that with the `soffice` on the machine (`command` changes
|
|
90
|
+
the path), one conversion per call:
|
|
80
91
|
|
|
81
92
|
```ts
|
|
82
93
|
import { libreOffice } from '@pollium/vyva-convert/libreoffice';
|
|
83
94
|
|
|
84
|
-
await importFile(upload.bytes, upload.filename, { saveImage,
|
|
95
|
+
await importFile(upload.bytes, upload.filename, { saveImage, officeEngine: libreOffice() });
|
|
85
96
|
```
|
|
86
97
|
- `saveImage` stores each image and returns its file id, which must be a UUID. Return `null` to
|
|
87
98
|
leave the image out. If it throws, the import fails with your error.
|
|
@@ -98,4 +109,4 @@ a bug or a broken file, so log it.
|
|
|
98
109
|
| `too-large` | The file, or what it unzips to, is over the limit |
|
|
99
110
|
| `empty` | The file has nothing to import |
|
|
100
111
|
| `pdf-failed` | The PDF engine could not be reached or answered with an error |
|
|
101
|
-
| `
|
|
112
|
+
| `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);
|
|
@@ -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 };
|
|
@@ -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,59 @@
|
|
|
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 { DECK_SIZES } from "@pollium/vyva-core/content";
|
|
7
|
+
import { newId } from "@pollium/vyva-core/ids";
|
|
8
|
+
import { DECK_THEME } from "@pollium/vyva-core/themes";
|
|
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
|
+
const { frame: _frame, ...theme } = DECK_THEME;
|
|
29
|
+
return {
|
|
30
|
+
title: titleOf("", filename),
|
|
31
|
+
content: {
|
|
32
|
+
kind: "deck",
|
|
33
|
+
format,
|
|
34
|
+
theme,
|
|
35
|
+
slides: [{
|
|
36
|
+
id: newId(),
|
|
37
|
+
background: null,
|
|
38
|
+
elements: [{
|
|
39
|
+
id: newId(),
|
|
40
|
+
type: "image",
|
|
41
|
+
x: Math.round((slide.width - w) / 2),
|
|
42
|
+
y: Math.round((slide.height - h) / 2),
|
|
43
|
+
w,
|
|
44
|
+
h,
|
|
45
|
+
rotation: 0,
|
|
46
|
+
opacity: 1,
|
|
47
|
+
isLocked: false,
|
|
48
|
+
fileId,
|
|
49
|
+
fit: "contain",
|
|
50
|
+
radius: 0,
|
|
51
|
+
alt: "",
|
|
52
|
+
credit: null
|
|
53
|
+
}]
|
|
54
|
+
}]
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
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: 'pdf' | 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 };
|
package/dist/import/pdf-lines.js
CHANGED
|
@@ -179,7 +179,7 @@ const joinSlide = (slide, font, slideWidth, slideHeight) => {
|
|
|
179
179
|
const blocks = blocksOf(lines.filter((line) => !owned.has(line)));
|
|
180
180
|
const replaced = /* @__PURE__ */ new Map();
|
|
181
181
|
for (const block of blocks) {
|
|
182
|
-
if (block.lines.length === 1 && block.lines[0].marker === null) continue;
|
|
182
|
+
if (block.lines.length === 1 && block.lines[0].marker === null && right(block.lines[0].element) <= slideWidth - SLIDE_MARGIN) continue;
|
|
183
183
|
const [head, ...rest] = block.lines;
|
|
184
184
|
const own = /* @__PURE__ */ new Set([...block.lines.map((line) => line.element), ...[...owned].map((marker) => marker.element)]);
|
|
185
185
|
replaced.set(head.element, merged(block, slide.elements.filter((element) => !own.has(element)), font, slideWidth));
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { SaveImage } from "./images.js";
|
|
2
|
+
import { OfficeEngine } from "./office.js";
|
|
2
3
|
import { ImportedDeck } from "./pptx.js";
|
|
3
4
|
//#region src/import/pdf-slides.d.ts
|
|
4
|
-
export
|
|
5
|
-
export declare const isLandscapePdf: (buffer: Buffer) => Promise<boolean>;
|
|
6
|
-
export declare const importPdfSlides: (buffer: Buffer, filename: string, saveImage: SaveImage, pdfSlides: PdfSlides) => Promise<ImportedDeck>;
|
|
5
|
+
export declare const importPdfSlides: (buffer: Buffer, filename: string, saveImage: SaveImage, officeEngine: OfficeEngine) => Promise<ImportedDeck>;
|
|
7
6
|
//#endregion
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { ConvertError } from "../errors.js";
|
|
2
|
+
import { runOffice } from "./office.js";
|
|
2
3
|
import { joinLines } from "./pdf-lines.js";
|
|
3
4
|
import { importPptx } from "./pptx.js";
|
|
4
5
|
import { getDocumentProxy } from "unpdf";
|
|
5
6
|
//#region src/import/pdf-slides.ts
|
|
6
|
-
const
|
|
7
|
-
const {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
const { title, content } = await importPptx(
|
|
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);
|
|
15
16
|
return {
|
|
16
17
|
title,
|
|
17
18
|
content: joinLines(content)
|
|
18
19
|
};
|
|
19
20
|
};
|
|
20
21
|
//#endregion
|
|
21
|
-
export { importPdfSlides
|
|
22
|
+
export { importPdfSlides };
|
package/dist/import/pptx.d.ts
CHANGED
package/dist/import/pptx.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
@@ -23,32 +24,6 @@ const IMAGE_TYPES = {
|
|
|
23
24
|
gif: "image/gif",
|
|
24
25
|
webp: "image/webp"
|
|
25
26
|
};
|
|
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
27
|
const plainText = (html) => html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
53
28
|
const deckTitle = (titles, slides, filename) => {
|
|
54
29
|
const biggest = slides[0]?.elements.filter((element) => element.type === "text" && !element.isLocked).sort((a, b) => b.style.fontSize - a.style.fontSize)[0];
|
|
@@ -82,7 +57,7 @@ const importPptx = async (buffer, filename, saveImage) => {
|
|
|
82
57
|
const cy = num(attr(size, "cy"), DEFAULT_HEIGHT);
|
|
83
58
|
const format = formatFor(cx, cy);
|
|
84
59
|
const relations = pptx.relationsOf("ppt/presentation.xml");
|
|
85
|
-
const allParts = all(presentation, "p:sldId").map((slide) => relations[attr(slide, "r:id") ?? ""]?.target).filter((path) => path !== void 0).slice(0,
|
|
60
|
+
const allParts = all(presentation, "p:sldId").map((slide) => relations[attr(slide, "r:id") ?? ""]?.target).filter((path) => path !== void 0).slice(0, 300).map((path) => pptx.parts(path)).filter((parts) => parts !== null);
|
|
86
61
|
const themed = allParts[0]?.slide.palette.deckTheme();
|
|
87
62
|
if (themed === void 0) throw new ConvertError("empty", `${filename} has no slides`);
|
|
88
63
|
const theme = {
|
|
@@ -123,4 +98,4 @@ const importPptx = async (buffer, filename, saveImage) => {
|
|
|
123
98
|
};
|
|
124
99
|
};
|
|
125
100
|
//#endregion
|
|
126
|
-
export { importPptx };
|
|
101
|
+
export { MAX_SLIDES, importPptx };
|
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
2
|
import { importDocx, importMarkdown, importPdf, importPlainText } from "./import/documents.js";
|
|
3
|
+
import { imageTypeOf, importImage } from "./import/image.js";
|
|
4
|
+
import { runOffice } from "./import/office.js";
|
|
3
5
|
import { importPptx } from "./import/pptx.js";
|
|
4
|
-
import { importPdfSlides
|
|
5
|
-
import { unzipSync } from "fflate";
|
|
6
|
+
import { importPdfSlides } from "./import/pdf-slides.js";
|
|
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 officeEngine === void 0 ? importPdf(buffer, filename) : importPdfSlides(buffer, filename, saveImage, officeEngine);
|
|
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,25 @@ 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",
|
|
19
|
+
...from === "pdf" ? ["--infilter=impress_pdf_import"] : [],
|
|
20
20
|
"--convert-to",
|
|
21
|
-
|
|
21
|
+
to,
|
|
22
22
|
"--outdir",
|
|
23
23
|
folder,
|
|
24
24
|
source
|
|
25
25
|
], { timeout: timeoutMs }).catch((error) => {
|
|
26
|
-
throw new ConvertError("
|
|
26
|
+
throw new ConvertError("office-failed", `LibreOffice at ${command} could not turn the ${from} file into ${to}`, { cause: error });
|
|
27
27
|
});
|
|
28
|
-
return new Uint8Array(await readFile(join(folder,
|
|
29
|
-
throw new ConvertError("
|
|
28
|
+
return new Uint8Array(await readFile(join(folder, `source.${to}`)).catch((error) => {
|
|
29
|
+
throw new ConvertError("office-failed", `LibreOffice finished without writing the ${to} file`, { cause: error });
|
|
30
30
|
}));
|
|
31
31
|
} finally {
|
|
32
32
|
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.3.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": {
|