PDFから「使える」テキストを取り出す(第5回)
昨日の記事では、PDFのコンテンツストリームから文字を読めたことにして、その文字をテキストとして再構築する話をしました。 今日は昨日までの話の締めくくりとして、「PDFごとにカスタムなテキスト取り出し」の話をするつもりだったのですが、その前に文字とコンテンツストリームについて落穂拾いをしておくことにしました。
というのは、昨日までの記事への反応を見ていて、この本のことをちょっと思い出したからです。
- John Whitington 著、村上雅章 訳 『PDF構造解説』(オライリー・ジャパン、2012年5月)
この本、PDFのドキュメント構造を知りたい人が最初に読むにはぴったりだと思います。 自分で簡単なPDFを手書きしながら「PDFの中身がどうなっているのか」を学べるように書かれているので、ドキュメント構造やコンテンツストリームの雰囲気を手軽に体験できる良書です。
しかし、この「自分で簡単なPDFを手書きしながら」って部分に、実用上はどうしても難があるんですよね。 特に、テキストに関していうと、英文の例でも和文の例でも「コンテンツストリームにリテラル文字列を書く」というアプローチをとっているので、これが誤解を与えやすいと思います。 この本のサンプルコードだけでPDFを理解した気になってしまうと、「世の中のPDFには文字データがそのまま含まれている」ような錯覚にとらわれてしまい、この連載で1回目に強調した「PDFの文字は文字ではない」ということが意識できなくなる気がします。
PDFファイルを開いてドキュメント構造をパースし、そこで得られたコンテンツストリームを通常のテキストデータと同じ感覚で読んでも、期待される「文字」は手に入らないのです。 CID/GIDを表現する値からUnicodeの文字を変換して手に入れる必要があります。
そして、実を言うと、罠はこれだけではありません。 PDFのコンテンツストリームにおいて「CID/GIDを表現する値」がそもそも単純ではなく、PDF生成エンジンの実装に依存しているのです。
PDFのコンテンツストリームでテキストを表す方法は生成エンジンごとに違う
たぶん、この記事を読んでいるような人は、PDFのコンテンツストリームを自分で開いてみたことがあるように思います。 そこで目にしたのは、ほとんどの場合、昨日までの例に出てきたのと同じような16進表記のCIDの値の列だと思います。
BT /F1 12.4811 Tf 125.585 -462.55 Td [(#1)] TJ /F2 13.2657 Tf 19.932 0 Td [<0b450a3a0c2403c3029403bb0715037103cd03bb029403ef03da03bf03bd0377062c0ac5>] TJ ET
この例の0b450a3a0c24...
というやつですね。このような方法でコンテンツストリームのテキストが表現されたPDFは「もっともよく見かける」タイプです。
言い換えると、コンテンツストリームにおけるテキストの表現方法はこれだけではありません。
ぼくが今まで遭遇したものだけで、世間には少なくとも次の3種類のテキストの表現方法をもったPDFが存在しています。
- PDFのコンテンツストリームでは、16進表記で文字列を表現することになっています。なので、この方法でテキストを表現する実装が一般的です。
- PDFのコンテンツストリームではリテラルを使えるという話をしましたが、このリテラルの一種で「8進表記の3桁の数字で任意の文字を表せる」というチートがあります。このチートを使ってテキスト中の文字を表現してくる実装があります。
- PDFでは、スラッシュで始まる文字列を「名前」として使うという話を昨日か一昨日かにしたのを覚えているでしょうか。
/Name
みたいなやつです。この表現を使ってコンテンツストリームのテキスト中でUnicode文字を表現してくる実装があります。利用する名前についてはどこかで決められていた気がしますが、忘れました。名前がない文字も、XXXX
をUnicodeのコードポイントとして/uniXXXX
というふうに表現されます。
個人的にはじめて見たときに特に驚いたのは、このうち2つめの「ASCIIの印字可能文字と「エスケープ+8進3桁」によりCID/GIDをテキストとして指定してくるPDF」の存在でした。
わりと歴史が古いPDF生成エンジンの中には、そういう実装になっているものがあるようです。
まあ、どのみち/CIDSystemInfo
を確認してから文字列を見くので、リテラル文字の扱いを仕様どおりに実装していれば混乱はないのですが、ぼくは最初にhpdftを作り始めたときリテラル文字をそのままASCIIとして読むという怠惰なことをしていたので、この手のPDFを始めて見たときには混乱しました。
なんにしても、まず「文字」の原料となる値の取り出しだけで、少なくともこれだけの方法に対応したパーサを書く必要があります。
CMapをどこで手に入れるか
次に、適切なCMapを見つけるという問題があります。
/CIDSystemInfo
で具体的なCMapの種類が指定されている場合には、そのCMapをどこかで入手して参照できるようにしておかなければなりません。
Adobe Japan-1などであれば、Adobeが公開しているものが使えるので、この辺から手に入れておきます。
- adobe-type-tools/cmap-resources
https://github.com/adobe-type-tools/cmap-resources
もし、こういう形で入手できない独自のcmapを利用しているTrue TypeフォントがPDFの中で使われていたりしたら、一体どうすればいいでしょうか? わかりません。たぶん、どうしようもない気がします。
さらにやばいのはType 3フォントです。Type 3、まじやばい。 これは「グリフの形状を表現した完全なPostScript命令」がそのままフォントとして使えるという仕組みで、つまり出自からしてグリフしか存在せず、「文字」がない。 TeX界隈だと、DVIPSというソフト経由で生成されたPDFではType 3フォントが使われることになっているので(DVIPSの仕様のはず)、この理由で文字が取れないPDFファイルはわりと目にします。
一方、PDFに/ToUnicode
が埋め込まれている場合にはむしろ楽で、そこで指示されている材料からCMapを自分で復元するだけです。
まあ、自分でCMapを復元しないといけないので、PDFの中を眺めるだけだと限界がありますが。
ただ、ここにも罠があって、/ToUnicode
エントリから復元したCMapではコンテンツストリームのテキストから文字を取れないことがあります。
たとえば、昨日の例でうまく取れなかった「κ」の文字。
コンテンツストリームではこんなふうに表現されています。
BT /F4 10.5206 Tf 395.991 -462.55 Td[(\024)]TJ...
フォントリソース/F4
を使って、\024
という8進表記の値から、Unicodeの文字を解決すればよさそうですよね(余談ですが、こんな感じでカジュアルに「8進表記の数字3桁によるリテラル」は身近なPDFに登場します)。
そこで、/F4
を表すオブジェクトを見てみると、こんな感じになっています。
$ hpdft -r 59 NML-book.pdf [ /Type: /Font /Subtype: /Type1 /ToUnicode: 2309 /Widths: 2310 /FirstChar: 20.0 /LastChar: 110.0 /BaseFont: /SRDEOM+LucidaNewMath-AltDemiItalic /FontDescriptor: 2353]
2309
に/ToUnicode
があるらしいので、当然、これを見てUnicodeの値を解決できそうな気がします。
$ hpdft -r 2309 NML-book.pdf [ /Filter: /FlateDecode /Length: 247.0, /CIDInit /ProcSet findresource begin 12 dict begin begincmap /CMapName /SRDEOM+LucidaNewMath-AltDemiItalic-UTF16 def /CMapType 2 def /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def 1 begincodespacerange <00> <FF> endcodespacerange 1 beginbfchar <5D> <266F> endbfchar endcmap CMapName currentdict /CMap defineresource pop end end ]
これがCMapの材料です。これから、CMapの仕様にしたがってCMapを再構築すると、こういう対応関係が1つだけ得られます。
(93,"\9839")
これは、「10進表記で93
のテキストはUTF-16の9839
として読めばよい」という意味です。
UTF-16の9839
というのは、「♯
」のことで、実際、このPDFの別の場所のコンテンツストリームには10進表記で93
に相当する8進表記を含むテキストが出てきて、それは文字「♯」として読むことになります。
しかし、このCMapには、\024
つまり10進表記の20
に対応する変換表がありません。
そのため、たとえばAcrobatで開いてこの「κ」をコピペしても「κ」は得られず、10進表記の20
に対応する文字(ASCIIの制御文字のひとつ)しか得られません。
こうなると、なんでpdftotextでこれを文字「κ」として読めるのか、のほうが謎に見えてきます。 いまのぼくにはほんとに謎なんですが、XPdf系の実装は何か特殊なことをしているのかもしれない。 でも、Sumatraでも「κ」が取れるので、ぼくの理解(とAcrobatの実装)がダメなだけかもしれない。
追記:たぶんぼくが手を抜いてるからで、埋め込まれているTrueTypeフォントの中に入ってるcmapを見れ、との情報をいただきました。ありがとうございます。
/ToUnicode に “CMapの逆” の情報がなくても復元できるの,部分埋め込みされているTrueTypeフォントの cmap テーブルにUnicodeコードポイントからGIDへのマッピングが残っていてそれを逆引きしているんじゃないですかね
— 画力・博士号・油田 (@bd_gfngfn) December 5, 2019
そもそもPDFのコンテンツストリームの表現方法に生成エンジンの実装による癖がある
昨日の記事では、「どこに文字が配置されるか」を表現するのにTd
オペレータとTJ
オペレータを使っているPDFの例を見ました。
あのPDFはdvipdfmxというPDF生成エンジンで生成したものなんですが、dvipdfmxはこのような比較的単純なコンテンツストリームの表現でPDFを出してくるようです。
これがmacOS標準のQuartzというエンジンだと、比較的単純なドキュメントであってもTm
というオペレータを多用してくるように見えます。
なのでちょっと「いい感じの改行とスペーシングでテキストを取り出す」のがうまくいかないことがあります。
今日の記事で最初のほうに説明したテキストの表現やCMapの埋め込み方がまちまちなのも、生成エンジンのクセみたいな感じで、いろんなPDFの中身を見ているとなんだかPDFソムリエ的な気分になってきます。
明日はPDFからHTMLを作って本の原稿にした話です
というわけで、今日は「気が向いたら明日の記事で話すつもりだった話」をしました。 明日こそは、ほんとうは今日話すつもりだった、PDFからHTMLを作った話をしたいと思います。