以下の内容はhttps://zrbabbler.hatenablog.com/entry/20141230/1419927594より取得しました。


画像データを TeX ソースに埋め込む話

アッ、何か面白そうなことをやっているゾ!

EPS ファイルを経由させると Ghostscript のアレな話が絡んできてとってもとってもアレな感じ。pdfTeX の機能を使いまくって、中間ファイルの生成なしでやりたいね!

というわけで、pdfTeX の「PDF オブジェクトを直接書き出す機能」を使って「ソース中に書かれた画像のダンプデータから画像オブジェクトを作って PDF 文書に埋め込む」ことができるか、試してみた。

事前調査

まずは、普通に pdfTeX の機能を使って画像を埋め込んだ時に、PDF ファイルの中のコードがどうなるかを調べてみよう。

[sample.tex]
% plain pdfTeX 文書
\nopagenumbers
\pdfminorversion=4
% データ圧縮を抑止
\pdfcompresslevel=0 \pdfobjcompresslevel=0
% ボックスの中でJPEG画像を配置する
\newbox\imagebox
\setbox\imagebox\hbox{%
  % lena.jpg は 256px×256px, 96dpi のJPEG画像ファイル
  \pdfximage{lena.jpg}%
  \pdfrefximage\pdflastximage
}
% TeXのボックスからフォームXObjectを作る
\pdfxform\imagebox
% それをページに出力
\pdfrefxform\pdflastxform
\bye

このソースを pdftex で PDF 文書 sample.pdf に変換して、その中を覗いてみる。

ページのオブジェクトの ID は 4 で、コードは以下の通り。

4 0 obj <<
/Type /Page
/Contents 5 0 R
/Resources 3 0 R
/MediaBox [0 0 595.276 841.89]
/Parent 6 0 R
>> endobj

これを見ると、ページのコンテンツ(/Contents)のオブジェクトの ID は 5、リソース(/Resources)のオブジェクトの ID は 3 だと解る。

ページのコンテンツのオブジェクト(ID=5)のコードは以下の通り。

5 0 obj <<
/Length 33        
>>
stream
q
1 0 0 1 72 577.89 cm
/Fm1 Do
Q
endstream
endobj

参照点を移動(cm)した後、/Fm1 という XObject を配置(Do)している。

リソースのコンテンツのオブジェクト(ID=3)のコードは以下の通り。

3 0 obj <<
/XObject << /Fm1 2 0 R >>
/ProcSet [ /PDF ]
>> endobj

これより、名前が /Fm1 の XObject の ID は 2。そのコードは以下の通り。

2 0 obj <<
/Type /XObject
/Subtype /Form
/BBox [0 0 192 192]
/FormType 1
/Matrix [1 0 0 1 0 0]
/Resources 7 0 R
/Length 31
>>
stream
q
192 0 0 192 0 0 cm
/Im1 Do
Q
endstream
endobj

これより、/Fm1 はフォームであることが解る。その中身は、拡大(cm)を適用した上で、/Im1 という XObject を配置(Do)している。

このフォームのリソースのオブジェクトの ID は 7 で、コードは以下の通り。

7 0 obj <<
/XObject << /Im1 1 0 R >>
/ProcSet [ /PDF /ImageC ]
>> endobj

これより、/Im1 の XObject の ID は 1。そのコードは以下の通り。

1 0 obj <<
/Type /XObject
/Subtype /Image
/Width 256
/Height 256
/BitsPerComponent 8
/Length 10909
/ColorSpace /DeviceRGB
/Filter /DCTDecode
>>
stream
****(バイナリデータ)****
endstream
endobj

この streamendstream の中には、lena.jpg のバイナリデータが丸々そのまま含まれている。なのでストリームの長さ(/Length)はファイルサイズに一致している。

補足事項:

  • PDF では、JPEG 形式の画像データは“そのまま”画像 XObject として埋め込める。フィルタの /DCTDecode というのは要するに“JPEG形式”(をデコードすべし)という意味である。
  • これに対して、PNG 形式については“そのまま埋め込む”ことはサポートされていないので、一旦(PNG 形式を解読して)ビットマップデータを復元した上で“一般的なビットマップデータ”として埋め込む必要がある。ただし、PNG 形式の中で利用されているデータ圧縮方式である Deflate 自体は PDF の圧縮フィルタとしてサポートされている(/FlateDecode)ので、PNG のデータ記述形式によっては、“伸長・再圧縮”無しで埋め込み処理ができることもある。*1
  • 生成した画像 XObject を実際に(PDF 描画命令列の中で)Do で配置すると、常に 1 ポイント(bp)四方の大きさをもつと見なされる。従って、所望の大きさで出力させるために拡大(cm)する必要がある。今の例では原寸、つまり 192bp×192bp での読込を指示しているので、縦横 192 倍に拡大することになる。
作業開始

というわけで、この出力の“真似”をしつつ、かつ画像 XObject の部分だけ“自分で書く”ことにすればよいだろう。

まずは、JPEG 画像のバイナリデータを用意する。ここでは 16 進数のダンプを \pdfunescapehex で変換している。

\edef\ImageBinary{\pdfunescapehex{%
FFD8FFE000104A46494600010101006000600000FFDB0043000A07070807060A%
……(略)……
98F4E428ED5F500B98D4FA62975B5ABDD91CC2F5344B4AD82ADB3FFFD9}}

これを使って画像 XObject を作りたいのでさるが、当然 \pdfximage(これは“普通に”画像ファイルを読むためのもの) は使えない。なのでより直接的な \pdfobj 命令を用いる。\pdfobj 命令にはストリームオブジェクト用の形式が存在するが、それを用いた場合、ストリームデータとして「デコード後のデータ」を与える必要がある。((\pdfcompresslevel が非ゼロであれば、エンジンによって圧縮されて PDF に書き出される。))今の場合、/DCTDecode でデコードする前のデータを与えたいのでこれは不適当である。((/Filter を自分で指定すれば良さそうだが、エンジンでの圧縮が有効の場合、エンジン自体が /Filter を設定しようとするため衝突してしまう。))そこて、ストリーム形式を使わずに「stream 部分を自分で書く」という強引な手段をとる。*2

\endlinechar=10 % 改行を素通しさせる
% パラメタは先程のPDFの結果の通りにする.
% stream〜endstream の間にさっきのバイナリを入れる.
\immediate\pdfobj{<<
/Type /XObject /Subtype /Image
/Width 256 /Height 256
/BitsPerComponent 8 /ColorSpace /DeviceRGB
/Length 10909 /Filter /DCTDecode
>>
stream
\ImageBinary
endstream}%
\endlinechar=13 %
% \ImageObj は生成したオブジェクトのID
\mathchardef\ImageObj\pdflastobj

これで、ストリームの圧縮が有効(\pdfcompresslevel が非ゼロ)な場合でも通用する。ただし、オブジェクトストリームの使用を有効(\pdfobjcompresslevel が非ゼロ)にすると失敗する((stream〜endstream の部分も“オブジェクト”の記述の一部と思ってしまうためだろう。))ので、それは無効化する必要がある。

% オブジェクトストリームは使わない
\pdfobjcompresslevel=0

次に、生成した画像 XObject(\ImageObj)を TeX のボックス(\hbox)の中に配置したい。ところが、\ImageObj は“自分で手書き”したものでエンジンに「画像 XObject である」と認識されてはいないので、\pdfrefximage を使う、というわけにいかない。従って、この“画像”を実際に文書内に出現させる方法は、「PDF 描画命令を直接書く」より他にない。

一般に、エンジンが生成する PDF 描画命令に割り込んで辻褄の合うような描画命令を構成するのは難しい。しかし、幸いなことに、「手本」の PDF である sample.pdf においては、画像を入れたボックスをフォーム XObject として独立させている。

(sample.pdf の中のコード)
2 0 obj <<
/Type /XObject
/Subtype /Form
/BBox [0 0 192 192]
/FormType 1
/Matrix [1 0 0 1 0 0]
/Resources 7 0 R
/Length 31
>>
stream
q
192 0 0 192 0 0 cm
/Im1 Do
Q
endstream
endobj
% リソースオブジェクト
7 0 obj <<
/XObject << /Im1 1 0 R >>
/ProcSet [ /PDF /ImageC ]
>> endobj

従って、“このフォーム XObject の作り方を真似する”ことで、「“手書き”した画像 XObject の入ったボックス」を作ることができる。そしてこれができれば、後は文書中の好きな場所で、\pdfrefxform 命令により、そのボックス(つまり画像)を使うことができるわけである。ただしそのためには、このオブジェクト自体は \pdfxform で(\pdfobj ではなく)作ることが必要になる。

というわけで、まずは TeX ボックスの中身を上掲のフォームのストリームデータと同じになるようにしよう。ただし、/Im1 という名前は“エンジンが生成する名前”なので、衝突を避けて代わりに /myIm1 とする。PDF 描画命令を直接書き出すには \pdfliteral 命令(page 指定付((page がないと“TeX が把握する現在位置と整合させるための座標変換”が自動的に補われ、ここでは不都合である。)))を用いる。

\newbox\ImageBox
\setbox\ImageBox\hbox{\pdfliteral page {%
  q 192 0 0 192 0 0 cm /myIm1 Do Q
}}

ところで、もちろん \pdfliteral の中身は TeX は関知しないので、このままではボックスの寸法(高さと幅)はゼロになる。\pdfrefximage を入れた場合(当然その画像の寸法になる)と辻褄を合わせるためボックスの寸法を更新する。

\wd\ImageBox=192bp \ht\ImageBox=192bp

そして、このボックスから \pdfxform 命令でフォーム XObject を作りたい。

\pdfxform\ImageBox % ダメ

ところがこのままではダメである。sample.pdf のフォーム(のボックス)では中で \pdfrefximage を使っているので、リソースオブジェクトに適切な /XObject キーが書き出される。ここで作るフォームでは当然 /myIm1 が何であるかはエンジンは知らないので、自分でリソースに書き込む必要がある。\pdfxform 命令では resources 指定を使って、リソースオブジェクトの内容を指定できる(エンジン自体が出力するものに追記される)。ここに /XObject キーの内容を書いておく。

\pdfxform resources {% リソースに書き込む内容
% /myIm1 はIDが \ImageObj のオブジェクトの参照である
  /XObject << /myIm1 \the\ImageObj\space 0 R >>
}\ImageBox
% \FormObj は生成したオブジェクトのID
\mathchardef\FormObj\pdflastxform

ちなみに、ここでも「フォーム XOjbect を間に入れている」ことが意味を持っている。ページに直接(Do で)画像を書き出そうとすると、「ページのリソース」に /myIm1 を追加する必要がある。pdfTeX にはそのための命令 \pdfpageresouces も存在する。ところが、当該ページに \pdfrefximage で挿入した画像があるなどの理由でエンジンが既に /XObject キーを書き出してしまった場合は、自身で /XObject キーを書くと衝突してしまって上手くいかないのである。それに対して、ここで作ったフォームには他の要素が入っていないので、エンジンが /XObject キーを書き出すことはあり得ないのである。

“画像の入ったフォーム XObject”さえできてしまえば、後は簡単で、それを \pdfrefxform で配置すれば \ImageObj の画像が出力される。

\pdfrefxform\FormObj

これで完了であるが、実は 1 つ注意すべきことがある。\pdfobj で作った画像 XObject \ImageObj について、実は“エンジンの機能”は一度も参照していない。(\ImageObj への参照は“自分で書きだしたコード”、つまりエンジンが知らない所で行われている。)ところが、普通に \pdfobj でオブジェクトを作った場合、それが一度も参照されていなければ、エンジンはそれを書き出さないのである。実は、\ImageObj の作成の際にこの問題への対処を行っている。

\immediate\pdfobj{<<
....

このように \pdfobj の前に \immediate を付けておいた。これで、エンジンはその場で作成したオブジェクトを書き出すようになる。

作業完了

というわけで完成したのがコレ。*3

これを pdftex でコンパイルすると、“例のレナ”の画像が埋め込まれた PDF 文書ができる。

この実験により、ダンプデータから生成した画像の埋込が原理的に可能であることが判った。なので、後はものすごく頑張れば、HTML での「data スキームによる画像データの文書ソースへの埋込」と同様の機能をもつパッケージを作成することができるだろう。*4そういう機能が LaTeX にあった方がよいと思う人*5は実装に挑戦してみては如何だろうか。

*1:PNG データの中の IDAT チャンクだけをストリームデータとした上で、フィルタパラメタを適当に合わせる。

*2:同様の手段は例えば pgfplot パッケージなどで実際に使われている。

*3:このソース自体は 12/6 に公開していたが、とある事情があり、ブログでの紹介を今まで見合わせていた。

*4:JPEG 画像については確実にできる。PNG 画像については制限があるだろう。

*5:ちなみに私自身はあまり思わない。




以上の内容はhttps://zrbabbler.hatenablog.com/entry/20141230/1419927594より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14