1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

pdf-libで日本語の透かしを入れる。drawTextが日本語を描けない問題をCanvas→PNG埋め込みで回避する

1
Posted at

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 に貼るときの drawImageopacity で透過させている。

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 のスライダーも「色」「透明度」を独立に出せる。

そして drawImagerotatedegrees(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 側translaterotate)、透明度は PDF 側drawImageopacity)で分担。色と透け具合を分離できる
  • Canvas を ページと同サイズにして (0,0) 等倍で貼ると位置合わせが楽。drawImagerotate は二重回転回避で degrees(0)
  • ページごとに実寸で生成。サイズ混在PDFでもズレない

「pdf-lib で日本語が出ない」は CJK 圏の定番のハマりどころだが、透かしのように"見た目だけ整えばいい"用途なら、文字を画像にしてしまうのが一番軽い。フォントを抱えずに日本語をPDFへ焼き込めるので、機密書類の透かしもサーバーに送らずローカルで完結できる。

ぱんだツールズ では他にもPDFの結合・分割・圧縮・署名・ページ番号付与など、開発者向けのブラウザ完結ツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?