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
This commit is contained in:
Ryo 2026-07-02 09:55:01 +08:00
parent 393441faa1
commit 143ade3d36
3 changed files with 48 additions and 0 deletions

View file

@ -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 \

View file

@ -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);

View file

@ -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<ExecFileFn> = ["", [], () => {}];
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<ExecFileFn> = ["", [], () => {}];
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<ExecFileFn> = ["", [], () => {}];
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(