▍AI Dev Lab
PDFAnvil

PDFAnvil ができるまで — pdf-lib で結合・分割・圧縮する

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

·decision改善·stage公開中

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本目 という位置づけも兼ねています。

#見た目の方針

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

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

#実装の見どころ

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

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

ts
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 がうっかり日本語にしても通るようにしました。

ts
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にしています。

tsx
{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 を実際に動かす。

この開発ログは PDFAnvil をどう作ったかの記録です。 読み終わったらそのままサービス本体へ戻って、 実物で価値を確かめてください。

[ ./related_logs ]

関連する開発ログ

all logs →
ShadowTwin

立体パズルの別解を作る:同じ影と最少の積み木

ShadowTwinの開発記録。3方向の図から別々の最少配置を2つ探す仕組み、5問の独立全探索、スマホの横並び比較、保存・再編集と入力検査を解説します。実照明の計算や能力診断ではありません。

read log →
SlitCard

スリットアニメーションを作る:4コマの合成と透明原稿の設計

SlitCardの開発記録。4コマの帯を交互に合成し、すき間と黒帯で別のコマを見せる仕組みを解説。同縮尺の台紙・透明シートSVG、50mm確認線、手動プレビュー・初期非表示、再編集と実出力検査を整理します。

read log →
WindowLight

日当たりシミュレーションを作る:窓の光を床へ投影する幾何モデル

WindowLightの開発記録。太陽位置近似から窓の4隅を床へ投影し、凸多角形を部屋で切り取る設計を解説。国立天文台の16時刻との限定照合、大気差・低高度・実際の採光との違い、SVGと設定の再編集を整理します。

read log →
GridLights

電源構成の比較を作る:割合と実量を100個の電球で読み解く

電源構成比較GridLightsの開発記録。12か国の年次発電データを最大剰余法で100個の電球に変換し、割合とTWhを比較する設計を解説。欠損を0補完しない7分類、予想と実値、出典付きSVGと実データ検証を整理します。

read log →
SoundLoom

音のお絵かきを作る:実WAVから絵を測り直す設計

音のお絵かきの開発記録。描画を正弦波のPCMとWAVへ変換し、保存音声の周波数をGoertzel法で測って比較する仕組みを解説。音量・測定窓・量子化の限界と、編集・離脱時の古い結果を抑止する検証を整理します。

read log →
MazeLoom

宝探し迷路を作る:寄り道まで含めた最短ルートの探索

宝探し迷路の開発記録。通路を木として生成し、現在地と3つの宝の取得状態を組み合わせて最短ルートを探す仕組みを解説。問題コードの版固定、画面と解答付きPDFの一致、手数と印刷の検証を整理します。

read log →