3行まとめ
- PDFに「社外秘」などの日本語テキスト透かしを入れるツールをブラウザ完結で作った
- pdf-lib の
drawTextは 標準フォントだと日本語を描けない(WinAnsi 限定でエラーになる)。日本語フォントの埋め込みは数MB重い - 回避策は Canvas に日本語を描いて PNG 化 → それを画像として PDF に埋め込む。ブラウザのシステムフォントを使うのでフォント同梱ゼロ
PDF に「社外秘」「複製禁止」みたいな透かし(ウォーターマーク)を全ページに入れたい。pdf-lib を使えば PDF はブラウザ内で編集できるが、日本語の透かしを入れようとすると素直にいかない。
ぱんだツールズに作った PDF 透かし追加ツールは、ここを Canvas 経由で回避している。pdf-lib で日本語を扱うときの定番の落とし穴と、その回避策をまとめる。
pdf-lib の drawText は日本語を描けない
まず最初にぶつかる壁。pdf-lib でテキストを描く素直な方法は page.drawText() だが、これに「社外秘」を渡すとエラーになる。
Error: WinAnsi cannot encode "社" (0x793e)
pdf-lib の標準フォント(Helvetica などの14種)は WinAnsi エンコーディングで、扱えるのは Latin-1 の範囲だけ。日本語(CJK)の文字コードはこのエンコーディングに乗らないので、エンコードできずに落ちる。
日本語を drawText で描くには、@pdf-lib/fontkit を登録して日本語 TTF フォントを自前で埋め込む必要がある。だがこれは重い。日本語フォントは全文字を含むと、たとえば Noto Sans JP Regular だけでも約4〜5MB あって、ブラウザ完結ツールのバンドルに乗せるには大きすぎる(サブセット化すれば減らせるが、その仕組みを別途持つ必要がある)。透かしに「社外秘」の4文字を出したいだけなのに、フォント数MBを抱えるのは割に合わない。
回避策:Canvas に描いて PNG にして埋め込む
そこで発想を変える。テキストを「文字」としてではなく「画像」として埋め込む。
ブラウザの Canvas は OS のシステムフォントを使えるので、fillText('社外秘') で日本語をそのまま描ける。これを PNG にして、pdf-lib では embedPng で画像として貼る。pdf-lib にとっては「ただの画像」なので、日本語フォントの問題は発生しない。フォントを1バイトも同梱せずに日本語透かしが入る。
透かし生成部分はこうなる。ページと同じサイズの Canvas を作り、中央で回転させてテキストを描く。
async function createWatermarkPng(
text: string, fontSize: number, color: string,
angleRad: number, pageWidth: number, pageHeight: number
): Promise<Uint8Array> {
const canvas = document.createElement('canvas')
canvas.width = Math.ceil(pageWidth)
canvas.height = Math.ceil(pageHeight)
const ctx = canvas.getContext('2d')
if (!ctx) throw new Error('Canvas context is not available')
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 中央を原点にして回転 → テキストを描く
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate(angleRad)
ctx.font = `bold ${fontSize}px sans-serif`
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
const { r, g, b } = hexToRgb(color)
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`
ctx.fillText(text, 0, 0)
ctx.restore()
return new Promise<Uint8Array>((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) { reject(new Error('Failed to create PNG blob')); return }
blob.arrayBuffer().then((buf) => resolve(new Uint8Array(buf))).catch(reject)
}, 'image/png')
})
}
ポイントがいくつか。
-
Canvas のサイズをページと同じにする。
pageWidth × pageHeightでキャンバスを作っておくと、後で PDF に貼るときに原点(0,0)から等倍で貼るだけで位置がぴったり合う -
回転は Canvas 側でやる。
translate(中央)→rotate(angleRad)→textAlign='center'/textBaseline='middle'で、ページ中央を軸にテキストを斜めに描ける。透かしの「斜め45度」はここで作る -
テキスト自体は不透明な
rgb()で描く。透明度はこの時点では入れない(後述)
透明度は PDF 側の opacity で制御する
透かしは薄く透けてないと本文が読めない。ここで「Canvas 側で rgba の α を下げる」のではなく、PDF に貼るときの drawImage の opacity で透過させている。
const pages = pdfDoc.getPages()
const opacityValue = opacity / 100 // 0〜100% → 0〜1
const angleRad = (angle * Math.PI) / 180 // 度 → ラジアン
for (const page of pages) {
const { width, height } = page.getSize()
const pngBytes = await createWatermarkPng(watermarkText, fontSize, color, angleRad, width, height)
const watermarkImage = await pdfDoc.embedPng(pngBytes)
page.drawImage(watermarkImage, {
x: 0, y: 0, width, height,
opacity: opacityValue, // ← ここで透過
rotate: degrees(0), // ← 回転はCanvasで済んでいるので0
})
}
透明度を PDF 側に持たせる利点は、テキストの「色」と「透け具合」を分離できること。Canvas では狙った色(例:グレー #888888)で不透明に描いておき、透け具合は PDF の描画命令で別に決める。色と透明度が混ざらないので、UI のスライダーも「色」「透明度」を独立に出せる。
そして drawImage の rotate は degrees(0)。回転はすでに Canvas 側で焼き込んであるので、PDF 側で二重に回す必要はない。PNG はページと同サイズで中身が回転済みのテキストなので、ページ全面に等倍で重ねるだけでいい。degrees は pdf-lib が提供する角度のヘルパーで、ラジアンではなく度を渡す。
ページごとに Canvas を作り直す
地味だが大事なのが、ページごとに透かし PNG を作り直していること。ループの中で page.getSize() を取って、そのページの寸法で Canvas を起こしている。
PDF は1ファイル内でページサイズが混在することがある(A4 と A3 が混ざっている等)。全ページで同じ PNG を使い回すと、サイズの違うページで透かしが伸びたりズレたりする。1ページずつ実寸で生成すれば、どのページでも中央にきれいに乗る。透かしは画像として「焼き込み」になるので、テキストレイヤーのように選択してコピー・削除はされにくい(ただし画像オブジェクトなので、Acrobat 等の編集機能で消せないと保証するものではない)。
まとめ
pdf-lib で日本語の透かしを入れるときの要点。
- pdf-lib の
drawText+標準フォントは 日本語を描けない(WinAnsi 限定でcannot encodeエラー)。フォント埋め込みは数MBで重い - 回避策は Canvas に日本語を描いて PNG 化 →
embedPngで画像として貼る。ブラウザのシステムフォントを使うのでフォント同梱ゼロ - 透かしの 回転は Canvas 側(
translate→rotate)、透明度は PDF 側(drawImageのopacity)で分担。色と透け具合を分離できる - Canvas を ページと同サイズにして
(0,0)等倍で貼ると位置合わせが楽。drawImageのrotateは二重回転回避でdegrees(0) - ページごとに実寸で生成。サイズ混在PDFでもズレない
「pdf-lib で日本語が出ない」は CJK 圏の定番のハマりどころだが、透かしのように"見た目だけ整えばいい"用途なら、文字を画像にしてしまうのが一番軽い。フォントを抱えずに日本語をPDFへ焼き込めるので、機密書類の透かしもサーバーに送らずローカルで完結できる。
ぱんだツールズ では他にもPDFの結合・分割・圧縮・署名・ページ番号付与など、開発者向けのブラウザ完結ツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com
この記事は Zenn にも同じ内容を投稿しています。