2 ポイント 投稿者 GN⁺ 2023-09-05 | 1件のコメント | WhatsAppで共有
  • 商用PDFツールなしでPDFのテキスト内容を変更するには、圧縮されたストリームを展開し、フォントごとのエンコーディングをたどって元の文言と置換文を同じ方式で置き換える必要がある
  • 多くのPDFは実際の文書テキストをプレーンテキストではなく、圧縮ストリームと16進数のように見える文字列として保存しており、qpdf --qdf --object-streams=disableで編集可能な形に近い状態まで展開できる
  • テキストブロックは BT, Tf, Td, Tj, ET のようなPDFコマンドで構成され、<002a004800570003003600480057> Tj のような値は単純なUnicodeコードポイントではない場合がある
  • フォントごとの ToUnicode マッピング を含むPDFであれば、pdffontsembuniyes か確認し、/ToUnicode 19 0 R のような参照をたどってエンコーディングテーブルを見つけられる
  • この方法はPDF標準の複雑さゆえにすべてのファイルへ一般化するのは難しいが、特定のPDFでは元のエンコーディングを見つけて find-and-replace でテキストを置き換えられる

圧縮されたPDFテキストストリームを展開する

  • PDFをテキストエディタで開くと一部の構造は読めても、実際の文書テキストが見えないことがある
  • 多くのPDFはテキストデータを圧縮形式で保存している
  • qpdf を使うと圧縮されたテキストストリームを展開して確認できる
    • Mac では homebrew formula がある
    • Linux ディストリビューションでも関連ツールを利用できる
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_0CLDQZB+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件のコメント

 
GN⁺ 2023-09-05
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 を外すというものだった
    • 方法: https://www.cyberciti.biz/faq/removing-password-from-pdf-on-...
    • サポートされているブレンドモードは単なる Porter-Duff 合成モードではないのか?やり過ぎに見えるかもしれないが、他のレンダリングパイプラインが提供するものと非常によく対応しており、PDF を生成する作業量を減らすのにかなり役立つ
    • 権限フィールドを掘っていくと、一部の PDF 作成ツールが仕様を守っていないやり方や、PDF リーダーやライブラリごとに存在するかもしれないし存在しないかもしれない回避処理を見つける、いわゆる沼に入り込むことになる
    • 公平に言えば、テキストのコピーを防ぎたいなら、フォントの ToUnicode マッピングを抜くのがいちばん簡単だ。そうすると人々が手作業で作り直さなければならなくなる
  • この話題は定期的に出てくるが、多くの人は PDF を突破しにくいバイナリ形式だと思っている。でも実際はそうではない
    PDF は異なる型のオブジェクトグラフであり、型そのものは公式仕様にきちんと説明されている。趣味でそれを読むという苦しい嗜好がある
    いつも勧めているのは、筆者のように PDF を圧縮データのないバージョンへ変換することだ。好みのツールは mutool で、mutool clean -d in.pdf out.pdf を使えばよい。その後に探ってみると、思ったより多くの部分を追えることに驚くはずだ
    記事では、ページオブジェクトを見てリソースを確認する段階が抜けていた。コンテンツストリームで使われるフォント名が実際の下位オブジェクトと結び付けられる場所がそこだ
    もう1つ抜けていた重要な点は、ほとんどのフォントが PDF 内にサブセットとして入るということだ。必要なグリフだけがフォントに残される。再エンコードは普通ここで起きるようで、ToUnicode はテキストコピーや PDF 検索を可能にするために維持される。ユーザーにとってあるとうれしい要素であり、私の経験上はたいてい存在し、概ね正しい

    • 「趣味で読む」ならサディストではなくマゾヒストに近い。仕様策定に参加していたり、他人に読ませるようにしていたりしたならサディストだっただろうが
    • Adobe があまりにも扱いにくいフォーマットを設計したため、本来なら基本的な作業であるべきことを誰かがやり遂げると人々が驚くのは残念だ
      そもそも読み取り専用フォーマットを作るという設計思想からして間違っていた。人々が最初に求める機能は明らかではないか
    • 苦痛を受けることに喜びを感じるならマゾヒストで、他人に苦痛を与えるのが好きならサディストだ。人々を助けようとしている側に見えるので前者に近い。mutool の助言はありがたく受け取る
    • PDF 解析には Amazon Textract に大きく依存している
      クラウドプロバイダーなしでネイティブにうまく処理する方法について、ほかに知見があるか気になる。特に表を扱う場合
  • こうすればできる:
    pdf2ps a.pdf で PostScript の a.ps に変換
    vim a.ps で PostScript を直接編集
    ps2pdf a.ps で再び PDF に変換
    組み込み JavaScript やアニメーションのようなものがある複雑な PDF は、この往復の後で正しく動作しないことがある。それでも 普通の文書ならかなりうまく動く。ウォーターマークの削除、単語や数字の変更は簡単にでき、間隔の修正はより難しい。もちろん PostScript をある程度知っている必要がある

    • 以前勤めていた郵便処理業者では、一部の 印刷準備作業をほぼこのような方法で行っていた。通常はきちんとしたツールで印刷用文書を作っていたが、たまに顧客が自分のほうがよく分かっていると思って PDF を送ってくることがあり、たいてい手間が増え、エラーの可能性も高かった
      出力が正しくても、ページの並べ替え、郵便機械処理と郵便仕分け用バーコードの適用、レポート生成が必要だった。通常は Perl やほかのツールでページからテキストをかき出して住所を取得しており、PDF より PS のほうが概してはるかに簡単だったが、「セキュリティ」PDF に正しいグリフマッピングがない場合のように、非常に不安定なケースもあった
      最悪の場合、顧客が単一文書の PDF を送ってきて、それらを結合すると出力物に サブセットフォントが爆発的に増え、プリンタのメモリを埋め尽くして停止させた。その仕事を辞める時点でも、同じ元フォントから来たサブセットフォントを統合・結合する有用なツールはなかったように思う。できるはずだし、とても役に立っただろうが、調べる時間も知識もなかった
    • PDF に JavaScript とアニメーションを入れられるなら、その中に フロントエンドを作るのを妨げるものは何だろう? PDF でできることの境界が気になる
      正直なところ、PDF の複雑さで得をしているのは マルウェア作者だけのように見える
  • 重要な点が抜けているように思う。PDF の末尾には、ファイル内の複数オブジェクトの バイトオフセットを保存する表、つまり 相互参照表がある
    ファイル内を修正すると通常これらのオフセットが変わり、ファイルが壊れる。記事では数字を別の数字に置き換えることだけに関心があったようで、そのため位置が変わらなかったのかもしれない
    しかし一般には、ファイルの途中で追加・削除・修正を行うと xref 表を再計算する必要があるため、テキストを直接編集するよりライブラリを使うほうがずっと簡単になる

    • だから qpdf でデコードして、後で再びエンコードするのだ。そうすれば qpdf がその部分を処理してくれる
      qpdf は元の PDF 構造を再構築し、オブジェクト番号もそのまま維持しようとするようだが、オフセットは完全に再計算される
    • そこが PDF 仕様で最も奇妙な部分だと思う。バイナリとテキストが混在しており、バイトオフセットはテキストで指定されている
      作者たちがいつか話してくれるなら、なぜフォーマットがそうなったのか読んでみたい。最初は完全なテキスト形式として意図していたが、後からバイナリのほうが効率的だと判断したのだと思う。ただし xref 表の項目長を固定しなければならない点は、その仮定とも少し奇妙に噛み合っている
    • 私の経験では、xref 表をそのまま壊して mutool clean のようなものを実行して直すのが一番簡単だ。内容から完全に導出できるので安全だ
    • コンパイル済みバイナリを編集するのとかなり似ている
      長ささえ保てば、バイナリはいくらでも変更できる
      昔使っていたあるソフトウェアはサーバーで認証していたが、処理はすべてクライアント側で行われていた。クライアントがサーバーに直接 SQL を実行する方式で、サーバーはこのクライアントが購入したライセンス数を超えていないかだけを確認していた
      逆アセンブラにかけてチェックしている箇所を見つけ、その箇所をそのまま JMP に変え、余った領域を NOP で埋めることができた
  • qpdf の --qdf モードを使うときに重要な手順が抜けているように思う。編集が終わった後は、fix-pdf ユーティリティにファイルを通して、すべてのオブジェクトオフセットを再計算し、ファイル末尾の相互参照表を再構築する必要がある。ただし、バイトを追加または削除せず、その場で置き換えただけなら例外だ
    面白い PDF の事実トップ 3 は次のとおり

    1. PDF 文書は通常 8 ビットのバイナリファイルだが、ASCII85 フィルタを使うと、画像まで含めて有効な UTF-8 平文の PDF を作れる
    2. PDF は 3D オブジェクト、JavaScript、内蔵 Flash オブジェクト内の動画、見えない注釈など、奇妙な機能を非常に多く許可している。PDF/A ははるかにまともで安全なサブセットだ
    3. PDF 仕様では、ウィジェット、たとえばフォームコントロールを XHTML と CSS のサブセットである リッチテキストで書けるようにしている。ただしこの機能は、公式の Adobe Reader 以外ではサポートが非常にまれだ
      [0] 例: https://lab6.com/2
    • 編集が終わった後は、記事の前半で説明されているように、引数なしで qpdf に通せばよい。そうすればデータを再圧縮し、xref 表も作り直してくれる。別のツールは不要だ
  • 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がなぜあのような形なのかを理解するうえで有用な見方です。

    • PDFの本来の目標は、移植可能な印刷忠実度の高いコピーを作ることでした。本当のWYSIWYGです。PDFファイルをレーザープリンター、ライノタイプ、画面のどこへ送っても同じに見える必要がありました。
      PostScriptプリンターで出力すれば正確に同じに見え、Type 1フォントを使うとさらに良くなることもありました。
  • これをやろうとする人は、PDFの本質を見落としています。PDFはページ記述形式なので、文書構造ではなくページ上の表示の痕跡だけを表します。
    PDFを編集しようとするのではなく、PDFを生成した元文書を編集すべきです。

    • 人々が私に編集したいPDFを送ってくるのをやめたら、私もPDF編集をやめます。
      いつの間にか、編集するために作った文書をそのまま送るのが「プロらしくない」ことになってしまい、結局こういう状況になっています。
    • PDFは論理的な文書構造情報を含める機能もサポートしています。Tagged PDFと呼ばれるものです。
      任意ですが、アクセシビリティのために推奨されています。たとえばPDF/UAがあります。[1]の14.7〜14.8章を参照してください。
      [1] https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandard...
    • ここで「すべきではない」と言ってもあまり意味がありません。現実には、何らかの形でPDF編集を求められる状況が非常に多いからです。
    • 難しいことの一つは、元文書に戻して反映すべき校正を示すことです。組版担当者から校正刷りを受け取り、修正してもらうように印を付ける必要があります。
      PDFのテキストを直接変えることはできません。組版担当者がその変更に気づけないからです。Acrobatのマークアップツールはひどいわけではありませんが、紙と赤鉛筆を使っていた時代ほどではありません。Acrobatの「pencil」ツールを使わない限りは、ですが。ここが改善されるといいですね。
    • 「ページ記述形式」であり、文書構造ではなくページ上の表示だけを表すのなら、名前はPortable Document FormatではなくPage Description Formatにすべきだったのではないでしょうか。
  • 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