読者です 読者をやめる 読者になる 読者になる

pdfTeXじゃないけどGhostscriptさえあれば画像は埋め込めるよねっ

2015/1/24 16:48 GhostscriptをGhostScriptと記述していました.お詫び申し上げます.

はじめに

こんなことTeXユーザの集いであった.そのなかで,TeXの限界というタイトルでいくつかのトピックが示された.私の知る限り,TeXチューリング完全なのだから,原理的に計算可能ならどうにかなるだろうと思った.実際,「え,list環境再実装すれb……」などの容易に実現できそうな項目もある.ただ,一つのトピックに目を奪われた.

画像のBase64埋め込みの非対応

確かに,TeXでバイナリを扱うのは面白そうな課題である.

やってみた.

といっても,Base64はまだ実現していないので,画像埋め込みをするお話*1

技術概要

某奥底にあるように,LuaTeXのような特殊な場合を除き,TeXエンジンでバイナリファイルを書くことは困難である.よって,TeXでバイナリで与えられる画像データを直接扱うのは不可能である.すなわち,テキストで書き出して,そのバイナリへの解釈をTeXの外部に任せることになる.さて,どうしようかと悩んだところで,ふと思いついた.EPSに出力して\includegraphicsで読めばいいじゃないか,と.EPSはテキスト形式なのでTeXから出力できる.また,ラスタ画像を埋め込むための命令もある.

作成した機能は,LaTeXのパッケージとして動作するようにした.そのパッケージ,binaryimage.styはここに置いておいた.このパッケージは,次のようにして利用する.

\documentclass{article}
\usepackage[dvipdfmx]{graphicx}
\usepackage{binaryimage}
\begin{document}
  ...
  \includebinary[size=wxh,option={width=6cm, clip}]<<バイナリの16進数表現>>\endbinary
  ...
\end{document}

画像を入れるために,includebinaryという命令を用意している.この命令は,オプションにsize(必須)とoptionをとる.sizeはwxhの形で幅と高さのピクセル数を与える.例えば,幅400px,高さ300pxの画像ならばsize=400x300とする.optionはこの命令によって自動的に挿入される\includegraphicsへ与えるオプションである*2.オプションの後には画像のバイナリを16進数表現したものを入れる.このバイナリは,JPEGなどの画像形式をそのまま入れるのではなく,「左上から右へ,右端まで行ったら次の行の左端へ」という順で対応するピクセルのRGBをそれぞれ1Byteで表現したものである.こんなPythonコードで変換できる*3

# -*- coding:utf-8-unix -*-
from PIL import Image
import sys


def main():
    img = Image.open(sys.argv[1])
    for j in range(img.size[1]):
        for i in range(img.size[0]):
            print("{:02X}{:02X}{:02X}".format(*img.getpixel((i,j))), end="")
        print()
    

if __name__ == "__main__":
    main()

実行すると,なぜか-binaryimage<0からの通し番号>.epsというファイルが生成され,\includegraphicsされる.epsファイルの中身は次のようになっている.

%!PS-Adobe-3.0 EPSF-3.0
%%BoundingBox: 0 0 w h
/DeviceRGB setcolorspace
1 1 scale
<<
/ImageType 1
/Width w
/Height h
/BitsPerComponent 8
/Decode [0 1 0 1 0 1]
/ImageMatrix [w 0 0 h neg 0 h]
/DataSource currentfile /ASCIIHexDecode filter
>>
image

バイナリの16進数表現

ここで,w,h,および「バイナリの16進数表現」には\includebinaryで指定した値が入る.

これらの処理の結果,画像がtexファイルに埋め込まれたように見える.サンプルはここ(ソース結果).

実装小話(急カーブ注意×1)

ここで,binaryimage.styを実装するにあたり利用したテクニックを紹介する.

%の出力

TeXにおいて,%はコメントの開始文字なので,単純に出力できない.さらに,今回の場合は,\%も利用することはできない.\write(ファイル出力)のタイミングで\%が処理できないからである.そこで,binaryimage.styでは\lccodeを使ってコメント開始の役割を持たない%を作り出すことにした.

次のコードを見てみよう.

\lccode`+=`\%%
 \lowercase{\def~{+}}%

\lccodeはある文字に対して,その小文字の文字コードを指定するプリミティブである.上記のコードの1行目では,「+」の小文字に「%」を指定している.そして,\lowercaseを用いて「\def~{+}」の部分を小文字化する.これにより「\def~{%}」となるのだが,このときのカテゴリーコード(文字の役割)は変換前のものが使用されるため,「%」はカテゴリーコード12,すなわち一般の文字として扱われる.後は,%を出力したいところに~を書けば,~が展開されて%が出力される.

長いバイナリ入力への対応

\binaryimageでは,その用途の関係上,長大な入力を必要とする.しかし,TeXのマクロの引数として与えられる文字列はそんなに大きくない.そこで,今回は\edefを使って長大な入力を受け付けるようにした.

\binaryimageは,最初,オプション部分しか読まないようになっている.そして,\binaryimageを展開すると,末尾は次のようになる.

\edef\@bin@data{\ifnum`}=0\fi
       image^^J^^J

\@bin@dataの定義になっているように見えるが,閉じ波括弧が「\ifnum`}=0\fi」となっている.この部分が展開され,空になるので,上記のコードは

\edef\@bin@data{image^^J^^J

という閉じ波括弧がない未完成の\edefになる.いわゆる\ifnumトリックである.ここから,<<バイナリの16進数表現>>が読み込みおよび展開されていき,\endbinaryに達する.\endbinaryを展開すると,その先頭は次のようになっている.

\ifnum`{=0\fi}% end of \edef

これは,先の\ifnumトリックの対をなすコードで,閉じ波括弧単体となる.結果,\@bin@dataの定義が完成する.

この後,\endbinaryによってepsファイルの出力と\includegraphicsが行われ,画像が読み込まれる.

副次効果

binaryimage.styを通じて,Ghostscriptの闇などに触れてしまった.ここでは,その一部を紹介しよう.

Ghostscript9.10と9.15におけるヘッダ解釈の差異

現在,binaryimage.styが吐き出すepsファイルの1行目は「%!PS-Adobe-3.0 EPSF-3.0」となっている.実は,つい最近まで「%!PS-Adobe-3.0」という誤った出力をしていた.後者の出力の場合,Ghostscriptのバージョンが9.10なら動作するが9.15だと読み込めないという謎の事象に見舞われた.本記事の投稿が遅れた最大の理由がこれである.

epstopdfのMSYS対応

graphicxのドライバをpdftexにしたところ,指定した覚えのないディレクトリがエラーメッセージに出るという不可解な現象に遭遇した.中身を開いて調べたところ,epstopdfがMSYSに対応していないことが原因だと発覚した.仕方がないので自分で直し,フォーラムに投げた.現在は,Akira Kakutoさん,KUROKI Yusukeさん,およびKarl Berryさん*4のご尽力により,改良されたMSYS対応コードが本家に取り込まれたはずである*5

関連研究

ZRさんによって,このような記事がすでに発表されている.これはpdfTeXの機能を用いて,直接pdfファイル中に画像を埋め込む手法になっている.Ghostscriptに悩まされない分,綺麗な手法といえるが,pdfTeX依存になるため,TeXエンジンを複数使い分ける場合などに支障が出る.

今後の課題

Base64デコーダの実装が今後の課題にある.今回の使用目的に対してならいくつか実装アプローチがある.完全展開可能なデコーダTeX上に実装するか,PostScript側にデコーダを実装するかである.どちらもそれなりに苦行なので,そもそもbinaryimage.styに需要があるなら実施してもいいかもしれない*6

*1:某奥底に先を越されたのはGSの謎仕様のせい.

*2:命名規則が酷いのは仕様

*3:これが初Pythonであることを告白しておく

*4:著名人しかいない怖さ

*5:なお,全部rungsでもいいのではないかという提案を投げるのは私の仕事になっているが未実施である.仕事が早いのも見習いたい点である

*6:少なくとも,作者である私は使用する予定はない