ai-lab.org
PDFAnvil

PDFAnvil ができるまで — ブラウザ内 pdf-lib で結合・分割・圧縮を成立させる設計

pdf-lib を使ってPDFの結合・分割・軽量化をブラウザ完結で実装したWebサービスの設計記録。並べ替え可能ファイルリスト、ページ範囲指定、メモリ上限の扱いまで解説します。

判断: 改善

PDFAnvil ができるまで — ブラウザ内 pdf-lib で結合・分割・圧縮を成立させる設計

PDFAnvil は、PDFファイルをアップロードせずに 結合・分割・圧縮 できるWebサービスです。 voice-scribe / clip-cast / bg-snap / text-pluck と並ぶ、メディア処理ラボの 5本目の柱。今回は ML を使わない確実動作の領域で、ブラウザ完結の主張を補強しました。

なぜこの形にしたか

PDFツールは「結合」「分割」「圧縮」 で検索需要が極めて広く、 既存サービス (smallpdf / ilovepdf / Adobe online) はほぼ全部 アップロード前提 です。 契約書・領収書・本人確認書類・社外向け資料といった機密度の高いPDFを、 名前も知らない海外SaaSに上げ続けているのが現状。 これを「ブラウザだけで完結する代替」 で塗り替えられる可能性があるなら、ラボの 5本目として置く意味があります。

技術的にも、 pdf-lib という Pure JS のライブラリが結合・分割・再保存をすべて成立させてくれるので、 ML モデルや WASM コアファイルを別途ダウンロードする必要がなく、 初回ロードが軽い (ライブラリ自体 ~1MB)。 ラボの他4本がモデルDLを要する重量級ばかりだったので、 "軽くて即動く" を売りにできる5本目 という位置づけも兼ねています。

visual direction

エンジニアリングワークショップ × ブループリント。

  • スレート系 (#0d1014) を背景に、 ライムグリーン (#a3e635) を主アクセント
  • ヒーロー背景は 製図用方眼ライク な細い罫線で構成し、 シアン系の voice-scribe や暖色系 (clip-cast / bg-snap / text-pluck) と完全に別温度
  • 3モードのタブを最上段に並べて、 「結合 / 分割 / 圧縮」 のサブラベル付きで一目で分かる構成
  • ボタンとアクセントは黒文字 on ライム緑で、 工作機械の操作パネル感を出した

実装の見どころ

1. 単一エントリで 3 操作を扱う runner

pdfRunner.tsmergePdfs / splitPdf / compressPdf を並べて、 すべて同じ PdfProgress コールバック型でUIにステータスを返します。 ライブラリの API はそれぞれ違うのに、UI側からは1本の関数に見える形にしました。

const out = await PDFDocument.create()
for (const file of files) {
  const src = await PDFDocument.load(await file.arrayBuffer(), { ignoreEncryption: true })
  const pages = await out.copyPages(src, src.getPageIndices())
  pages.forEach((p) => out.addPage(p))
}
const bytes = await out.save({ useObjectStreams: true })

2. 並べ替え可能なファイルリスト (結合モード)

結合は順番が結果を決めるので、 ファイルカードに ↑↓ ボタンを置いて 1ステップで並べ替えられるようにしました。 ドラッグ&ドロップによる並べ替えも将来追加できる構造です。

3. ページ範囲パーサ (分割モード)

1-3,5,7-9 のような自然な範囲指定を受け取って、 それぞれ独立した PDFファイルに切り出します。 全角ハイフン () もサポートし、 IME がうっかり日本語にしても通るようにしました。

const range = /^(\d+)\s*[-〜]\s*(\d+)$/.exec(part)

各グループは別ファイル名 (docname-p1-3.pdf, docname-p5.pdf...) で書き出されるので、 分割後の管理もそのまま意味が通ります。

4. 圧縮は誠実なベースラインだけ

PDFDocument.save({ useObjectStreams: true })オブジェクトストリーム + 圧縮XRef で再保存するだけ、 という最も誠実なベースラインで実装しました。 これは「FAT な手作りPDFを軽量化する」 ことには効くものの、 「画像PDFを劇的に小さくする」 ような期待にはほぼ応えません。

ここで嘘をつかないように、 圧縮率が 2% 以下のときは結果パネルに 「ほぼ変化なし」 の説明を表示し、 「画像PDFの強圧縮は今後の拡張」 と書いておく。 過剰な期待を煽らないUXにしています。

{compressResult.savingsRatio <= 0.02 ? (
  <p className={styles.lowEfficiencyNote}>
    ほぼ変化なし: 元のPDFが既に最適化済みだと判断しました。
    本サービスはオブジェクトストリームでの再保存のみ行い、 画像の再エンコードはしません。
  </p>
) : null}

5. メモリ上限の扱い

pdf-lib は全ページをメモリ上に展開するので、 100MB を超えるPDFは厳しい場面が出ます。 FAQ に上限の目安を明記し、 大きいPDFは 「先に分割」 → 個別処理 → 再結合のワークフローを提案しています。

苦労したところ

  • Uint8Array の BlobPart 型衝突 (text-pluck / clip-cast でも経験済) を、 ヘルパー blobOf(bytes) で内側で new ArrayBuffer にコピーして回避。
  • モード切替時に古い結果が残る問題 を、 タブクリック時に clearResults() で初期化することで解消。
  • 圧縮の見かけ上の効果が薄い ことを UX 上ごまかしたくなる衝動を、 ラベルに「再保存」と書き、 効果が薄ければそれを明示する、という方向に倒した。

今後の拡張

  • 画像PDFの強圧縮: ページごとに Image を取り出し → JPEG再エンコード → 埋め込み直し
  • ドラッグ&ドロップ並べ替え: ファイルリストを @dnd-kit/sortable 等で物理的に並び替え
  • ページ削除 / 抽出: 範囲指定の逆 (この範囲を除外したPDFを出す)
  • OCR埋め込み: text-pluck の出力を invisible text として PDF に焼き込む → 検索可能PDFへ
  • 暗号化PDF対応: パスワード入力でロック解除
  • clip-cast との連携: 動画から切り出したフレーム集を1つのPDFに

このサービスから言える事

メディア処理ラボの5本柱が揃いました:

  • voice-scribe (音声→テキスト)
  • clip-cast (動画→トリム)
  • bg-snap (画像→透過)
  • text-pluck (画像→テキスト)
  • pdf-anvil (PDF→PDF)

「重い処理は全部ブラウザで」 という主張を、 ML系4本 + Pure JS系1本という配分で実証しました。 軽い実装 (今回) と重い実装 (前4本) を並べることで、 ラボの方針が「重い処理が ブラウザでできる」 だけでなく 「軽く済む処理はもっと軽くやる」 までカバーしていることを示しています。

Next Action

読んだあとに、そのまま使って確かめる。

この開発ログは PDFAnvil をどう育てているかの記録です。読んだらそのままサービス本体へ戻って、価値を確かめられるようにしています。

Keep Reading, Keep Trying

読むだけで終わらせない主力サービス

改善ログで方向を理解したあとに、そのまま使って価値を確かめやすい主力を並べています。

hack-sim主力

HackSim

/hack-sim

1分で始められるSNS向けハッキング体験シミュレーター

· エンタメ· 公開中· 48 pv
prompt-stock主力

PromptStock

/prompt-stock

使うだけで終わらせず、改善して育てるためのプロンプト実用品ストック

· 改善· 改善して伸ばす· 20 pv

More Logs

次に読みたい改善ログ

記事一覧へ