Extract PDFmark



Extract PDFmark

Extract PDFmark というツールを作成して公開( GitHub, 日本語ドキュメント, CTAN) しました。 Ghostscript で PDF のサイズを低減したいときなどに使えると思います。

Ghostscript を PDF ファイルサイズ低減のために使うと、 ページモードやリンクの宛先名等が保存されません。 本ツールは、元の PDF からページモードやリンクの宛先名を抽出し、 PDFmark として保存することができます。 この PDFmark を元の PDF と一緒に Ghostscript へ入力すると、 ページモードやリンクの宛先名を残したまま、 サイズの小さい PDF を得ることができます。

TeX などで図入りの PDF を作るとき

図も PDF

皆さんは TeX などで図入りの PDF を作るとき、どうしていますか? 昔は図のファイルとして何らかのドローソフトで EPS ファイルを用意し、 pLaTeX → dvips → Acrobat または Ghostscript という流れで処理していたのではないかと思います。 いまどきは、 dvips ではなく dvipdfmx を使うことが多くなってきているでしょうし、 pLaTeX ではなくて XeLaTeX や LuaLaTeX を使うこともあるでしょう。 そうすると、図も EPS で用意するよりは PDF で用意した方がよい、 ということになります。

図 PDF のフォントは?

図にはテキストが含まれていることがありますよね。 こういった図を PDF で用意する場合、 フォントの埋め込みはどうしているでしょうか。 普通に PDF を作ると、 フォントのサブセットが埋め込まれることが多いのではないかと思います。 図が、とにかくたくさんあって、 どれにも同じようなフォントが使われていた場合、 これらを貼り込んだ最終的な PDF はどうなるでしょうか。 なんと 同じフォントがいくつもいくつも重複して埋め込まれて しまいます。 同じフォントなのだから、本当はひとつだけ埋め込めばいい、にもかかわらず、 いくつも重複してしまいます。 これではファイルサイズもムダに増えてしまいますし、 読み込むのに時間もかかるようになってしまうでしょう。 もちろん、図の数が少なければ、重複する数も少なくなるので、 それほど大きな影響もないでしょうけど、 図がたくさんあると影響が出てきてしまいます。

LilyPond の場合

実際、 LilyPond の PDF ドキュメント(現在の開発版では Texinfo + XeTeX で処理したもの) は図として楽譜の断片がかなり大量に使われています。 どの図にも同じ楽譜用フォントが使われているため、 重複が多く影響が出てきてしまっています。

このあたり で議論になっています。

重複を防ぐには

重複してしまう理由

重複してしまう理由の一つには、 それぞれの図 PDF に埋め込まれているサブセットが図ごとに違っているから、 というものがあります。 サブセットは、使われているグリフのみを埋め込むことによって、 PDF のサイズを低減するためのものです。 図によって使われている文字が違えばサブセットも違ってしまうので、 それらを張り付けた最終 PDF には、 同じフォントの異なるサブセットがたくさん埋め込まれている、 という状態になってしまいます。 一つ一つの個別の図 PDF だけでみれば、サイズが低減されいてよいのですが、 全体で見ればまったく最適ではありませんよね。

フルセットにする方法

では、サブセットではなくてフルセットで埋め込めばどうなるでしょうか。 大抵の場合、出力された PDF にはフルセットのフォントが重複して埋め込まれると思います。 これではサブセットの場合よりひどい、と思われるかもしませんが、 フルセットが重複している場合には、すべて同じフルセットなので、 後から重複分をまとめることができます。 この「まとめる」処理に Ghostscript が使えるのです。

ただし、この方法にも欠点はあります。 それぞれの図 PDF にフルセットのフォントが埋め込まれることにより、 処理に必要なディスクサイズが増えてしまうのです。 図が少ないとか、ディスクが大量にあって困っていないとか、 であればいいのでしょうけど、図が大量にあると塵も積もれば山となって、 問題になってしまいます。

LilyPond で、この方法を 提案 したのですが、ただでさえ数 GB ものディスクを必要とする処理に、 さらに追加でディスクが必要になってしまうということもあり、 事実上却下されています。

ディスクサイズを節約する方法

ディスクサイズを節約しつつ、最終 PDF の重複を防ぐ方法はあるでしょうか。 細田は試していないのですが、図 PDF ではフォントを埋め込まないようにして、 TeX などが出力した「フォントが埋め込まれていない PDF」を Ghostscript で処理することにより、フォントを一つだけ埋め込む、という方法が 提案 されています。 フォントファイルを Ghostscript に認識させなければならないので、 場合によっては多少面倒になりそうですが、 ディスクサイズは大幅に節約できそうです。

Ghostscript の問題

いずれの方法にしても、TeX などが出力した PDF を Ghostscript へ読み込ませて、最終 PDF を得る、というものになっています。 ところが、この方法では一つ残念なことがありました。 一旦 Ghostscript を通すと ページモードやリンクの宛先名がなくなってしまう のです。

http://bugs.ghostscript.com/show_bug.cgi?id=696943
http://bugs.ghostscript.com/show_bug.cgi?id=695760

ページモードがなくなると

LilyPond のドキュメントは、Acrobat Reader などで開くと、 左側にアウトラインとかしおりとか呼ばれるものが自動的に開くようになっています。 ですが、Ghostscript で処理したものだと、自動的には開きません。 削除されたわけではないので、手動で開くことはできるのですが、不便です。

リンクの宛先名がなくなると

LilyPond のドキュメントは、あちこちにリンクが貼ってあります。 同じ PDF 内へのリンク(目次のページ番号をクリックすると、 そのページにジャンプするものなど)や、 Web サイトへのリンクは特に問題ないのですが、 他の PDF へのリンクがおかしくなります。 LilyPond のドキュメントは複数の PDF があり、 相互にリンクを貼ってあるところがいくつもあります。 普通はそういったリンクをクリックすると、 ジャンプ先に応じた場所(PDF 内の章や節など)が表示されます。 しかし Ghostscript で処理した PDF へジャンプする場合には、 いつもその PDF の先頭が表示されるようになってしまいます。

PDF 間のリンクは、宛先の PDF ファイル名と、その PDF 内に定義された 宛先名 (named destination) を指定します。 これによって、リンクをクリックすると、宛先の PDF が選ばれて、 その中から宛先名を検索し、指定されている場所が表示されるようになっています。 ところが、Ghostscript で処理すると、 (ジャンプ先側の)宛先名がなくなってしまいます。 その場合、リンクをクリックすると、宛先の PDF が選ばれるまでは正常ですが、 その中から宛先名を検索しても(なくなっているので)見つけられず、 仕方なく先頭を表示するようになってしまうのです。

# named destination は、PDF 間のリンク以外にも、 html から <a href="filename.pdf#nameddestination"> のように 使うこともできますが、これも Ghostscript で処理すると使えなくなります。

そこで Extract PDFmark

そこで Extract PDFmark です。 TeX などが出力した PDF からページモードやリンクの宛先名を抽出できます。 普通に ./configure && make && make check && make install でビルドからインストールまでできると思います。 以下のように使います。

$ extractpdfmark TeX出力.pdf > 抽出したPDFmark.ps

抽出したファイルは PDFmark という形式になっていて、 PDF の情報が記載された PostScript ファイルになっています。 中身は以下のような感じです。

% Extract PDFmark 1.0.0 (with poppler-core)
% https://github.com/trueroad/extractpdfmark/

[ /PageMode /UseOutlines /DOCVIEW pdfmark
[ /Dest (für one) /Page 1 /View [/XYZ 72 769.89 0] /DEST pdfmark
[ /Dest (für two) /Page 2 /View [/XYZ 72 769.89 0] /DEST pdfmark

# PDFmark については Adobe から pdfmark Reference Manual などが出ています。

Ghostscript で処理する際に、この PDFmark も一緒に指定します。 以下のようにします。

$ gs -q -dBATCH -dNOPAUSE -sDEVICE=pdfwrite -sOutputFile=最終.pdf TeX出力.pdf 抽出したPDFmark.ps

これで最終目的の PDF を作ることができました。

Extract PDFmark について

PDF を解釈するために poppler を使っています。 本当は poppler のフロントエンドに poppler-glib を使いたかったのですが、 残念ながら named destination をすべて列挙するインタフェースが無くて、 仕方なく poppler-core フロントエンド?を使っています。 poppler-core にしても、当初そのままでは PDF 1.2 以降で使われる NameTree が C++ の private メンバであるため触ることができず、 強制的に private メンバへアクセスする方法をとりました。 (現在でも poppler 0.48.0 未満の場合は、この方法になります。)

しかし、あまり良い方法ではないため、正攻法として poppler-core へアクセス用インタフェースを追加するパッチと、 さらにそれを利用して poppler-glib へ named destination を列挙するためのインタフェースを 提案 しました。このうち poppler-core へのパッチがマージされ、これが含まれた poppler 0.48.0 がリリースされました。 この場合は正規の方法でアクセスするように実装してあります。

ただ、poppler-core も、本来は内部向けインタフェースらしく、 Extract PDFmark のような外部ツールが使うべきものではありません。 よって poppler-glib が本命なのですが、 残念ながらまだパッチがマージされておらず使うことができません。 実験的に提案中のパッチがマージされた場合に使える実装は用意してあります。

というわけで、都合 3 種類の実装をビルド時に切り替えるようになっています。 起動したときに表示されるメッセージでどの実装が使われているか、 わかるようにしてあります。

Extract PDFmark 1.0.0 (with poppler-core)
poppler 0.48.0 以上の popler-core 正規インタフェース使用
Extract PDFmark 1.0.0 (with poppler-core private)
poppler 0.48.0 未満の popler-core private インタフェース強制使用
Extract PDFmark 1.0.0 (with poppler-glib)
poppler 未マージの poppler-glib インタフェース使用