PostScript言語はファイルも扱えます。 それだけでなく、コールバックに渡すインスタンスも、自力でプロシージャに突っ込んでおくことができます。
名前の付いているファイルは簡単。
(path/to/filename) (r) file
fileオペレータは文字列をふたつとる。
ひとつ目はファイル名、ふたつ目はアクセスモード。
返ってくるのはファイルオブジェクト。
(r)
を(w)
にすると書き込みになる。
他に(a)
・(r+)
・(w+)
・(a+)
がある(PLRM 3.8.2)。
このほかにcurrentfileというオペレータがある。 これは今PostScriptプログラムを読み込んでいるファイルオブジェクトを返す。 このオペレータが返したファイルオブジェクトに対してファイル入力を行うと、その時点での読み込み位置からデータを読み込み始め、入力が終わると何食わぬ顔で読み込みが終わった位置から実行を続ける。 タコが自分の足を食っているような感じだが、これを使うとPostScriptプログラムファイル中にデータを置くことができる(決まり文句がある)。
このとき読み込みを始める位置は、名前・数値を読み込んだ場合は直後の空白文字・改行などを一文字だけ(一連の空白文字ではない)読み飛ばした直後から、それ以外の場合はトークンの直後の文字になる。 たとえば、
currentfile read x pop ==
はxの文字コード120を表示するが、
currentfile read pop ==
はスペースの文字コード32が表示される。 これはインタープリタのトークン解析部の動作を考えてみれば分かるだろう。 数値や名前のおしまいは、数値や名前として使えない文字を読み込むまで分からない。 つまり、終わりが分かったときにはその次の文字を1文字読み込んでいる。 PostScriptインタープリタはこの文字を入力に押し戻すようなことはしない、ということだ。
プロシージャの中からreadなどが行われることもあるので、必ずファイル読み込みオペレータの直後から読み取られるわけではない。 が、数値やそれ以外のトークンの直後から、という例は思いつかなかった・・・。
使ったファイルを閉じるのはclosefile。
readlineでは改行コードの変換が行われ、いずれの改行コードでも読み取りが終了し、次の読み込み位置は改行コードの次の文字になる(PLRM 3.8.1)。 そのほかのオペレータでは改行コードの変換は行われない。 readはもちろんreadstringも、である。 ただし、Cygwinのgs(少なくとも9.0.6では)ではマウントモードの影響を受けるようだ。 読み込もうとしているファイルがテキストマウントされている場所にあると0x0d・0x0aが0x0aに変換されてしまう(ちょっと困る)。 PostScript言語の仕様では「通信チャンネルからもらったデータをそのまま返す」と書いてあるので、Cygwinの改行コード変換レイヤーが通信チャンネル側に含まれるとすれば、これはこれで仕様どおりなのだが・・・。
改行コードの変換は入力となっているPostScriptプログラムファイルにも影響がある。 PostScriptインタープリタは入力となっているPostScriptファイルのLF・CR・CRLFの3つの改行コードを「ひとつのLF」に変換する。 たとえば、
( ) dup length == 0 get ==
というコードを書くと、つまり、文字列の中に改行が入った場合、ソースファイルがCR・LF・CRLFのどの改行コードで書かれていても「ひとつのLF」と扱われ、この場合できあがった文字列は1バイトで、コードは10進数で「10」と表示されるだろう。
一方、
3 { currentfile read pop == } repeat x
というコードの場合、数値・名前の場合は直後のホワイトスペースをひとつ読み飛ばした位置から読み込みが始まることになっているので、「x」から読み込みが始まり、最初の値は120である。 それ以後は改行がそのまま読み込まれるので、10 10 になったり、13 13 になったり、13 10 になったりするだろう。 もし、repeatの直後にスペースがひとつあると、readは改行から読み込みを始めるので、「10 120 10」・「13 120 13」・「13 10 120」のどれかになるだろう。
このほかにバックスラッシュを使った行継続の規則もある。
ファイルなどを加工して新しくファイルとして使えるようにする(PLRM 3.8.4)。 Unixシステムのパイプとフィルタプログラムに似ている。 フィルタを作るにはfilterオペレータを使う。 できあがったオブジェクトは加工後のデータを返すファイルオブジェクト。 filterオペレータはパイプを「つなぐ」作業だけを行い、加工が起きるのは実際に出口端からデータが要求されたとき、あるいは書き込まれたときになる。
データをデコードするフィルタとエンコードするフィルタがあって、デコードするフィルタでは入力データを指定してfilterオペレータを実行し、できあがったファイルオブジェクトからデータを読み込む。 エンコードするフィルタでは出力先を指定してfilterオペレータを実行し、できあがったフィルタオブジェクトに対してデータを書き込む。
フィルタの入力はimageオペレータなどと同様、文字列・プロシージャ・ファイルのいずれか。 文字列は繰り返し使われるのではなく、全部使い切った時点でデータ終了。 プロシージャは文字列を返すように書くが、空の文字列を返すとデータ終了を示す。 エンコードフィルタについては使ったことがないので割愛。
標準のフィルタとして各種エンコード・デコードをしてくれるものが用意されている(PLRM 3.13.3)。 たとえば、ASCIIHexDecodeは16進数文字列を読み込んでバイナリデータに変換してくれる。
(input.txt) (r) file /ASCIIHexDecode filter { dup read not { exit } if == } loop
とすると、16進文字列の書かれたinput.txtというファイルからデータを読み込んで、データが尽きるまで内容を表示する。
currentfileと合わせてよく使われるフィルタにASCII85Decodeフィルタがある。
このフィルタはbase64に似たエンコードをされた文字列をバイナリに戻してくれる。
855>2564なので、4バイトを5文字で表現できる(base64は3バイトを4文字)。
書式・エンコード方法はPLRM 3.2.2、3.13.3に書いてある。
データの最後を示す文字列は~>
と決まっている。
これは常套句があって、
currentfile /ASCII85Decode filter { dup 1 string readstring not { pop exit } if print } loop ,p<UKDfp/6Bl%?'-m`qVA8cU4.3L$_.3N5:Ch4_B5uU-B8N8RrDI[TqAKYT!Cij6/+Co%q$86+2 Anc'm+=M>CF*'$RF"&4[E[`,CBl%?'A7]?[HQ[$?Anc'm+ED%%A0>c.F<GoQAU%p2+FPAHAfu#7 FCo6'AKYQ/@q[!/EbTW,+FPAKDf9S%Dfp/6Bl%?'@r-()AS,XmAI8~> pop
とすると、loopが実行されたところで中にあるreadstringが実行されてフィルタが読まれ、フィルタは入力ファイルを読みに行く。
入力ファイルはcurrentfileが残したオブジェクトだから、PostScriptプログラムそのものからデータが読み込まれる。
プログラムはloopまで読み込まれているので、その直後にASCII85Encodeしたデータを貼り付けておくと、これが読み込まれてデコードされる。
ASCII85Decodeは~>
を読み込んだところでファイル終了を返すが、入力ファイルを閉じるわけではない。
PostScriptインタープリタはファイルの現在位置から実行を続けるので、何食わぬ顔をしてpopから実行を続ける。
ちなみにこのプログラムを実行すると、エンコード文字列を作るのに使用したプログラムが出てくる。
filterオペレータが返すオブジェクトはファイルだから、これを別のフィルタやimageオペレータなどに突っ込むことも可能だ。 これを使うと画像だろうがなんだろうが、PostScriptプログラムの中に埋め込んでしまうことができる。
何もしないエンコードフィルタとしてNullEncodeフィルタがある。 これを使うとファイルへの書き込みを文字列・プロシージャに向かって出力することができる。
一方、何もしないデコードフィルタというのは明示的には用意されていないが、SubFileDecodeフィルタが使える。
このフィルタは特定のパターンを指定して、ひとつのファイルを複数のブロックに分けるために使うフィルタだが、パラメータとしてEODCount
を0、EODString
を空の文字列にする、つまり、source 0 () /SubFileDecode filter
とすると、何もしないデコードフィルタになる。このフィルタを使うと文字列やプロシージャをファイルとみなして読み込めるようになる。
/dumpfile { { dup read not { exit } if 3 string cvs print ( ) print } loop } def <0102030405060708090a> 0 () /SubFileDecode filter dumpfile (\n) print pop
フィルタそのものを自分で作ることはできないようだが、これらのフィルタを使えばほぼ同じようなことができる。
フィルタにプロシージャを与えた場合、そのプロシージャにパラメータを与えたい場合や、内部で状態を保持したい場合がある。 普通のプロシージャならばパラメータをオペランドスタックに与えて呼び出し、他の言語で言うローカル変数は内部でローカルに辞書を用意すればいいのだが、フィルタプロシージャはフィルタが呼び出すものなので、オペランドスタックには何も積まれてないし、内部で辞書を作ってもプロシージャから抜けると辞書が捨てられてしまう。 これはフィルタだけでなく、imageなどの入力としてプロシージャを与えた場合も同様である。
たとえば先ほどの例に「入力を順に加えていく」プロシージャを与えたSubFileDecodeフィルタをつなげてみる。 普通のプロシージャなら、
/calcsum { 1 dict begin /sum 0 def { sum add /sum exch def } forall sum end } def
と書けるだろう(この例なら辞書なしでも書けるが)。 ところが、同じことをフィルタプロシージャでやると、1バイト読んだところでプロシージャから戻るから、そのときに辞書を捨ててしまってうまく動かない。 簡単には状態変数を外に出してしまって、以下のようにすれば動く。
% file [ accumulate ] file /accumulate { /accfile exch def /accsum 0 def { accfile read { accsum add /accsum exch def 1 string dup 0 accsum 256 mod put } { () } ifelse } 0 () /SubFileDecode filter } def <0102030405060708090a> 0 () /SubFileDecode filter accumulate dumpfile pop (\n) print
accumulate
にはフィルタを作るところまで含まれているのだが、accfile
とaccsum
の2つはグローバルな辞書に作らないといけない。accumulate
内でローカルな辞書にしてしまうと、フィルターを作ってaccumulate
を抜けたところで捨てられてしまい、うまく動かないだろう。
試しに2 dict begin
とend
と書き加えてうまく動くように見えた人は、おそらくgsを抜けずに実行しているはずだ。
直前に実行したときにグローバルな辞書にaccfile
とaccsum
が作られてundefinedを逃れている。
しかし、gsを抜けなくてもさらにもう一度実行してみるとうまく動かないことが分かるだろう(グローバルな方のaccfile
はファイルが終端まで行っちゃってるからね)。
この例ではaccsum
の初期値をaccumulate
の冒頭で与えているから、これをスタックから取るようにすればパラメータも与えることができる。
しかし、もしこのプロシージャを2つ同時に動かしたらどうなるか?
たとえば、もうひとつ、「ふたつのファイル入力を足す」プロシージャを与えたSubFileDecodeフィルタを作り、その入力にaccumulate
を与えたらどうなるか?
新たなプロシージャの方はこんな感じになるだろう。
% file file [ add2data ] file /add2data { /addfile1 exch def /addfile2 exch def { addfile1 read { addfile2 read { add 256 mod 1 string dup 0 4 -1 roll put } { pop () } ifelse } { () } ifelse } 0 () /SubFileDecode filter } def <0102030405060708090a> 0 () /SubFileDecode filter <0102030405060708090a> 0 () /SubFileDecode filter add2data dumpfile pop (\n) print
これにaccumulate
を入れるわけだが、実行してみるまでもなく、accfile
とaccsum
がふたつのaccumulate
で共用されるわけだから、当然うまく動かないだろう。
この場合はプロシージャを抜けても辞書の内容を保持していなければならないので、ローカル変数というよりもC++のクラスのようにプロシージャにメンバ変数をくっつけておかなければならないことが分かるだろう。
果たしてそんなことがPostScript言語でできるのか?
できるのである。 これに気付いたときは正直ちょっとびっくりした。 先人はとっくの昔に実現していたのであるが。 実例を挙げる前に少しPostScript言語でのオブジェクトの扱いについて説明しておかないといけない。 PostScript言語でスタックに値を積むと、たとえば単純な数値ならば数値そのものがスタックに積まれる(単純オブジェクト: simple object)。 しかし、複雑なオブジェクト(複合オブジェクト: composite object)・・・と言っても数値以外の文字列・名前・配列・辞書・ファイルなど、ほとんどは全部複雑なオブジェクトになるのだが・・・は、実体は仮想メモリ(VM、PLRM 3.7.1)上に作られて、スタックにはそこへのポインタが格納されるだけだ(PLRM 3.3.1)。
これらのオブジェクトはたとえスタック上でdupやindexで複製を作ったとしても、ポインタがコピーされるだけで、ポインタが指している先は同じである。
だから、(abc) dup 1 16#21 put
は(a!c)
を生み出すわけだ。
本当に複製を作りたければcopyオペレータを使って(abc) dup 3 string copy
としなければならない。
そして、このときVM上に作られたオブジェクトは、どこからかポインタで参照されている限り、破棄されることがない。
たとえそれが無名の配列・辞書やプロシージャブロックからの参照であっても、実体の方に名前を付けてなくても、だ(そりゃそうだろう)。
だから、プロシージャをフィルタに渡すごとに新しく辞書を作って、名前を付ける代わりにフィルタプロシージャの中に辞書の実体を直接置いてしまえばよい。 辞書はdupやindexなどによる複製ではなく、新しく作ったのだから、フィルタプロシージャを二つ作ればそれらはそれぞれのプロシージャ専用の独立した辞書になる。 C++で言えばあるクラスのインスタンスを二つ作ったことになる。
問題はどうやって直接プロシージャの中に辞書を置くか? である。
ひとたび{
と書いてしまうと、PostScriptインタープリタは}
に出会うまで、中身を実行することなく全部スタックに積んでしまう。
つまり、{ 2 dict
とか、/accdict 2 dict def { accdict
とか書いたのではダメである。
前者は2 dict
という命令列が積まれるだけだし、後者はaccdict
という名前が積まれるだけで、しかもせっかく作った辞書はプロシージャを実行する時点では捨てられてしまっているだろう。
まぁ、そうじゃないとプロシージャの定義なんてできないわけだから当たり前だ。
使えそうなのにbindオペレータ(PLRM 3.12.1)と//name
記法による即時評価(Immediately Evaluated Names、PLRM 3.12.2)があるが、この場合は実はどちらも使えない。
bindオペレータの展開はオペレータを示す名前にしか適用されないし、即時評価はプロシージャ(この場合はaccsum
)を定義するときに展開されてしまうので、実際にフィルタにプロシージャを渡すときに値を変えられないからだ。
じゃあどうするのかというと、実はPostScript言語にはcvxというオペレータがあって、オペランドスタックに置いた名前をこっそり実行可能な状態にすることができる(この時点ではまだ実行されない)。
これにexecオペレータを作用させると実際に実行することができる。
たとえば、/foo exec
と書いた場合、/foo
がもう一度スタックに積まれるだけ(リテラル扱い)なのだが、/foo cvx exec
と書くとfoo
が実行される(実行可能扱い)ようになる(PLRM 3.3.2、3.5.5および8.2のexecオペレータ。fooが定義されていなければもちろんundefinedを食らう)。
で、配列にcvxを適用すると配列からプロシージャを作れる。
配列ならば[
を書いた後でもオペレータは次々と実行されるので、currentdictとでも書いておけば辞書の実体(を指すポインタ、以下面倒なので略)を置いてくれる。
最後にこれをcvxすればプロシージャになる。
このプロシージャを実行するととりあえず「辞書の実体」を実行しようとするわけだが、辞書の実体を実行すると何が起きるのだろう?
この答えは簡単で、辞書だってリテラルの一員なので、答えは「辞書がそのままスタックに積まれる」である。
これで目的が達成できそうだ。
まず、[ currentdict
である。
このあと、begin
と続けたいのだが、ここはプロシージャの定義ではなくて配列の定義だから、直接書いてしまうとbeginが実行されてしまう。
実行されないように置いて、しかもあとで実行できるように/begin cvx
と書くことになるが、これを延々と書き続けるのは面倒なので、ここから先をプロシージャに入れる。
つまり、{ begin .... end }
と続く。
これだけでいいかというと、たとえば、{ (!) print } loop
は}
を打ったところでは何もせず、loop
と打ったところで初めてプロシージャが実行される。
当たり前だ。
そうでなきゃプロシージャの定義ができない。
つまり、このままではプロシージャをスタックに置いただけで実行されない。
後ろにexecを書く必要があるが、これも実行されないようにコッソリと置く必要があるので、} /exec cvx
と続く。
最後に] cvx
である。
これをつなげると常套句
10 dict begin [ currentdict { begin ... end } /exec cvx ] cvx end
というのができあがる。 これが専用辞書を持ったプロシージャになる。
外側のbegin〜endはプロシージャを定義するときに実行されるが、プロシージャの中にcurrentdictを置き去りにしているので、endを実行しても辞書本体はVMの中に残っている。 辞書スタックからポインタが取り除かれるだけだ。 一方、プロシージャの中のbegin〜endは、プロシージャ定義時には実行されず、実際にプロシージャを実行したときに実行される。 プロシージャが実行スタック(PLRM 3.4)にある間は辞書への参照はなくならないから、フィルタがプロシージャを握っている間は辞書が消滅することはない。
これを使えばaccumulate
はちょっとした変更で次のようになる。
パラメータも、初期値も与えられる点に注目。
% file [ accumulate ] file /accumulate { 2 dict begin /accfile exch def /accsum 0 def [ currentdict { begin accfile read { accsum add /accsum exch def 1 string dup 0 accsum 256 mod put } { () } ifelse end } /exec cvx ] cvx 0 () /SubFileDecode filter } def
これを先ほどのadd2data
と共に、
<0102030405060708090a> 0 () /SubFileDecode filter accumulate <0102030405060708090a> 0 () /SubFileDecode filter accumulate add2data dumpfile pop (\n) print
とすれば、きちんと独立して動いていることが分かるだろう。
さらにadd2data
の方も同様の修正が可能である。
Copyright (C) 2012 You SUZUKI
$Id: file-filter.htm,v 1.3 2016/02/20 08:56:10 you Exp $