- 商用PDFツールなしでPDFのテキスト内容を変更するには、圧縮されたストリームを展開し、フォントごとのエンコーディングをたどって元の文言と置換文を同じ方式で置き換える必要がある
- 多くのPDFは実際の文書テキストをプレーンテキストではなく、圧縮ストリームと16進数のように見える文字列として保存しており、
qpdf --qdf --object-streams=disableで編集可能な形に近い状態まで展開できる
- テキストブロックは
BT, Tf, Td, Tj, ET のようなPDFコマンドで構成され、<002a004800570003003600480057> Tj のような値は単純なUnicodeコードポイントではない場合がある
- フォントごとの ToUnicode マッピング を含むPDFであれば、
pdffonts で emb と uni が yes か確認し、/ToUnicode 19 0 R のような参照をたどってエンコーディングテーブルを見つけられる
- この方法はPDF標準の複雑さゆえにすべてのファイルへ一般化するのは難しいが、特定のPDFでは元のエンコーディングを見つけて find-and-replace でテキストを置き換えられる
圧縮されたPDFテキストストリームを展開する
- PDFをテキストエディタで開くと一部の構造は読めても、実際の文書テキストが見えないことがある
- 多くのPDFはテキストデータを圧縮形式で保存している
qpdf を使うと圧縮されたテキストストリームを展開して確認できる
qpdf --qdf --object-streams=disable in.pdf out.pdf
qpdf out-edited.pdf out-recompressed.pdf
- 再圧縮コマンドはエラーを出したが、結果のPDFはPreviewで読めた
PDFコマンド内でテキストを探す
- 圧縮を展開した後も、テキストはPDFコマンドブロックの中に残っている
- 基本的なテキストブロックは次のような形になる
BT
/Font_0 12 Tf
288 720 Td
<002a004800570003003600480057> Tj
ET
- PDF Reference によれば、このブロックは次の処理を順番に行う
- テキストオブジェクトを開始する
- 使用するフォントとサイズを設定する
- ページ上の開始位置を指定する
- その位置に文字列のグリフを描画する
- テキストオブジェクトを終了する
テキストがそのまま読めない理由
<002a004800570003003600480057> のような値は16進文字列のように見えても、単純なUnicodeコードポイントの一覧ではない場合がある
- PDFにはエンコーディングの指定方法が多く、ファイル内にユーザー定義エンコーディングを含められる
- こうしたエンコーディングの多くはUnicodeコードポイントへマッピングできる
- この手順は、作業対象のPDFが埋め込みエンコーディングを含んでいる場合を前提としている
フォントとUnicodeマッピングを確認する
- PDFのテキストエンコーディングは特定のフォントと結び付いている
pdffonts はPDF内のフォントとエンコーディング関連情報を表示する
$ pdffonts sample.pdf
name type emb sub uni prob object ID
------------------------------------ ----------------- --- --- --- ---- ---------
CLDQZB+TrebuchetMS,Bold CID TrueType yes yes yes 9 0
YQBAIZ+TrebuchetMS CID TrueType yes yes yes 10 0
- ここで確認すべきフィールドは次のとおり
emb: フォントがPDFに埋め込まれていることを意味する
uni: Unicodeマッピングがあることを意味する
- 両方が
yes なら、必要なマッピングを見つけられる可能性がある
- 例の
/Font_0 のようなフォント識別子は、場合によっては pdffonts 出力のフォント名に対応する
- その事例では
/Font_0 が CLDQZB+TrebuchetMS,Bold を指していた
ToUnicode マッピングテーブルをたどる
- テキストに使われているフォントの完全な名前を見つけた後、PDF内部でその名前を検索する
- 特定の場所で次のような参照を見つけられることがある
/ToUnicode 19 0 R
- この値はエンコーディングテーブルのオブジェクトIDを指している
- その後
19 0 obj を検索すると、該当テーブルを見つけられた
エンコーディングテーブルを使ったテキスト置換
- マッピングテーブルの中核部分は次のような形になっていることがある
38 beginbfrange^M
<0036><0036><0053>^M
<0057><0057><0074>^M
<0044><0044><0061>^M
<0048><0048><0065>^M
<0050><0050><006D>^M
...
- この例は、ユーザー定義エンコーディングポイントの範囲をUnicodeポイントへマッピングしている
- ここでは範囲が1文字だけになっている
0036 は Unicode ポイント 0053 にマッピングされる
- テーブル形式が異なる場合は ToUnicode mapping file tutorial を参照できる
- 自動変換のために、Pythonでテーブルを辞書に変換し、簡単なエンコード/デコード関数を作成した
- 最終的な置換の流れは次のとおり
- 置き換えたい元テキストのユーザー定義エンコーディング値を確認する
- 置換テキストを同じ方法でユーザー定義エンコーディングする
- PDF上で find-and-replace により元の値を置き換える
1件のコメント
Hacker News のコメント
PDF 仕様はかなり荒っぽい。最近いちばん気に入っている豆知識は、重なり合う要素をレンダリングするときに Photoshop のすべてのレイヤーブレンドモードをサポートしていること
2番目に気に入っているのは、後ろに追加されたコンテンツが前のコンテンツを修正できるため、1つのファイル内に表現された異なるバージョンをすべてフォレンジック的に調べる必要があるという点
DRM のむなしさを示す例でもある。仕様にはパスワードベースの暗号化があり、「所有者」と「ユーザー」のパスワードを別々に設定でき、「印刷禁止」「テキストコピー禁止」のようなビットフィールドもある。しかし文書を読むには結局復号しなければならないので、「ユーザー」パスワードで非準拠ツールから暗号化された PDF を開き、暗号化されていないバージョンとして保存すれば、編集可能な同等物が得られる
[1] https://blog.adobe.com/en/publish/2022/01/31/20-years-of-tra... の “More than just transparency” セクション
[2] https://blog.didierstevens.com/2008/05/07/solving-a-little-p...
[3] https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandard... の61ページ
[4] 例えば pypdf ライブラリを使うスクリプト
当時有名だったクラッキングチュートリアルの1つは、権限を検査する関数にパッチを当てて公式リーダーの DRM を外すというものだった
この話題は定期的に出てくるが、多くの人は PDF を突破しにくいバイナリ形式だと思っている。でも実際はそうではない
PDF は異なる型のオブジェクトグラフであり、型そのものは公式仕様にきちんと説明されている。趣味でそれを読むという苦しい嗜好がある
いつも勧めているのは、筆者のように PDF を圧縮データのないバージョンへ変換することだ。好みのツールは mutool で、
mutool clean -d in.pdf out.pdfを使えばよい。その後に探ってみると、思ったより多くの部分を追えることに驚くはずだ記事では、ページオブジェクトを見てリソースを確認する段階が抜けていた。コンテンツストリームで使われるフォント名が実際の下位オブジェクトと結び付けられる場所がそこだ
もう1つ抜けていた重要な点は、ほとんどのフォントが PDF 内にサブセットとして入るということだ。必要なグリフだけがフォントに残される。再エンコードは普通ここで起きるようで、ToUnicode はテキストコピーや PDF 検索を可能にするために維持される。ユーザーにとってあるとうれしい要素であり、私の経験上はたいてい存在し、概ね正しい
そもそも読み取り専用フォーマットを作るという設計思想からして間違っていた。人々が最初に求める機能は明らかではないか
クラウドプロバイダーなしでネイティブにうまく処理する方法について、ほかに知見があるか気になる。特に表を扱う場合
こうすればできる:
pdf2ps a.pdfで PostScript のa.psに変換vim a.psで PostScript を直接編集ps2pdf a.psで再び PDF に変換組み込み JavaScript やアニメーションのようなものがある複雑な PDF は、この往復の後で正しく動作しないことがある。それでも 普通の文書ならかなりうまく動く。ウォーターマークの削除、単語や数字の変更は簡単にでき、間隔の修正はより難しい。もちろん PostScript をある程度知っている必要がある
出力が正しくても、ページの並べ替え、郵便機械処理と郵便仕分け用バーコードの適用、レポート生成が必要だった。通常は Perl やほかのツールでページからテキストをかき出して住所を取得しており、PDF より PS のほうが概してはるかに簡単だったが、「セキュリティ」PDF に正しいグリフマッピングがない場合のように、非常に不安定なケースもあった
最悪の場合、顧客が単一文書の PDF を送ってきて、それらを結合すると出力物に サブセットフォントが爆発的に増え、プリンタのメモリを埋め尽くして停止させた。その仕事を辞める時点でも、同じ元フォントから来たサブセットフォントを統合・結合する有用なツールはなかったように思う。できるはずだし、とても役に立っただろうが、調べる時間も知識もなかった
正直なところ、PDF の複雑さで得をしているのは マルウェア作者だけのように見える
重要な点が抜けているように思う。PDF の末尾には、ファイル内の複数オブジェクトの バイトオフセットを保存する表、つまり 相互参照表がある
ファイル内を修正すると通常これらのオフセットが変わり、ファイルが壊れる。記事では数字を別の数字に置き換えることだけに関心があったようで、そのため位置が変わらなかったのかもしれない
しかし一般には、ファイルの途中で追加・削除・修正を行うと xref 表を再計算する必要があるため、テキストを直接編集するよりライブラリを使うほうがずっと簡単になる
qpdf は元の PDF 構造を再構築し、オブジェクト番号もそのまま維持しようとするようだが、オフセットは完全に再計算される
作者たちがいつか話してくれるなら、なぜフォーマットがそうなったのか読んでみたい。最初は完全なテキスト形式として意図していたが、後からバイナリのほうが効率的だと判断したのだと思う。ただし xref 表の項目長を固定しなければならない点は、その仮定とも少し奇妙に噛み合っている
mutool cleanのようなものを実行して直すのが一番簡単だ。内容から完全に導出できるので安全だ長ささえ保てば、バイナリはいくらでも変更できる
昔使っていたあるソフトウェアはサーバーで認証していたが、処理はすべてクライアント側で行われていた。クライアントがサーバーに直接 SQL を実行する方式で、サーバーはこのクライアントが購入したライセンス数を超えていないかだけを確認していた
逆アセンブラにかけてチェックしている箇所を見つけ、その箇所をそのまま JMP に変え、余った領域を NOP で埋めることができた
qpdf の
--qdfモードを使うときに重要な手順が抜けているように思う。編集が終わった後は、fix-pdf ユーティリティにファイルを通して、すべてのオブジェクトオフセットを再計算し、ファイル末尾の相互参照表を再構築する必要がある。ただし、バイトを追加または削除せず、その場で置き換えただけなら例外だ面白い PDF の事実トップ 3 は次のとおり
[0] 例: https://lab6.com/2
PDF の旅を続けながらさらに学びたいなら、最近まとめたファイル解剖の文書を読める:
https://pdfsyntax.dev/introduction_pdf_syntax.html
PDFについて人々がよく見落とすのは、ある意味ではWord文書よりも画像形式に近いという点です。文書編集におけるWord文書、PDF、画像は、音楽におけるDAWプロジェクト、MIDI、MP3のようなもので、ソフトウェアにおけるJavaソースコード、JVMバイトコード、純粋なx86機械語にも似ています。
PDFファイルの主な目的は、何を表示・印刷するかを、実際の画像よりはるかに少ないバイト数で完全に明確に伝えることです。文書作成者が文書構造のパターンを知っていることを利用し、適切に表現すれば実際の画像圧縮アルゴリズムよりずっと高い圧縮率になります。たとえば実際のフォントにアクセスできるなら、各文字の出現を画像の一部として入れて圧縮アルゴリズムが繰り返しをうまく減らしてくれることを期待するより、「これらの文字をこの座標にこの間隔で置け」と指示するほうがよいわけです。どの文字がどの単語に属するのか、さらにはどのUnicodeコードポイントがどのフォントグリフに対応するのかも、文書画像を効率よく伝えることだけが目的なら基本的には重要ではありません。
編集可能な文書では、表現だけでなくコンテンツの意味構造がはるかに重要になります。テキストの特定の区切りが複数のスペースなのか、表の次の列なのか、画像のせいで生じた奇妙なページ配置なのかが重要です。各ページ下部のテキストが、作成者が何度も入れたものなのか、一度入力してフッターに設定したものなのかも重要です。新しい段落を追加してページレイアウトを変えなければならないなら、このページ最後の段落が脚注なので次のページへ送ってはいけない、という事実が重要になります。セクション見出しが別ページへ移動したら目次が自動更新されるべきで、作成者が手入力しただけのテキストであっては困ります。
プリンターや画面の立場からすれば、こうしたことは何の関係もありません。指示されたとおりに印刷または表示すればよいだけです。PDFでは脚注、セクション見出し、フッター、目次が特別である必要はなく、意味のない書式が適用されたテキストである場合もあります。そのため、表示や印刷以外の目的でPDFを使おうとすると、100%正確にはなり得ません。もちろんこれを補おうとする取り組みはあり、PDF生成プログラムは望むメタデータを自由に入れられますが、必須ではありません。
これがPDFの作者たちが意図したメンタルモデルではないかもしれませんが、PDFがなぜあのような形なのかを理解するうえで有用な見方です。
PostScriptプリンターで出力すれば正確に同じに見え、Type 1フォントを使うとさらに良くなることもありました。
これをやろうとする人は、PDFの本質を見落としています。PDFはページ記述形式なので、文書構造ではなくページ上の表示の痕跡だけを表します。
PDFを編集しようとするのではなく、PDFを生成した元文書を編集すべきです。
いつの間にか、編集するために作った文書をそのまま送るのが「プロらしくない」ことになってしまい、結局こういう状況になっています。
任意ですが、アクセシビリティのために推奨されています。たとえばPDF/UAがあります。[1]の14.7〜14.8章を参照してください。
[1] https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandard...
PDFのテキストを直接変えることはできません。組版担当者がその変更に気づけないからです。Acrobatのマークアップツールはひどいわけではありませんが、紙と赤鉛筆を使っていた時代ほどではありません。Acrobatの「pencil」ツールを使わない限りは、ですが。ここが改善されるといいですね。
20年前、新聞社で印刷版担当として働いていました。100万ドルするKodakの版「プリンター」が2台ありましたが、プリンターという言い方は正確ではありません。記憶が正しければ、版の乳剤が紫外線を浴び、化学浴槽で溶ける方式でした。
Kodakの機器は頻繁に失敗し、上司はPostScriptやEPSファイルを直接開いて、レイアウトソフトウェアが送ってきたヘッダーやおかしな部分を直していました。するとまた正常に動きました。ちなみに、私たちの巨大なドイツ製オフセット輪転機はLinuxで動いていました。
上司の名前はBillだったと思います。17歳だった私をSigur Rosのコンサートに連れて行ってくれた良い人でした。PDFとは関係ない話が2つも出てしまいましたね。
良い記事です。ここ5年ほどDocSpring [1]を作りながらPDF仕様をたくさん読んできましたが、それでもまだ表面を少しなぞっただけという感じです。qpdfは素晴らしいツールですし、もう一つ好きなツールとして、PDF構造を深く掘り下げられるRUPS [2]があります。
[1] https://docspring.com
[2] https://github.com/itext/i7j-rups