掴んだ物(技術)はちゃんと食べる記録帳

興味を持ったものに対する足跡を残していくために作成しました。勉強しながら書いているので、間違いや短絡もあると思います。ご指摘いただけると喜びます。

Let's PostScript(11) [1 2 3] [1 2 3] eq はなぜfalseなのか

以前の記事

hlsme.hatenablog.com

で以下のようなことを言っていて、宿題だと書いていました。

本を見ると、eqオペレータとneオペレータは任意のObjectが比較できるようなのですが、配列でやってみたところ、うまくいきませんでした。 このあたりよくわからないので、後日の課題としたいと思います。

その時試したのが以下のScriptだったのですが、ここでfalseとなる理由がわかったような気がしますので、書き残しておこうと思います。

GS>[1 2 3] [1 2 3] eq ==
false

hlsme.hatenablog.com で自分が書いた内容を思い返すと、上記のScriptの結果がfalseとなるのは自明だなと。

GS>[1 2 3] 

まず、最初の配列の登録で、仮想メモリに対して配列の実体を格納し、OperandStackにはユニークIDをPushします。

GS<1>[1 2 3]

次に、二つ目の配列の登録で、仮想メモリに対して2つ目の配列の実体を格納します。たまたま、最初の配列と同じ内容ですが、格納される仮想メモリは別の場所です。当然、OperandStackには最初の配列とは異なるユニークIDがPushされます。

GS<2> eq ==
false

eqオペレータでOperandStackから1つ目の配列のユニークIDと2つ目のユニークIDを受け取り、その両者が一致するか比較します。 当然、両者のユニークIDは別なので、結果はfalseとなります。

ということなのかなと。とりあえず、自分の中では納得がいったのですっきりしました。

Let's PostScript(10) ユーザー空間とデバイス空間

ようやく、座標系の話にたどり着きました。Page描画まであと少しです。

ユーザー空間

PostScriptユーザが描画を行う際にPostScriptは「何に対して描画するか」を意識させない仕様になっています。

それが、15インチの640dot×480dotの古いブラウン管のディスプレイであったとしても、それが2400dpi×2400dpiの超高精細で印刷されるA1サイズのポスターであったとしても。 同じ画像の形であれば、全く同じコマンドで描画します。そのコマンドは出力先に依存しません。

この、ユーザーが描画を指示する際に使用する描画領域の空間をユーザー空間と呼びます。 ユーザー空間は出力先に依存しないため、論理上無限の広さを持っています。 座標空間はの一単位をどの程度の長さとするかは、ユーザーが自由に決めることができます。 デフォルトでは1座標単位 = 1/72インチです。一般的には72dpiといったほうがわかりやすいでしょうか。 これは、一般に広く使用されている、DTPポイントとほぼ同様です。というか、PostScriptが1座標単位 = 1/72インチとしたからこそ、その単位がDTPポイントして一般化したというのが正しいかもしれません。

デフォルトのユーザー空間ではその原点は、左下隅に設定されます。そこから上と右に向かって値が増えていきます。折れ線グラフ等の目盛りをイメージしていただくとわかりやすいと思います。

これは大事なことですが、ユーザー空間の座標の値は実数で表現しますユーザー空間は論理的な空間のため、解像度、dot、ピクセルの概念から解放されているためです。

バイス空間

バイス空間はユーザー空間で指示した画像を、実際に出力する物理デバイスに合わせて調整した描画領域です。 この、デバイスに合わせて調整した描画領域のことをバイス空間と呼びます。

この物理的な描画領域というのは、例えば15インチの640×480dotの古いブラウン管の画面出会ったり、プリンタの300dpiのA4での印刷領域であったり、 2400dpiのLetterサイズのJpeg画像であったりします。

ユーザー座標とデバイス座標の関係を示した図を以下に記します。出典はPDF Referenceからとなります。 f:id:hlsme:20201005223325p:plain

ユーザー空間から、デバイス空間への矢印のところにCTMと書かれています。これが座標変換に用いるパラメータでPostScriptの中でも最重要の要素となります。

次回はユーザー空間からデバイス空間への変換を一例にCTMについて押さえていきたいと思います。

Let's PostScript(9) 辞書Objectとカレント辞書

おきて破りの一日2記事です(笑)

辞書Object

辞書Objectは配列Objectと非常に似ていますが、データの管理の仕方が異なっています。 配列Objectの場合、データの管理はデータの配置順(Index)なのに対し、辞書ObjectはKeyとValueの組み合わせで管理します。

そのため、辞書Objectの作成は以下のように指定します。KeyとValueをペアで登録していく形ですね。 << key1 Value1 .... keyn valuen >>

これは実際のスクリプトを見るのが早いと思います。

GS><< /data1 123 /data2 (abcd) /data3 1.25>>
GS<1>/data2 get
GS<1>=
abcd
GS>

先ほどの配列と極めてよく似ていることがわかると思います。異なるのは配列ObjectがindexでValueを取得したのに対して、辞書ObjectはkeyでValueを取得しに行っているという点です。

仮想メモリの使われ方も前回の配列Objectと基本同様であると思います。

辞書Stack

PostScriptのインタープリタは辞書Objectを使用して、オペレータの検出をしています。インタープリタが参照する辞書Objectは階層化(辞書Stack)されていて、上から順に検索をしていきます。

PostScript起動時に辞書Stackに積まれている辞書Objectは

  • システム辞書:オペレーターの名前と処理が格納されている辞書
  • グローバル辞書:グローバル仮想メモリを使用する、辞書オブジェクト
  • ユーザー辞書:ローカル仮想メモリを使用する、辞書オブジェクト

の3種類です。ユーザー辞書、グローバル辞書、システム辞書の順で積まれています。そして、一番上に積まれている辞書Objectのことをカレント辞書と呼びます。 そしてカレント辞書用のオペレーターで使用頻度が高いものの一つがdefオペレータです。 defオペレータは、カレント辞書にkeyとValueのペアを登録します。辞書Stackに積まれた辞書はインタプリタが自動的に取得するため、OperandStackに頼らない柔軟なデータや手続きの制御ができるようになります。

カレント辞書への登録

以下に一例を示します。カレント辞書にtmpを登録、再登録することで、あたかも変数のように使っていることがわかるでしょうか。

GS>/tmp 0 def
GS>5 {/tmp tmp 1 add def} repeat
GS>tmp
GS<1>=
5

手続きObjectを登録することで関数のように扱うこともできます。

GS>/average {add 2 div} def
GS>100 20 average
GS<1>=
60.0

カレント辞書の切り替え

ここまでの例では、カレント辞書はユーザー辞書を使用していましたが、スクリプトが複雑化するに従いユーザー辞書では制御が難しくなります。

そのため、一般的には処理の塊ごとに専用のカレント辞書を用意し、処理の中のみその辞書を使用し、処理の完了後はカレント辞書を破棄するのが一般的です。

以下のような実装となります。

GS>/temp 1234 def
GS>10 dict
GS<1>begin
GS>/temp (abcd) def
GS>temp
GS<1>=
abcd
GS>end
GS>temp
GS<1>=
1234
GS>

最初の

GS>/temp 1234 def

はカレント辞書がユーザー辞書で、ユーザー辞書に対し、tempを1234で登録します。

次のコードで、10要素サポートする辞書を新たに作成し、beginオペレータで辞書Stackに作成した辞書をPushします。 それにより、カレント辞書が新たに作成した辞書に切り替わります。

GS>10 dict
GS<1>begin

この状態でtempをインタープリタが処理すると、カレント辞書である新たに作成した辞書のtempから定義(abcd)を検出します。

GS>/temp (abcd) def
GS>temp
GS<1>=
abcd

endオペレータでカレント辞書を辞書StackからPopします。それにより、カレント辞書がユーザー辞書に切り替わります。

GS>end

この状態でtempをインタープリタが処理すると、カレント辞書であるユーザー辞書のtempから定義1234を検出します。

GS>temp
GS<1>=
1234

このように、辞書スタックを操作することにより、定義の有効期間を制御することができるようになります。 これにより、処理の独立性を維持しやすくなります。

Let's PostScript(8) 配列とその動作の推測

もう少し早く、学んでおくべきだったようにも思えますが、座標系の話をする前に、配列Objectと辞書Objectについてまとめておこうと思います。 どうもそろそろ、このあたりも抑えないと先に進めないようです。

配列Object

複数のObjectを1つにまとめるObjectです。 [...]で使用します。実際に定義すると、このようになります。

GS>[1 (abc) /cde 1.25]
GS<1>1 get =
abc
GS>

これらの配列は、配列用のオペレータを使用することで、取得や更新が可能です。配列を理解するためには仮想メモリについての知識が必要となりますので、 いかに簡単に説明します。

仮想メモリのざっくりとした説明

仮想メモリはざっくりと以下の特徴を持っています。(との私の認識です)

  1. 複数の要素を含むObjectの実体を格納する領域である
  2. 自動生成、自動開放する
  3. 自動開放のタイミングは、該当Objectを参照する参照元がなくなった時

配列作成時の内部処理の推測

GS>[1 (abc) /cde 1.25]

が実行されたとき何が起こっているかは、以下のような感じだと推測しています。

  1. 作成した配列用のユニークIDのようなもの(PostScript内部でユーザーからは見えない、以降ユニークIDと記載する)を取得する。
  2. ユニークIDに紐づけた仮想メモリ領域を取得する。参照カウンタをインクリメントする、そこに実体1,(abc),/cde,1.25を格納する
  3. ユニークIDをOperandStackに格納する

getオペレータ実行時の内部処理の推測

GS<1>1 get =
abc
GS>

が実行されたときは、以下のようなことが起こっていると推測しています。

  1. getオペレータはOperandStadkからユニークIDとindexを取得する。
  2. ユニークIDに紐づく仮想メモリ領域とindexから、配列の値を取得しOperandStackへ格納する
  3. getオペレータが完了したことで、OperandStackからユニークIDがPOPされる。それにより、仮想メモリ領域の参照カウンタがデクリメントされる。
  4. 参照カウンタが0となったため、仮想メモリ領域は不要になったと判断し、領域を解放する。

あくまで推測なのですが、大きくは間違っていない気がします。 正解を知ろうと思ったら、GhostScriptのソースコードを見ないといけませんが、さすがにそこまでの気力はありません・・・

次は、辞書について、押さえていきたいと思います。

Let's PostScript(7) テキストの描画の基本

ポイントについて

DTPの一般的な話となるのですが、描画位置を指定するのにポイントという単位を使います。 ポイントにもいろいろな種類がありますが、PostScriptで用いるのはDTPポイントと呼ばれるものです。 もともとはXeroxのローカル仕様だったものですが、AdobeがPostScriptで採用したことが決定打となり、デファクトスタンダートとなって現在に至っています。

1ポイント=1/72インチ=0.3528mm

※1インチ=2.54センチ

座標変換や空間の種類を説明する際に詳細を説明する予定です。

テキスト描画の基本

テキスト描画の基本となるスクリプトを以下に記します。

/Times-Roman findfont
12 scalefont
 setfont
0 720 moveto
(Sample Text) show

これでshowpageすれば、TimesRomanフォントでSampleTextが描画されるのを確認できます。

このプログラムは、以下の処理を行っています。

/Times-Roman findfont

findfontオペレータはフォント名のリソースを探索し、存在している場合にはフォントの定義を仮想メモリに格納し、フォントの定義を格納したフォント辞書をOperandStackに格納する。

12 scalefont

OperandStackからフォント辞書とグリフサイズの指定を受け取り、グリフサイズを反映させたフォント辞書をOperandStackへ格納する

setfont

OperandStackからフォント辞書を受け取り、グラフィックス状態のフォント辞書へ反映する

0 720 moveto

描画位置を X=0 y=720ポイントへ移動する

(Sample Text) show

OperandStackへ(Sample Text)の文字列Objcetを格納する。showオペレータはOperandStackから文字列Objectを受け取り、カレントフォント(グラフィックス状態のフォント辞書)で文字列を描画する。

とりあえず、テキスト描画をしたかったので、最低限の部分を押さえました。

詳細はフォントを学ぶ際に抑えていこうと思います。

PostScriptは標準で以下のフォントはまず使用可能です。

  • /Times-Roman(タイムズローマン)
  • /Helvetica(ヘルベチカ)
  • /Courler(クーリエ)

使用可能なフォントの一覧は以下のスクリプトを実行するとえることができます。おそらくはものすごい量が出てきます。

(*) {==} 256 string /Font resourceforall

次は、ユーザー空間とデバイス空間を押さえていきたいと思います。

Let's PostScript(6) 繰り返し処理と算術オペレータ

繰り返し処理と算術オペレータ

PostScriptの繰り返し処理を順番に紹介すると同時に、その中で算術オペレータも使っていこうと思います。

一番わかりやすいのはrepaetオペレータだと思います。このようなものです。

回数 手続きObject repeat

repeatオペレータは整数の回数を受け取り、その回数分だけ手続きObjectを実行します。スクリプトにすると以下のような感じになります。 addオペレータは見れば自明だと思うので説明しません。

GS>1
GS<1>5 {dup 1 add} repeat
GS<6>stack
6
5
4
3
2
1

制限値になるまで繰り返すような処理の場合、forオペレータが向きます。

初期値 増分値 制限値 手続きオブジェクト

repeatオペレータと違い、forオペレータは増分した値をOperandStackに格納します。以下に一例を示します。

GS>1 2 10 {} for
GS<5>stack
9
7
5
3
1

なので、例えば30°ごとのsin値を知りたいというのであれば以下のようなスクリプトになります。

GS>0 30 360 {sin} for
GS<13>stack
0.0
-0.5
-0.866025
-1.0
-0.866025
-0.5
0.0
0.5
0.866025
1.0
0.866025
0.5
0.0

特定の条件を満たすまで処理を繰り返し続けるような処理に対しては、loopオペレータとexitオペレータを用います。

手続きObject loop

exit

で、exitが呼ばれるまでloopオペレータは手続きOjbectを繰り返し実行し続けます。exitオペレータが呼ばれない場合、無限ループとなるので注意が必要です。 例として、(iida)の文字列ObjcetをOperandStackがなくなるまで探索するスクリプトを示します。

GS>(kato) (ohyama) (iida) (konishi)
GS<4>{count 0 eq {exit} {(iida) eq {exit} if} ifelse} loop
GS<2>stack
ohyama
kato

次回は、テキスト描画の基本について、見ていきたいと思います。

Let's PostScript(5) 手続きObjectと制御オペレータ

手続きオブジェクト

{.....}は実行可能の属性持つ配列のObjectです。手続きObjectと呼ばれます。 手続きObjectは今まで見てきた”dup”や"copy"といった実行可能の属性を持つオブジェクトとは大きな相違があります。 それは、インタプリタがObjectを検出した時に現れます。

属性 配列 インタプリタ動作
実行可能 yes Operand StackにPushされる
実行可能 no インタープリタによって直ちに実行される
Literal Operand StackにPushされる

手続きObjectは実行可能であるにも関わらず、即実行せずにいったんOperandStackに格納します。このような動作を遅延実行と呼びます。 一方で、"dup"や"copy"といった単一の実行可能Objectの即実行される動作を即時実行と呼びます。

OperandStackの手続きオブジェクトを直接実行するにはexecオペレータなどを使用しますが、一般的には後述する制御オペレータによって実行するのが一般的ではないかと思います。

GS>{1 2 3 4 5 exch}
GS<1>exec
GS<5>stack
4
5
3
2
1

制御オペレータ

論理値(true,false)から、次に実行する手続きObjectを制御するオペレータを制御オペレータと呼びます。例えば、以下のようなスクリプトです。

ex.1
GS>true {(I'm True)} if
GS<1>=
I'm True

ex.2
GS>false {(I'm True)} if

ex.3
GS>true {(I'm True)} {(I'm False)} ifelse
GS<1>=
I'm True

ex.4
GS>false {(I'm True)} {(I'm False)} ifelse
GS<1>=
I'm False

ex.1では trueと{(I'm True)}}の2つのObjectがOperandStackに格納され、ifオペレータは論理値を受け取ります。今回はtrueだったため、手続きObjcet{(I'm True)}を実行しその結果(I'm True)ObjectがOperandStackに格納されます。

ex.2では falseと{(I'm True)}}の2つのObjectがOperandStackに格納され、ifオペレータは論理値を受け取ります。今回はfalseだったため、手続きObjcet{(I'm True)}は破棄されます。

ex.3では、trueと{(I'm True)}と{(I'm False)}の3つのObjectがOperandStackに格納され、ifelseオペレータは論理値を受け取ります。今回はtrueだったため、1つ目の手続きObjectである{(I'm True)}を実行しその結果(I'm True)ObjectがOperandStackに格納されます。2つ目の手続きObject{(I'm False)}は破棄されます。

ex.4では、falseと{(I'm True)}と{(I'm False)}の3つのObjectがOperandStackに格納され、ifelseオペレータは論理値を受け取ります。今回はfalseだったため、1つ目の手続きObjectである{(I'm True)}は破棄されます。2つ目の手続きObject{(I'm False)}を実行しその結果(I'm False)ObjectがOperandStackに格納されます。

これらの例では、論理値を直書きしていますが、実際には関係オペレータの結果の論理値を制御オペレータに渡すパターンが多くなります。以下のようなスクリプトになります。

ex.5
GS>1 1 eq {(Equal Object!)} if
GS<1>=
Equal Object!

ex.6
GS>1 0 eq {(Equal Object!)} if

ex.7
GS>1 1 eq {(Equal Object!)} {(Non Equal Object!)} ifelse
GS<1>=
Equal Object!

ex.8
GS>1 0 eq {(Equal Object!)} {(Non Equal Object!)} ifelse
GS<1>=
Non Equal Object!

ex.4~8はex.1~4までとほぼ変わらないのですが、ようやくスクリプトっぽい動作に見えてくるのが不思議。

次回は、繰り返し処理を見ていこうと思います。