tech-book-labs
データユーティリティ · 最終検証 2026-09-05 · jspdf 4.2.1 · 初公開 2026-09-05

jsPDF でブラウザだけで日本語 PDF を生成する

jsPDF 4.2 でブラウザ完結の PDF 生成。標準フォントでは日本語が『エラーなしで文字化けする』理由と、TTF を addFileToVFS / addFont で埋め込む 3 手順(サブセット化で 1MB、PDF には使ったグリフだけ 67KB)、mm/pt/px の座標系とベースライン、y を自分で進めるレイアウト、jspdf-autotable の表と bold 未登録の罠、canvas 画像・ページ番号・Blob プレビューまで。書き換えて試せる demo 2 つ付き。

jspdf pdf report-generation japanese-font browser

検証日: 2026-09-05

使用バージョン: jspdf@4.2.1 + jspdf-autotable@5.0.8(React 19)

対象: 帳票・レポートをブラウザだけで PDF にしたい / jsPDF で日本語を出したら化けた場面

jsPDF で ブラウザ完結の PDF 生成を、日本語込みで動かすまで。この記事の中核は「なぜ日本語が化けるのか」と「TTF 埋め込みの 3 手順」で、pptxgenjs(PowerPoint)・ExcelJS / SheetJS(Excel)に続く ファイル生成 3 部作の PDF 編です。

触って試す

本文を書き換えるとその場で PDF が再生成され、右のプレビューに出る。まず 「日本語フォントを埋め込む」のチェックを外してみてほしい。エラーは何も出ないのに、プレビューの日本語が記号の列に化ける — これが jsPDF で最初に踏む罠で、原因と対処が本記事の前半。表・画像・ページ番号のトグルは後半のレイアウトの節に対応する。

セットアップ(npm 1 行、TypeScript 型は同梱)

npm i jspdf jspdf-autotable
項目
検証バージョンjspdf@4.2.1(2026-03 時点の latest)、jspdf-autotable@5.0.8
TypeScript 型本体同梱
importimport { jsPDF } from "jspdf"(名前付き。default import は非推奨)
任意依存html2canvas(doc.html() を使う時)/ dompurify(HTML 文字列を渡す時)/ canvg(SVG を addImage する時)
v3 → v44.0.0 で Node のファイルシステムアクセスが既定で制限(セキュリティ)。ブラウザ用途の API は 2.x から大きく変わっていない
ライセンスMIT

最小コードは 3 行。座標は 左上原点・単位 mmtext の第 2・3 引数は「左端 × ベースライン」:

import { jsPDF } from "jspdf";

const doc = new jsPDF({ unit: "mm", format: "a4" }); // 210 × 297 mm
doc.text("Hello jsPDF", 20, 20);                      // x=20mm, y=20mm(ベースライン)
doc.save("hello.pdf");                                // ブラウザならダウンロードが始まる

日本語は「エラーなしで化ける」— フォント埋め込みの 3 手順

標準フォントのまま doc.text("日本語の見出し", 20, 30)
eåg,Šž0Æ0¹0È
例外
出ない
PDF
6.8 KB
原因
標準 14 フォントに CJK 字形が無い
TTF を埋め込む addFileToVFS → addFont → setFont
日本語の見出し
例外
出ない
PDF
88 KB(使った字形だけ)
準備
静的 TTF。可変フォントは静的化
同じ doc.text() でも結果が二つに分かれる。左は demo で「日本語フォントを埋め込む」を外した実出力。

jsPDF の標準フォント(helvetica / times / courier …)は PDF ビューア側が持つ Latin 字形を前提にしていて、CJK の字形を持たない。doc.text("日本語", …) は例外を投げず、Latin 用のエンコードでそのまま書き出すので、開くと eåg,Šž0Æ0¹0È のような記号列になる(実測。demo のチェックを外した状態がこれ)。

日本語を出すには TTF フォントを PDF に埋め込む。手順は 3 つで、フォントの base64 文字列を仮想ファイルに置き、名前とスタイルを付けて登録し、使う:

// 1) フォントファイル(TTF)を取得して base64 に
const buf = await fetch("/fonts/notosansjp-subset.ttf").then((r) => r.arrayBuffer());
const b64 = btoa(String.fromCharCode(...new Uint8Array(buf)));   // 大きい場合は分割して連結

// 2) 仮想ファイルシステムに置いて、フォント名 + スタイル名で登録
doc.addFileToVFS("NotoSansJP.ttf", b64);
doc.addFont("NotoSansJP.ttf", "NotoSansJP", "normal");

// 3) 使う
doc.setFont("NotoSansJP");
doc.text("日本語の見出し", 20, 30);
TTF ファイル notosansjp-subset.ttf 可変フォントは静的化し、必要な字だけに絞って 1MB 級
仮想 FS に置く addFileToVFS(name, base64) base64 文字列で持つ。1 回読んで使い回す
名前を付けて登録 addFont(name, "NotoSansJP", "normal") 第 3 引数はスタイル名。bold は別登録
使う setFont("NotoSansJP") splitTextToSize もこの字幅で計算
PDF 88 KB 使った字形だけ埋め込まれる
日本語 PDF の 3 手順(中央の 3 つ)。フォントの大きさは PDF の大きさに直結しない。

押さえどころ:

  • PDF に入るのは実際に使ったグリフだけ。demo のフォントは約 1MB だが、生成される PDF は数十 KB(実測: 本文 + 表で 67〜88KB)。フォントの大きさは PDF の大きさに直結しない
  • フォントは TrueType アウトライン(glyf)の TTF が必要。OTF(CFF)や可変フォント([wght].ttf)はそのままでは使えない。Google Fonts の Noto Sans JP は可変 TTF なので、fontTools で wght=400 に静的化 → 必要な文字だけにサブセット化した(下記)
  • splitTextToSize() は登録したフォントの字幅で折り返し幅を計算するsetFont の前に呼ぶと標準フォントの幅で計算され、日本語では行がはみ出す

座標系を体で覚える(mm / pt / px とベースライン)

スライダーで点を動かすと、右の A4 上で文字がどこに載るかが見える。3 つの事実:

  • 原点は左上、y は下向き。CSS と同じ向きだが、y はベースラインなので文字は指定点の上に載る。y = 0 に書くと紙の外に出て消える
  • unit"pt""px" にすると座標の数値が変わるが、setFontSize() は常に ptunit: "px" は 96dpi 換算(1 mm = 3.78 px)
  • 用紙サイズは doc.internal.pageSize.getWidth() / getHeight() で unit 単位の数値が取れる(A4・mm なら 210 × 297)

レイアウトは「y を自分で進める」

y = 30 mm
クリックで任意の段へ
紙は A4(210×297 mm)。赤線が次に書く y。自動で流れるのは autoTable だけで、text / addImage は紙の外に出ても何も起きない。

jsPDF には CSS のような自動レイアウトが無い。上から順に置き、置いた高さぶん y を足していく:

const W = doc.internal.pageSize.getWidth();
const H = doc.internal.pageSize.getHeight();
const m = 18;                                          // 余白 mm
let y = m;

doc.setFontSize(18);
doc.text(title, m, y + 4);  y += 12;                   // 見出し

doc.setFontSize(11);
for (const line of doc.splitTextToSize(body, W - m * 2)) {
  if (y > H - m) { doc.addPage(); y = m; }             // 下端を超えたら改ページ
  doc.text(line, m, y);
  y += 11 * 0.5;                                       // 行送り(mm)。pt × 0.35 × 行間 の目安
}

doc.textaddImage も、紙の下端を超えても 何も起きない(描画命令が紙の外に記録されるだけ)。改ページは自分で判定する。

表は jspdf-autotable(bold 未登録の罠つき)

表だけは自動で流れる。autoTable(doc, { startY }) で描き、doc.lastAutoTable.finalY で次の位置を受け取る:

import autoTable from "jspdf-autotable";

autoTable(doc, {
  startY: y,
  margin: { left: m, right: m },
  head: [["書名", "分野", "価格"]],
  body: rows,                                          // string[][]
  styles: { font: "NotoSansJP", fontSize: 10 },
  headStyles: { font: "NotoSansJP", fontStyle: "bold" },
});
y = doc.lastAutoTable.finalY + 6;

実測で踏んだ罠: ヘッダ行は既定で bold スタイルを要求する。addFont(…, "NotoSansJP", "normal") しか登録していないと、コンソールに Unable to look up font label for font 'NotoSansJP', 'bold' が並び、ヘッダだけ標準フォントに落ちて日本語が化ける。対処は 2 つ:

  • headStyles.fontStyle"normal" にする(demo はこちら。Regular しか無い TTF は bold 登録しても太くならない)
  • Bold の TTF を別に用意して addFont("NotoSansJP-Bold.ttf", "NotoSansJP", "bold") で登録する。同じ Regular の TTF を bold としても登録すると動くが、グリフが二重に埋め込まれて PDF が肥大する(demo 構成で 88KB → 139KB、実測)

autoTable は行がページ末を超えると自動で改ページし、ヘッダも繰り返す。列幅は内容から自動計算され、columnStyles で固定もできる。

画像・ページ番号・出力

  • 画像: canvas.toDataURL()doc.addImage(dataUrl, "JPEG" | "PNG", x, y, w, h)。w/h は unit 単位。demo の棒グラフは canvas で描いて貼っている。canvas 由来の PNG は jsPDF が非圧縮で格納するため、600×300 の単純な棒グラフでも PDF が 667KB に膨らんだ(実測)。toDataURL("image/jpeg", 0.85) にすると +9KB。PNG のままにしたいなら addImage(…, undefined, "FAST") の compression 引数を試す。SVG を直接貼るには canvg が要る
  • ページ番号: 最後に getNumberOfPages() で全ページを回り、setPage(p) してから text(…, { align: "right" })。書く時点で総ページ数が確定している
  • 出力: save("a.pdf") はダウンロード、output("blob") は Blob(demo は URL.createObjectURL で iframe にプレビュー)、output("arraybuffer") はサーバー送信向け
  • doc.html(): DOM を html2canvas で画像化して流し込む別経路。html2canvas を入れないと Could not load html2canvas で失敗し、Node では document is not defined(実測)。文字が画像になるので検索・コピーできない PDF になる点に注意

サイズの実測(A4 1 枚・本文 + 表)

つまずいたポイント

  • 日本語が記号列になる — 標準フォントには CJK 字形が無い。例外も警告も出ないので気づきにくい。TTF 埋め込み(前述の 3 手順)
  • autoTable のヘッダだけ化ける — bold スタイル未登録。headStyles.fontStyle: "normal" か Bold TTF の別登録
  • 画像を 1 枚貼ったら PDF が数百 KB — canvas の PNG は非圧縮格納。JPEG にするか compression 引数
  • OTF / 可変フォントを addFont しても表示されない — glyf を持つ静的 TTF が必要。fontTools で静的化 + サブセット化
  • splitTextToSize の折り返しがはみ出すsetFont より前に呼んでいる。フォント設定 → 折り返し計算の順
  • 文字が紙の上端に消えるy はベースライン。y = 0 ではなく y = フォントサイズ相当 から始める
  • unit: "px" で座標が合わない — jsPDF の px は 96dpi 換算(0.75pt)。CSS のピクセル座標をそのまま渡すなら unit: "px" + hotfixes: ["px_scaling"] を検討
  • base64 化で「Maximum call stack」String.fromCharCode(...bytes) を 1MB に対して一度に呼ぶと落ちる。demo のように分割して連結する
  • demo は client:only="react" で描画 — Blob URL と canvas はブラウザ前提のため SSR では描画しない

3 部作の使い分け(pptx / xlsx / pdf)

出したいものライブラリ特徴
PowerPoint(.pptx)pptxgenjsスライド単位の API、グラフ・表内蔵
Excel(.xlsx)ExcelJS(書き出し・装飾)/ SheetJS(読み込み・多形式)セル単位、数式・書式
PDF(.pdf)jsPDF(本記事)描画命令を記録。日本語はフォント埋め込み必須、レイアウトは手動

「見た目どおりに固定したい・印刷する」なら PDF、「相手が編集する」なら pptx / xlsx。PDF で HTML の見た目を再現したい場合は doc.html()(画像化)か、サーバー側でヘッドレスブラウザに印刷させる方が確実。

関連書籍

tech-book.net /books/9784297129163

TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発 | tech-book.net

手島 拓也/吉田 健人/高林 佳稀 · 技術評論社

新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開発する入門書です

この本が役立つ理由 — 帳票・レポート生成は業務 Web アプリの定番機能。TypeScript + React で「フォーム入力 → PDF 出力」まで一体で作る際の土台(状態管理・型付け)をこの本で固められる
詳細を tech-book.net で見る
tech-book.net /books/9784814401093

Effective TypeScript(第2版) : 型システムの力を最大限に引き出す83項目 | tech-book.net

Dan Vanderkam/今村 謙士 · オライリージャパン

急速に普及が進んでいるTypeScriptの実用書! TypeScriptの実用書。TypeScriptは、

この本が役立つ理由 — jsPDF のオプション型(unit / format / TextOptionsLight)を安全に扱うには型システムの理解が効く。any に逃げずに書く判断基準を 83 項目で
詳細を tech-book.net で見る

関連記事 / 関連 Topic