コンテンツにスキップ
getnextpdf.com

PDF の中のテキストが、実はテキストではない理由

PDF を読むとき、あなたには言葉が見えます。しかしファイルは言葉を含んでいません。それが含むのは、座標に図形を描く命令であり、その図形がたまたま文字のように見えるだけです。PDF が 描く ものと、それが 意味する ものとの間のギャップこそが、ページが完璧に見えながらも、コピーするとナンセンスとして出てくる理由です。このページは、そのギャップと、それを埋める小さく別個のマップについて扱います。

PDF に対して テキストとして 行うことすべて — 選択する、コピーする、検索する、検索用にインデックス化する、スクリーンリーダーで読み上げる — は、ファイルが直接には決して保存していない文字を復元することに依存します。その復元が失敗すると、その失敗は目に見えません。ページは依然としてレンダリングされます。誰かが段落をメールに貼り付けて □□□□ を得るまで、あるいは 400 ページの契約書で明らかにそこにある条項を検索して何も見つからないまで、誰も気づきません。

これは高くつく種類のバグです。まさに、あらゆる視覚的レビューを生き延びるからです。見るべきものが何もありません。ドキュメントは権威ありげに見え、そして機械の目的からすれば、沈黙しています。

  • PDF が描くのは グリフ — フォント内の数値コードによって選ばれた視覚的な図形 — であり、Unicode の 文字 ではありません。
  • 同じ図形が異なる文字を意味することもあり、同じ文字が異なるグリフで描かれることもあります。見た目と意味は設計上切り離されています。
  • テキストを取り戻すには、リーダーは描画のパスを 逆向きに たどります。コードから文字へ、です。フォントの /ToUnicode CMap が、その逆ステップのためのルックアップテーブルです(Spec: ISO 32000-2, §9.10)。
  • /ToUnicode がない、あるいは誤っていると、コピー&ペーストは文字化けし、検索は失敗します — ページは依然としてピクセル単位で完璧なのに、です。
  • これは 意味 の問題です。グリフが正しく見えるかどうかは別個の 見た目 の問題であり、Fonts: the hard part で扱われています。NextPDF は正しい /ToUnicode を出力するため、ラウンドトリップは忠実です。

そもそもテキストがどうやってページに載るのか、から始めましょう。コンテンツストリームは「file という単語を書け」とは言いません。フォントを選択し、それから コード の文字列をテキスト描画演算子に渡します(Spec: ISO 32000-2, §9)。各コードはインデックス — フォントプログラム内の位置 — であり、フォントはそのインデックスをグリフのアウトラインに変えて描画します。コード 70 は文字 F ではありません。それは「この フォントのスロット 70」であり、スロット 70 がたまたま F の形をした曲線を保持しているのです。別のフォントを選べば、スロット 70 は雪の結晶かもしれません。

つまりコードは、そのフォントのエンコーディングに対して相対的に のみ意味を持ちます。シンプルフォントの場合、そのエンコーディングはおおよそグリフあたり 1 バイトです。何千ものグリフを必要とする文字体系 — CJK、あるいは全 Unicode のドキュメント — の場合、それでは余地が足りず、PDF は 複合(Type 0)フォント に手を伸ばします(Spec: ISO 32000-2, §9.7)。複合フォントは、マルチバイトのコードを CMap を通じて読み取り、それらを CID(文字識別子)に変え、CID をグリフへマッピングします。それは、一つのフォントが膨大なグリフ集合をアドレス指定できるようにする、エレガントな間接化です。同時にそれは、「何が表示されているか」から「それが何を意味するか」への手がかりが途絶えうる、もう一つの場所でもあります。

今度はそれを逆向きに動かしましょう。テキストを抽出するには、リーダーはコンテンツストリーム内に見つけたコードを取り、文字を復元しなければなりません(Spec: ISO 32000-2, §9.10)。グリフを描いたエンコーディングは 文字からグリフへ 進みました。抽出が必要とするのは グリフコードから文字へ であり、その方向が反転可能である保証はありません。サブセットフォントはグリフを振り直しているかもしれません。複合フォントの CID は私的なものかもしれません。ページ上の図形は、本来的な Unicode の意味を何も運びません。

解決策は、逆マップをフォントとともに同梱することです。そのマップが /ToUnicode CMap です。ドキュメントが使うコードごとに、そのコードが表す Unicode 値(一つまたは複数)を記録します。それがあれば、抽出はきれいなルックアップになります。それがなければ、リーダーはフォントのエンコーディングとヒューリスティクスから推測するしかなく — そして推測こそが、fi がクエスチョンマークになる、まさにその場所です。

  1. Your charactersThe Unicode text you set, for example the word 'file'.
  2. Encoding → codesThe font's encoding turns characters into numeric codes; a composite font routes them through a CMap to CIDs.
  3. Codes → glyphsEach code selects a glyph outline, which is painted to the page. This is the part you see.
  4. Extraction reverses itA reader reads the codes back and looks each one up in /ToUnicode to recover Unicode.
  5. Text againWith a correct /ToUnicode, copy, search, indexing, and screen readers all get the original characters back.
The two directions of PDF text. Rendering goes character → glyph and is what you see. Extraction goes code → character and is what you can copy, search, and read aloud — and it only works when the /ToUnicode map closes the loop.

NextPDF は逆マップを当然のこととして書き出します。複合フォントを埋め込むとき、CIDFontType2 子孫、Type0 親、そしてコードが Unicode へ解決されるようにする /ToUnicode CMap を出力します — これは Fonts: the hard part で説明されているエンコーディング作業です。ここで切り分けておく価値のある点は、その 理由 です。/ToUnicode ストリームは、ページの見た目を正しくするためのものではありません。グリフはそれなしでもすでに正しく見えます。それは、テキストを 抽出可能 に保つ、ただ一つの成果物なのです。

これが最も有名な形で表面化するのは、fi 合字 です。多くのフォントは fi を一つの結合したグリフとして描きます。i の点が f のフックと衝突するからです。ページ上ではそれは一つの図形であり、一つのコードによって描かれます。問題は、それをコピーしたときに何が起こるか、です。

% A /ToUnicode entry that maps the single ligature code to TWO characters,
% so selecting the 'fi' glyph copies out as 'f' then 'i' — not one mystery box.
1 beginbfchar
<0085> <00660069> % code 0x85 -> U+0066 'f' U+0069 'i'
endbfchar

その一つのエントリこそが、「file」の検索がヒットすることと、「file」の検索が合字でレンダリングされたすべての出現箇所を黙って取りこぼすこととの違いです。合字のコードを 2 文字のシーケンス f + i にマッピングすれば、その単語は検索可能でコピー可能になります。それをマッピングしないままにすると、ページは依然として file を完璧に表示しながら、テキストは静かに、機械が読めるものを何も語りません。

これが、「fi」が 1 文字または 2 文字として抽出されうる理由であり、その違いがランダムではない理由です — それは、/ToUnicode マップが何を語るよう指示されたか、そのままです。正しいマップは合字をその文字へと分解し直します。NextPDF はまさにその種のエントリを出力するため、ページ上の合字は、クリップボード上では二つの普通の文字になります。

名前を付けるなら、その罠はこうです。「テキストはレンダリングされているのだから、テキストは問題ない」。レンダリングと抽出は、異なるデータを読む異なる仕組み です。レンダリングはコードからグリフへのパスをたどり、それはあなたの目が確認するものです。抽出は /ToUnicode を通じてコードから文字へのパスをたどり、それはあらゆる機械の利用者が確認するものです。ドキュメントは、構造上それを明らかにする視覚的なものが何もないがゆえに、前者を完璧にこなしながら後者を完全に失敗させることがありえます。

二つ目の、より微妙な罠は、グリフが自分がどの文字なのかを「明らかに」知っていると思い込むことです。知ってはいません。グリフはインデックスを持つ図形です。文字へのマッピングは、生成側が提供しなければならない追加情報 です。生成側がそれを一度も書かなかったなら、どのリーダーもそれを忠実には復元できません — 推測できるだけです。

Faithful text extraction from documents NextPDF writes — edition availability
EditionAvailability
CoreNextPDF emits a correct /ToUnicode CMap for the fonts it embeds, including the ligature and composite-font cases, so the documents it produces are searchable and copyable.
ProNot in this edition
EnterpriseNot in this edition

/ToUnicode マップが運べるのは、ソースフォントが実際に露出している情報だけです。グリフに決定可能な文字がないところ — 純粋に装飾的な記号、Unicode 割り当てのない私用領域のシンボル — では、どんなマップも、もともと存在しなかった意味を発明することはできません。NextPDF は導出できる真実のマッピングを出力します。ギャップを埋めるために文字を捏造することはありません。

このページは、NextPDF が 書き出す ドキュメントについてのものです。/ToUnicode がすでに欠けているか誤っている任意の入力 PDF を対象とする修復ツールではありません。そうしたものからテキストを復元するのは、別個のヒューリスティックな問題です。そして忠実な抽出はアクセシビリティに必要ですが、その全体ではありません。読み上げ 順序、タグ、代替テキストは What makes a PDF accessible で扱われています。

なぜ同じ PDF が、あるリーダーでは問題なくコピーでき、別のリーダーではうまくいかないのか? リーダーごとに、/ToUnicode が欠けているときのフォールバックの仕方が異なります。あるものはフォントの組み込みエンコーディングから推測し、よくある Latin テキストで運よく当たります。あるものは当たりません。正しい /ToUnicode はそのくじ引きを取り除きます — 適合するすべてのリーダーが同じ文字を得ます。

これはフォントが埋め込まれていないことと同じか? いいえ。埋め込みは 見た目 についてのものです — フォントがインストールされていなくてもグリフが描かれるかどうか。/ToUnicode意味 についてのものです — コードが文字へ戻ってマッピングされるかどうか。フォントは完璧に埋め込まれていながら、使える /ToUnicode を持たないこともありえます。それが、検索できないのに見た目はきれい、という障害です。

抽出がコードあたり 2 文字以上を必要とすることはあるか? あります — 合字のケースがまさにそれです。一つのコードが二つの文字にマッピングされます。/ToUnicode エントリは、一つのコードを短いシーケンスにマッピングできます。これが、合字や一部の合成形が、その構成文字として戻ってくる仕組みです。

  • Fonts: the hard part見た目 の側についての対となる記事。埋め込み、サブセット化、そしてエンコーディングがどう構築されるか。このページは同じコインの 意味 の側です。
  • What makes a PDF accessible — 忠実なテキストは一つの要素にすぎません。読み上げ順序、タグ、代替テキストが残りです。
  • What a PDF actually is — フォント、エンコーディング、/ToUnicode ストリームが内部に存在するオブジェクトモデル。
  • グリフ — フォント内の視覚的な図形(アウトライン)。PDF が実際に描くもの。グリフはインデックスを持つが、本来的な文字の意味は持たない。
  • 文字 — Unicode コードポイントによって識別される、書記言語の単位。あなたが意味するものであり、抽出が復元しようとするもの。
  • コード — コンテンツストリーム内の数値で、現在のフォントからグリフを選択するもの。Unicode 文字ではなく、フォントに対して相対的にのみ意味を持つ。
  • CID — 大きな集合の中でグリフをアドレス指定するために複合フォントが使う文字識別子。コードとグリフの間の中間ステップ。
  • 複合(Type 0)フォント — マルチバイトのコードを CMap を通じて読み取り、CID とグリフに到達するフォント。一つのフォントが何千ものグリフをアドレス指定しなければならないときに用いられる。
  • /ToUnicode — ドキュメントが使うコードを Unicode 値へ戻してマッピングする CMap ストリーム。PDF テキストを検索可能・コピー可能・アクセシブルにするもの。
  • 合字 — 二つ以上の文字を一つの図形として描く単一のグリフ(例:fi)。その /ToUnicode エントリが、それをその文字へと分解し直す。