jsPDF でブラウザだけで日本語 PDF を生成する
jsPDF 4.2 でブラウザ完結の PDF 生成。標準フォントでは日本語が『エラーなしで文字化けする』理由と、TTF を addFileToVFS / addFont で埋め込む 3 手順(サブセット化で 1MB、PDF には使ったグリフだけ 67KB)、mm/pt/px の座標系とベースライン、y を自分で進めるレイアウト、jspdf-autotable の表と bold 未登録の罠、canvas 画像・ページ番号・Blob プレビューまで。書き換えて試せる demo 2 つ付き。
検証日: 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 型 | 本体同梱 |
| import | import { jsPDF } from "jspdf"(名前付き。default import は非推奨) |
| 任意依存 | html2canvas(doc.html() を使う時)/ dompurify(HTML 文字列を渡す時)/ canvg(SVG を addImage する時) |
| v3 → v4 | 4.0.0 で Node のファイルシステムアクセスが既定で制限(セキュリティ)。ブラウザ用途の API は 2.x から大きく変わっていない |
| ライセンス | MIT |
最小コードは 3 行。座標は 左上原点・単位 mm、text の第 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) - 例外
- 出ない
- 6.8 KB
- 原因
- 標準 14 フォントに CJK 字形が無い
addFileToVFS → addFont → setFont - 例外
- 出ない
- 88 KB(使った字形だけ)
- 準備
- 静的 TTF。可変フォントは静的化
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);
notosansjp-subset.ttf 可変フォントは静的化し、必要な字だけに絞って 1MB 級 addFileToVFS(name, base64) base64 文字列で持つ。1 回読んで使い回す addFont(name, "NotoSansJP", "normal") 第 3 引数はスタイル名。bold は別登録 setFont("NotoSansJP") splitTextToSize もこの字幅で計算 88 KB 使った字形だけ埋め込まれる 押さえどころ:
- 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()は常に pt。unit: "px"は 96dpi 換算(1 mm = 3.78 px)- 用紙サイズは
doc.internal.pageSize.getWidth()/getHeight()で unit 単位の数値が取れる(A4・mm なら 210 × 297)
レイアウトは「y を自分で進める」
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.text も addImage も、紙の下端を超えても 何も起きない(描画命令が紙の外に記録されるだけ)。改ページは自分で判定する。
表は 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()(画像化)か、サーバー側でヘッドレスブラウザに印刷させる方が確実。
関連書籍
TypeScriptとReact/Next.jsでつくる実践Webアプリケーション開発 | tech-book.net
新しいフロントエンドの入門書決定版! 本書はReact/Next.jsとTypeScriptを用いてWebアプリケーションを開発する入門書です
Effective TypeScript(第2版) : 型システムの力を最大限に引き出す83項目 | tech-book.net
急速に普及が進んでいるTypeScriptの実用書! TypeScriptの実用書。TypeScriptは、
関連記事 / 関連 Topic
- pptxgenjs でブラウザだけで .pptx を生成する — スライド編
- ExcelJS で .xlsx をブラウザで生成・ダウンロードする — Excel 書き出し編
- SheetJS で Excel / CSV を読み書きする — Excel 読み込み編
- 体系的に学ぶなら: JavaScript の本 / TypeScript の本(tech-book.net)