sunshey

PDFに透かしを入れる——半透明のテキストをページ上に重ねる——は、デスクトップソフトウェアだけの機能のように思われがちですが、pdf-libと少しのcanvas計算で、完全にブラウザベースの透かしツールを作成できます。

この記事では、テキストのレンダリング、回転、マルチページ対応などの実装の詳細を解説します。

クライアントサイドの利点

従来の透かしツールはファイルをアップロードし、サーバー上で処理して結果を返送します。機密文書や個人情報を含む可能性のある文書の場合、これは不必要なプライバシーリスクを生みます。ブラウザベースのアプローチには以下のような利点があります:

  • すべてローカルで処理
  • ファイルをユーザーのデバイスに保持
  • 読み込み後はオフラインでも動作
  • サーバーサイドの帯域コストを回避

技術スタック

  • Vue 3 + Composition API
  • pdf-lib(PDF操作と透かし用)
  • PDF.jspdfjs-dist)(プレビュー表示用)
  • Vite(バンドル用)

テキスト透かしの追加

pdf-libにはカスタムフォント用のPDFDocument.embedFont()メソッドと、テキスト配置用のpage.drawText()が用意されています。以下は、全ページに回転した半透明の透かしを追加する方法です:

<script setup lang="ts">
import { ref } from 'vue'
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'

const file = ref<File | null>(null)
const watermarkText = ref('DRAFT')
const opacity = ref(0.3)
const fontSize = ref(72)
const rotationDeg = ref(-45)
const applying = ref(false)

async function handleFileUpload(selected: File) {
  file.value = selected
}

async function applyWatermark() {
  if (!file.value) return
  applying.value = true

  try {
    const arrayBuffer = await file.value.arrayBuffer()
    const pdfDoc = await PDFDocument.load(arrayBuffer)

    // Embed the Helvetica font — required for correct rendering
    const helveticaFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold)

    const pages = pdfDoc.getPages()

    pages.forEach((page) => {
      const { width, height } = page.getSize()

      page.drawText(watermarkText.value, {
        x: (width - helveticaFont.widthOfTextAtSize(watermarkText.value, fontSize.value)) / 2,
        y: height / 2,
        size: fontSize.value,
        font: helveticaFont,
        color: rgb(128, 128, 128), // mid-gray
        rotate: {
          type: 'rad',
          angle: (rotationDeg.value * Math.PI) / 180,
        },
      })
    })

    const watermarkedBytes = await pdfDoc.save()
    const blob = new Blob([watermarkedBytes], { type: 'application/pdf' })
    downloadBlob(blob, `watermarked_${file.value.name}`)
  } finally {
    applying.value = false
  }
}

function downloadBlob(blob: Blob, name: string) {
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = name
  a.click()
  URL.revokeObjectURL(url)
}
</script>

Enter fullscreen mode Exit fullscreen mode

回転の課題

難しいのは、テキストをその中心で回転させることです。pdf-libは原点(x, y)(文字の左下隅)からの相対位置でテキストを回転します。つまり、-45°で回転させても文字列の中心が合わないのです。

これを解決するには、まずテキストのバウンディングボックスを計算し、ページの中心と視覚的な中心が一致するように位置をオフセットします:

function getCenteredPosition(page: Page, text: string, fontSize: number, font: PDFFont) {
  const { width, height } = page.getSize()
  const textWidth = font.widthOfTextAtSize(text, fontSize)
  const textHeight = fontSize

  // Account for rotation — the bounding box changes when text is rotated
  const radians = (-45 * Math.PI) / 180
  const cos = Math.abs(Math.cos(radians))
  const sin = Math.abs(Math.sin(radians))

  // Rotated bounding box dimensions
  const rotatedWidth = textWidth * cos + textHeight * sin
  const rotatedHeight = textWidth * sin + textHeight * cos

  return {
    x: (width - rotatedWidth) / 2,
    y: (height - rotatedHeight) / 2,  
  }
}

Enter fullscreen mode Exit fullscreen mode

設定の柔軟化

実用的なツールを作るには、ワンサイズフィットオールの方法だけでは不十分です:

設定 範囲 デフォルト 目的
テキスト 任意のUnicode "DRAFT" 表示内容
フォントサイズ 12–200pt 72 視認性の調整
不透明度 0.05–0.8 0.3 控えめさの制御
回転角度 0–360° -45° 角度の好み
Hex/RGB Gray ブランドとの一致
位置 中央/カスタム 中央 配置場所
ページ すべて/範囲/選択 すべて 適用範囲の制御

大規模PDFの処理

100ページを超えるPDFに透かしを入れると、処理に時間がかかることがあります。主な最適化策:

  1. バッチ処理:ページごとの保存を避け、ページ操作をまとめて行う
  2. プログレッシブレンダリング:全ページを処理する前に、最初の数ページだけをプレビュー表示
  3. キャンセル機能:処理途中でユーザーが停止できるようにする

さらなる拡張

シンプルな透かし機能であれば、pdf-libの組み込みdrawText()で十分です。画像ベースの透かし(透過PNGロゴ)が必要な場合は、画像をフォームとして埋め込み、オーバーレイとして描画する必要があります。これは別の記事で取り上げる価値のあるトピックです。

完全なソースコードはこちら:github.com/sunshey/pdf-tool


高度な透かし機能(バッチ処理、ロゴオーバーレイ、スケジュール処理など)が必要な場合は、Wondershare PDFelementをご確認ください。