From dc616439121283ff466341e8f9e8f45963f9cf16 Mon Sep 17 00:00:00 2001 From: tr3lane <32203735+tr3lane@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:01:02 -0500 Subject: [PATCH] fix(libreoffice): don't force MS Word 97 infilter on .wps (#585) * fix(libreoffice): don't force MS Word 97 infilter on .wps .wps is Microsoft Works, not MS Word 97/.doc. Forcing that infilter makes soffice reject a genuine Works document with "source file could not be loaded", even though the same file converts fine when no --infilter is passed at all (LibreOffice auto-detects it correctly). Fixes #582 * test: lock in wps export direction; document why null covers both sides Per review: the filter map feeds both --infilter and the --convert-to suffix, so wps: null also affects exporting TO wps. That is deliberate and behavior-preserving: LibreOffice has no Works export filter (its MS_Works filter is import-only), and bare `--convert-to wps` falls back to LibreOffice's default export filter for the extension - "MS Word 97", the exact filter this map pinned before, so export output is unchanged. Adds a regression test asserting the docx -> wps invocation. --------- Co-authored-by: tr3lane <182920672+tr3lane@users.noreply.github.com> --- src/converters/libreoffice.ts | 17 +++++++++-- tests/converters/libreoffice.test.ts | 43 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/converters/libreoffice.ts b/src/converters/libreoffice.ts index 1fd6488..cb9cfb1 100644 --- a/src/converters/libreoffice.ts +++ b/src/converters/libreoffice.ts @@ -77,7 +77,7 @@ export const properties = { type FileCategories = "text" | "calc"; -const filters: Record> = { +const filters: Record> = { text: { "602": "T602Document", abw: "AbiWord", @@ -113,7 +113,20 @@ const filters: Record> = { txt: "Text", wn: "WriteNow", wpd: "WordPerfect", - wps: "MS Word 97", + // .wps is Microsoft Works, not MS Word 97/.doc - forcing the "MS Word 97" + // filter makes soffice reject a genuine Works document with "source file + // could not be loaded", even though it converts the same file fine with + // no --infilter at all (LibreOffice auto-detects it correctly). + // + // null is deliberate for BOTH directions here, not just the import side: + // this map feeds both --infilter (import) and the --convert-to suffix + // (export). On import, null lets LibreOffice auto-detect - its Works + // import filter (MS_Works, libwps-backed) is import-only, so it can only + // be reached via auto-detection anyway. On export, LibreOffice has no + // Works export filter at all; bare `--convert-to wps` falls back to its + // default export filter for the extension, which is "MS Word 97" - the + // exact filter this map pinned before, so export output is unchanged. + wps: null, wpt: "MS Word 97 Vorlage", wri: "MS_Write", xhtml: "HTML (StarWriter)", diff --git a/tests/converters/libreoffice.test.ts b/tests/converters/libreoffice.test.ts index 5d34d45..8545780 100644 --- a/tests/converters/libreoffice.test.ts +++ b/tests/converters/libreoffice.test.ts @@ -102,6 +102,49 @@ test("uses only infilter when convertTo has no out filter (e.g., docx -> pdf)", expect(args.slice(-2)).toEqual(["out", "in.docx"]); }); +test("does not force an infilter for wps (Microsoft Works, not MS Word 97)", async () => { + // Regression test for https://github.com/C4illin/ConvertX/issues/582 - + // forcing --infilter="MS Word 97" on a genuine .wps file makes soffice + // reject it with "source file could not be loaded". No forced infilter + // lets LibreOffice auto-detect the real format instead. + await convert("in.wps", "wps", "docx", "out/out.docx", undefined, mockExecFile); + + const { args } = requireDefined(calls[0], "Expected at least one execFile call"); + + expect(args).toEqual([ + "--headless", + "--convert-to", + "docx:MS Word 2007 XML", + "--outdir", + "out", + "in.wps", + ]); + expect(args.some((a) => a.startsWith("--infilter"))).toBe(false); +}); + +test("does not force an outfilter for wps as an export target either", async () => { + // wps shares one filter-map entry for both directions (see the comment in + // libreoffice.ts) - docx's own infilter is still emitted (that describes + // the real source file), but no --convert-to wps: suffix is + // forced. Verified against a real soffice: this exact invocation succeeds, + // with LibreOffice's default export filter for .wps ("MS Word 97" - the + // same filter the map pinned before this change, so output is unchanged). + // LibreOffice has no Works export filter, so no suffix could do better. + await convert("in.docx", "docx", "wps", "out/out.wps", undefined, mockExecFile); + + const { args } = requireDefined(calls[0], "Expected at least one execFile call"); + + expect(args).toEqual([ + "--headless", + "--infilter=MS Word 2007 XML", + "--convert-to", + "wps", + "--outdir", + "out", + "in.docx", + ]); +}); + test("strips leading './' from outdir", async () => { await convert("in.txt", "txt", "docx", "./out/out.docx", undefined, mockExecFile);