diff --git a/README.md b/README.md index 86eb72b..96511b7 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ A self-hosted online file converter. Supports over a thousand different formats. | [Potrace](https://potrace.sourceforge.net/) | Raster to vector | 4 | 11 | | [VTracer](https://github.com/visioncortex/vtracer) | Raster to vector | 8 | 1 | | [Markitdown](https://github.com/microsoft/markitdown) | Documents | 6 | 1 | +| [pdftops](https://poppler.freedesktop.org/) | Documents | 1 | 2 | diff --git a/src/converters/main.ts b/src/converters/main.ts index 23839d9..6980cc6 100644 --- a/src/converters/main.ts +++ b/src/converters/main.ts @@ -18,6 +18,7 @@ import { convert as convertLibjxl, properties as propertiesLibjxl } from "./libj import { convert as convertLibreOffice, properties as propertiesLibreOffice } from "./libreoffice"; import { convert as convertMsgconvert, properties as propertiesMsgconvert } from "./msgconvert"; import { convert as convertPandoc, properties as propertiesPandoc } from "./pandoc"; +import { convert as convertPdftops, properties as propertiesPdftops } from "./pdftops"; import { convert as convertPotrace, properties as propertiesPotrace } from "./potrace"; import { convert as convertresvg, properties as propertiesresvg } from "./resvg"; import { convert as convertImage, properties as propertiesImage } from "./vips"; @@ -137,6 +138,10 @@ const properties: Record< properties: propertiesMarkitdown, converter: convertMarkitdown, }, + pdftops: { + properties: propertiesPdftops, + converter: convertPdftops, + }, }; function chunks(arr: T[], size: number): T[][] { diff --git a/src/converters/pdftops.ts b/src/converters/pdftops.ts new file mode 100644 index 0000000..8ac9766 --- /dev/null +++ b/src/converters/pdftops.ts @@ -0,0 +1,47 @@ +import { execFile as execFileOriginal } from "node:child_process"; +import { ExecFileFn } from "./types"; + +export const properties = { + from: { + document: ["pdf"], + }, + to: { + document: ["eps", "ps"], + }, +}; + +export async function convert( + filePath: string, + fileType: string, + convertTo: string, + targetPath: string, + options?: unknown, + execFile: ExecFileFn = execFileOriginal, // to make it mockable +): Promise { + const args: string[] = []; + + if (convertTo === "eps") { + args.push("-eps"); + } + + args.push(filePath, targetPath); + + return new Promise((resolve, reject) => { + execFile("pdftops", args, (error, stdout, stderr) => { + if (error) { + reject(`error: ${error}`); + return; + } + + if (stdout) { + console.log(`stdout: ${stdout}`); + } + + if (stderr) { + console.error(`stderr: ${stderr}`); + } + + resolve("Done"); + }); + }); +} diff --git a/tests/converters/pdftops.test.ts b/tests/converters/pdftops.test.ts new file mode 100644 index 0000000..ca6b525 --- /dev/null +++ b/tests/converters/pdftops.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, expect, test } from "bun:test"; +import { convert } from "../../src/converters/pdftops"; +import { runCommonTests } from "./helpers/commonTests"; + +runCommonTests(convert); + +let calls: string[][] = []; + +function mockExecFile( + _cmd: string, + args: string[], + callback: (err: Error | null, stdout: string, stderr: string) => void, +) { + calls.push(args); + if (args.includes("fail.pdf")) { + callback(new Error("mock failure"), "", "Fake stderr: fail"); + } else { + callback(null, "Fake stdout", ""); + } +} + +beforeEach(() => { + calls = []; +}); + +test("converts a normal file to ps", async () => { + const originalConsoleLog = console.log; + + let loggedMessage = ""; + console.log = (msg) => { + loggedMessage = msg; + }; + + const result = await convert("in.pdf", "pdf", "ps", "out.ps", undefined, mockExecFile); + + console.log = originalConsoleLog; + + expect(result).toBe("Done"); + expect(calls[0]).toEqual(["in.pdf", "out.ps"]); + expect(loggedMessage).toBe("stdout: Fake stdout"); +}); + +test("adds -eps flag for eps output", async () => { + const result = await convert("in.pdf", "pdf", "eps", "out.eps", undefined, mockExecFile); + + expect(result).toBe("Done"); + expect(calls[0]).toEqual(["-eps", "in.pdf", "out.eps"]); +}); + +test("fails on exec error", async () => { + expect(convert("fail.pdf", "pdf", "ps", "output.ps", undefined, mockExecFile)).rejects.toMatch( + /error: Error: mock failure/, + ); +}); + +test("logs stderr when execFile returns only stderr and no error", async () => { + const originalConsoleError = console.error; + + let loggedMessage = ""; + console.error = (msg) => { + loggedMessage = msg; + }; + + const mockExecFileStderrOnly = ( + _cmd: string, + _args: string[], + callback: (err: Error | null, stdout: string, stderr: string) => void, + ) => { + callback(null, "", "Only stderr output"); + }; + + await convert("input.pdf", "pdf", "ps", "output.ps", undefined, mockExecFileStderrOnly); + + console.error = originalConsoleError; + + expect(loggedMessage).toBe("stderr: Only stderr output"); +});