From 143ade3d3695d3b110981354a5859ece4623f4e4 Mon Sep 17 00:00:00 2001 From: Ryo Date: Thu, 2 Jul 2026 09:55:01 +0800 Subject: [PATCH] fix: add CJK font support for PDF conversion When converting documents containing Chinese, Japanese, or Korean characters to PDF via pandoc/xelatex, the output was blank or missing glyphs because no CJK-capable font was installed or configured. Changes: - Dockerfile: Add fonts-noto-cjk and texlive-lang-chinese packages - pandoc.ts: Pass -V CJKmainfont=Noto Sans CJK SC to pandoc when converting to pdf/latex, so xelatex uses a CJK-capable font - pandoc.test.ts: Add 3 tests verifying CJK font argument is present for pdf/latex output and absent for other formats Fixes #547 --- Dockerfile | 2 ++ src/converters/pandoc.ts | 5 ++++ tests/converters/pandoc.test.ts | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/Dockerfile b/Dockerfile index 820ae9d..de4b117 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,9 +73,11 @@ RUN apt-get update && apt-get install -y \ resvg \ texlive \ texlive-fonts-recommended \ + texlive-lang-chinese \ texlive-latex-extra \ texlive-latex-recommended \ texlive-xetex \ + fonts-noto-cjk \ python3 \ python3-pip \ pipx \ diff --git a/src/converters/pandoc.ts b/src/converters/pandoc.ts index c14c98b..da1aace 100644 --- a/src/converters/pandoc.ts +++ b/src/converters/pandoc.ts @@ -136,6 +136,11 @@ export function convert( if (xelatex.includes(convertTo)) { args.push("--pdf-engine=xelatex"); + // Specify a CJK-capable main font so that documents containing Chinese, + // Japanese or Korean characters render correctly instead of producing + // blank/missing glyphs. "Noto Sans CJK SC" is provided by the + // fonts-noto-cjk package installed in the Dockerfile. + args.push("-V", "CJKmainfont=Noto Sans CJK SC"); } args.push(filePath); diff --git a/tests/converters/pandoc.test.ts b/tests/converters/pandoc.test.ts index c910d6c..6a5ebff 100644 --- a/tests/converters/pandoc.test.ts +++ b/tests/converters/pandoc.test.ts @@ -57,6 +57,47 @@ describe("convert", () => { expect(calledArgs[1]).toContain("output.pdf"); }); + test("should add CJK mainfont argument for pdf output", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + await convert("input.md", "markdown", "pdf", "output.pdf", undefined, mockExecFile); + + const cjkFontIndex = calledArgs[1].indexOf("-V"); + expect(cjkFontIndex).toBeGreaterThan(-1); + expect(calledArgs[1][cjkFontIndex + 1]).toBe("CJKmainfont=Noto Sans CJK SC"); + }); + + test("should add CJK mainfont argument for latex output", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + await convert("input.md", "markdown", "latex", "output.tex", undefined, mockExecFile); + + expect(calledArgs[1][0]).toBe("--pdf-engine=xelatex"); + const cjkFontIndex = calledArgs[1].indexOf("-V"); + expect(cjkFontIndex).toBeGreaterThan(-1); + expect(calledArgs[1][cjkFontIndex + 1]).toBe("CJKmainfont=Noto Sans CJK SC"); + }); + + test("should not add CJK mainfont for non-pdf/latex output", async () => { + let calledArgs: Parameters = ["", [], () => {}]; + mockExecFile = (cmd, args, callback) => { + calledArgs = [cmd, args, callback]; + callback(null, "output-data", ""); + }; + + await convert("input.md", "markdown", "html", "output.html", undefined, mockExecFile); + + expect(calledArgs[1].some((arg) => arg.startsWith("CJKmainfont"))).toBe(false); + }); + test("should reject if execFile returns an error", async () => { mockExecFile = (cmd, args, callback) => callback(new Error("fail"), "", ""); await expect(