@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 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 text files, and exports to PDF, Word, PowerPoint and HTML. It runs in Node.
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 PowerPoint files are told apart
70
- by what their zip holds, and PDF by its signature. Only Markdown (`.md`) and text (`.txt`) are
71
- recognized by their extension, and only when they are valid UTF-8.
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
- - With `pdfSlides`, a PDF whose pages are landscape becomes a deck instead: each page a slide, with
76
- its pictures and shapes where they were, and the loose lines a PDF keeps joined back into
77
- paragraphs and lists. A portrait PDF stays a document. `pdfSlides` takes the PDF and returns it as
78
- a PowerPoint file; `libreOffice()` from `@pollium/vyva-convert/libreoffice` does that with the
79
- `soffice` on the machine (`command` changes the path), one conversion per call:
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, pdfSlides: libreOffice() });
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
- | `slides-failed` | `pdfSlides` could not turn a PDF into slides |
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' | 'slides-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,4 @@
1
+ import { DeckFormat } from "@pollium/vyva-core/content";
2
+ //#region src/import/deck-format.d.ts
3
+ export declare const formatFor: (width: number, height: number) => DeckFormat;
4
+ //#endregion
@@ -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 };
@@ -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 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>;
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 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);
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, isLandscapePdf };
22
+ export { importPdfSlides };
@@ -1,6 +1,7 @@
1
1
  import { SaveImage } from "./images.js";
2
2
  import { DeckContent } from "@pollium/vyva-core/content";
3
3
  //#region src/import/pptx.d.ts
4
+ export declare const MAX_SLIDES = 300;
4
5
  export interface ImportedDeck {
5
6
  title: string;
6
7
  content: DeckContent;
@@ -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, MAX_SLIDES).map((path) => pptx.parts(path)).filter((parts) => parts !== null);
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 { PdfSlides } from "./import/pdf-slides.js";
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
- pdfSlides?: PdfSlides;
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, isLandscapePdf } from "./import/pdf-slides.js";
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 false;
36
+ return file.name === "mimetype" && file.originalSize <= MAX_MIMETYPE_BYTES;
19
37
  } });
20
- return names;
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 parts = zipParts(buffer);
37
- if (parts !== null) {
38
- if (parts.has("word/document.xml")) return "docx";
39
- return parts.has("ppt/presentation.xml") ? "pptx" : null;
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 convert = async (detected, buffer, filename, { saveImage, pdfSlides }) => {
49
- if (detected === "docx") return importDocx(buffer, filename, saveImage);
50
- if (detected === "pptx") return importPptx(buffer, filename, saveImage);
51
- if (detected === "pdf") return pdfSlides !== void 0 && await isLandscapePdf(buffer) ? importPdfSlides(buffer, filename, saveImage, pdfSlides) : importPdf(buffer, filename);
52
- const text = buffer.toString("utf8").replace(BOM, "");
53
- return detected === "markdown" ? importMarkdown(text, filename) : importPlainText(text, filename);
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) => {
@@ -1,8 +1,8 @@
1
- import { PdfSlides } from "./import/pdf-slides.js";
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) => PdfSlides;
7
+ export declare const libreOffice: ({ command, timeoutMs }?: LibreOfficeOptions) => OfficeEngine;
8
8
  //#endregion
@@ -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 (pdf) => {
12
- const folder = await mkdtemp(join(tmpdir(), "vyva-slides-"));
13
- const source = join(folder, "source.pdf");
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, pdf);
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
- "pptx",
21
+ to,
22
22
  "--outdir",
23
23
  folder,
24
24
  source
25
25
  ], { timeout: timeoutMs }).catch((error) => {
26
- throw new ConvertError("slides-failed", `LibreOffice at ${command} could not turn the PDF into slides`, { cause: error });
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, "source.pptx")).catch((error) => {
29
- throw new ConvertError("slides-failed", "LibreOffice finished without writing the slides", { cause: error });
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.2.0",
4
- "description": "Server-side tools for Vyva content: clean what clients send, import Word, PowerPoint, PDF and Markdown files, and export to PDF, Word, PowerPoint and HTML.",
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": {