Japanese text missing from your PDFs
Your app shows Japanese on the screen, but the PDF it makes has other letters, empty boxes or nothing where the Japanese was. A PDF library draws text only with the fonts you give it, and the ones built in (Helvetica, Times and Courier, or Roboto in pdfmake) have no Japanese characters. Embed a Japanese font. In jsPDF:
async function addJa(doc, url, style) {
const res = await fetch(url);
const bytes = new Uint8Array(
await res.arrayBuffer());
let bin = "";
for (let i = 0; i < bytes.length;
i += 32768) {
const part = bytes.subarray(
i, i + 32768);
bin += String.fromCharCode.apply(
null, part);
}
const file = url.split("/").pop();
doc.addFileToVFS(file, bin);
doc.addFont(
file, "NotoSansJP", style);
}
const doc = new jsPDF();
await addJa(doc,
"/fonts/NotoSansJP-Regular.ttf",
"normal");
await addJa(doc,
"/fonts/NotoSansJP-Bold.ttf",
"bold");
doc.setFont("NotoSansJP", "normal");
Call it when the person asks for the PDF, not when the page loads: each font file is 5.7 MB. jsPDF takes the file as a binary string (its README); converting 32 KB at a time keeps the loop short, where adding one character at a time means millions of string concatenations.
What the built-in fonts do
We drew 請求書 Invoice 合計 1,980円 ("invoice", "total") with each library's default font in Chromium 152, and read each PDF back with pdf.js, Mozilla's PDF reader:
| Library | Default font | The Japanese came out as |
|---|---|---|
| jsPDF 4.2.1 | Helvetica | other letters (ŠËlBfø) |
| PDFKit 0.20.2 | Helvetica | other letters |
| pdfmake 0.3.13 | Roboto | empty boxes |
jsPDF printed no warning. With Noto Sans JP registered, all three gave back the text as written. react-pdf's built-in fonts are the same Helvetica, Times and Courier (its docs). jsPDF's README says it plainly: the 14 standard PDF fonts "are limited to the ASCII-codepage".
A real report, from timetagger, a time-tracking app: Japanese characters are missing when a report is saved as PDF. It has been open since January 2026; the maintainer answered that the PDF is made with jsPDF and its font, and wasn't sure the font could be replaced. It can, with addFont as above.
PDFKit, pdfmake and react-pdf
// Node: a path. In the browser,
// pass the file's ArrayBuffer.
doc.registerFont("ja",
"fonts/NotoSansJP-Regular.ttf");
doc.registerFont("ja-bold",
"fonts/NotoSansJP-Bold.ttf");
doc.font("ja").text("合計 1,980円");
That is PDFKit (its docs on fonts). pdfmake, which is built on PDFKit, takes font files by URL (its docs):
const dir = location.origin + "/fonts/";
pdfMake.addFonts({
NotoSansJP: {
normal:
dir + "NotoSansJP-Regular.ttf",
bold:
dir + "NotoSansJP-Bold.ttf",
},
});
const pdf = pdfMake.createPdf({
content: ["合計 1,980円"],
defaultStyle: { font: "NotoSansJP" },
});
PDFKit and pdfmake embed only the characters you use: PDFKit's PDF was 3.5 KB with 10 kanji and 133 KB with 1,000. jsPDF's was 277 KB when it drew one English word in the font, and 480 KB with 1,000 kanji.
In react-pdf, register each weight under one family, as its docs show; we did not run this one:
const dir = "/fonts/NotoSansJP-";
Font.register({
family: "NotoSansJP",
fonts: [
{ src: dir + "Regular.ttf" },
{ src: dir + "Bold.ttf",
fontWeight: 700 },
],
});
Register the bold, too
A Japanese font comes as one file per weight, and each library needs the bold file to draw bold Japanese. With only the regular file registered:
- jsPDF's
setFont("NotoSansJP", "bold")printed a warning, "Unable to look up font label", and switched to Times-Bold, so the bold Japanese came out as other letters again. - pdfmake stopped with an error: the font "in style 'bold' is not defined".
- react-pdf falls back to the nearest registered weight (its docs), so bold Japanese looks regular. That was reactive-resume #3079: bold had no effect on Chinese, Japanese and Korean text in its résumé PDFs, and the fix registered its fallback font at the bold weight too.
Which font file
Noto Sans JP is free under the SIL Open Font License. Use one static TTF file per weight: jsPDF reads .ttf files, react-pdf reads TTF and WOFF, and react-pdf's docs say variable fonts don't work properly. Google's variable file for Noto Sans JP starts at its thinnest weight (Thin, in its metadata), and with it jsPDF and PDFKit drew our text with less than half the ink of the Regular file.
Google Fonts' CSS API lists one static TTF per weight when a client that doesn't take WOFF2, such as curl, fetches the CSS for weights 400 and 700. Each file is 5.7 MB.
Line breaks without spaces
Japanese has no spaces, so a line can break between any two characters, except before closing marks: the W3C's Requirements for Japanese Text Layout say no line should begin with a closing bracket, a comma or a full stop, among others. PDFKit breaks lines "wherever the Unicode line breaking rules allow one" (its docs). We set a three-sentence paragraph at 87 widths in PDFKit and 44 in pdfmake, and no line started with 。, 、 or 」. jsPDF's splitTextToSize started a line with one at 30 of 91 widths (30 to 120 mm, at 12 pt). Move those marks back to the line above:
// Closing marks that don't start a line
const CL = /^[、。,.・)」』】!?]/;
function hang(lines) {
const out = [];
for (let s of lines) {
while (out.length && CL.test(s)) {
out[out.length - 1] += s[0];
s = s.slice(1);
}
if (s) out.push(s);
}
return out;
}
const lines = hang(
doc.splitTextToSize(text, 170));
doc.text(lines, 20, 30);
At all 91 widths, no line then started with a closing mark, and no character was lost. For 、 and 。, this is hanging punctuation, a standard way to set Japanese: the mark goes past the line length, here by 4.1 mm at most, one character at 12 pt. For a closing bracket, a typesetter would rather move the character before it down a line; hanging keeps the code short and the bracket off the start of the line.
PDFs made on the server
If your server prints HTML to PDF with headless Chrome, for example with Puppeteer, the Japanese is drawn with the fonts installed on the server. Puppeteer's Docker example installs fonts "to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)"; on Debian, the package with Noto Sans CJK is fonts-noto-cjk. Install them in the image you ship, not only in the one you develop in: one project found that its shipped image lacked the font files its PDF templates name, while its development image had them (japanese-vma #317, with dompdf). And set lang="ja" on the page, so Chrome takes the Japanese shapes from a font that also has Chinese ones (the fix).
The rest of a Japanese check
An invoice or a report with the Japanese missing is what your user sends on to someone else. The same pass catches Enter while typing Japanese (the fix), yen charged 100 times over (the fix), kanji drawn in their Chinese shapes (the fix), headings that break in the middle of a word (the fix), postal codes rejected when typed in Japanese (the fix), dates saved one day early in Japan (the fix), address forms in the American order (the fix), phone numbers sent to +81 with the 0 still in (the fix), search that finds nothing in Japanese (the fix), previews cut down to “...” in Japanese (the fix), lists sorted in the wrong order (the fix), email subjects that arrive garbled (the fix), CSV files that open garbled in Excel (the fix), Japanese titles that give an empty URL slug (the fix), file names garbled on upload and download (the fix), dates in the Japanese era that come out wrong (the fix), full-width spaces that get past your checks (the fix), public holidays your app gets wrong (the fix), and vertical text that comes out sideways or upside down (the fix).
Lesstep translates web apps into Japanese and delivers them as one pull request. In Complete (US$249), fonts and line breaks are part of the job, in the PDFs your app makes too: we embed a Japanese font with its bold, keep the files out of your page load, and fix the line breaks, in the same pull request. The first step is a free sample.
Google and Chrome are trademarks of Google LLC; Mozilla is a trademark of the Mozilla Foundation; Debian is a registered trademark of Software in the Public Interest, Inc.; React is a trademark of Meta Platforms, Inc.; Node.js is a trademark of the OpenJS Foundation. This page is not affiliated with or endorsed by any of them.
More: Enter sends the message while typing Japanese · Stripe charges 100 times too much in yen · Kanji in their Chinese shapes · Japanese headings that break in the middle of a word · Forms that reject numbers typed in Japanese · Dates that come out wrong for Japanese users · Address forms in the Japanese order · Japanese phone numbers in forms · Search that finds nothing in Japanese · Length limits that break in Japanese · Japanese lists sorted in the wrong order · Japanese email subjects that arrive garbled · Japanese CSV files that open garbled in Excel · Japanese titles that give an empty URL slug · Japanese file names garbled on upload and download · Japanese era dates that come out wrong · Full-width spaces that get past your checks · Japanese public holidays your app gets wrong · Japanese vertical text that comes out sideways or upside down · Japanese localization checklist · Japanese localization for your web app