PostScript言語 / 座標系

 まずはバイブル。 www.adobe.com から Learning → Adobe Developer Connection → Product and technology centers (more) とたどっていくと、Technologyの項に Adobe PostScriptがある。 このリンクの先はPostScript Technology Centerで、ここにPostScript Language Reference Manual - Third Edition(PLRM.pdf)という文書がある。 これがPostScript言語のリファレンスだが、長いので以後PLRMと書く。

 日本語がいいという人は、ASCIIから和訳本「PostScriptリファレンスマニュアル第3版」が出ている。 9,420円、高い。 当然持ってない。 英語が読める人は原書を読む方が安いし、確実だろう。 原書もAddison-Wesleyから出てるので、英語でも紙のほうがいいという人は、印刷するより買った方がおそらく早い(1000ページ近くある)。 $49.99らしいので、円-ドルレートによってはかなりお買い得だ。 今見たら(2012年10月)、日本のアマゾンで4,310円だった。

 わざわざPostScript「言語」というタイトルにしているが、Adobeによると「PostScriptは形容詞的に使う」そうなので、PostScriptの後ろには名詞入れろと。 なので、PostScript Languageとか、PostScript言語とか、PostScript Interpreterと書くのが正しいのだそうだ。 が、さっきのリンク元、「Adobe PostScript」と書いてありますがな。

最初の状態

 PostScriptのプログラムが走り始めた時点では、ページの左下が (0, 0) で、X座標は右方向が正、Y座標は上方向が正で、単位は 1/72 インチで、概ね1ポイントになる。 PLRMの4.3.1に書いてある通り「ポイント」は世界中できちんと決めれれたものではない(場所や業界で微妙に違う)のだが、DTPの世界では1/72インチをポイントと呼ぶことが多いので、まぁほとんどポイントだと思っていい。 PostScriptポイントと呼ぶ方法もあるようだ。 1インチは25.4mmなので、1ポイントは約0.353mm。 1ポイントの線を引くと、0.5mmのシャーペンの線よりも細い、という感覚。

 この座標系のことを「デフォルトユーザ空間」という。 ここでユーザ空間という名前が出てきたが、対になる言葉として「デバイス空間」がある。 デバイス空間は文字通りデバイスによって決まる空間で、たとえば360dpiのプリンタなら、プリンタの印字開始位置が (0, 0) で、X座標は右方向が正、Y座標はページを送っていく方向が正、単位は 1/360 インチだろうし、縦と横で解像度が異なるプリンタならば、X方向とY方向の単位が違うこともあるだろう。

 デバイス空間は基本的にPostScriptプログラムが意識することはない。 ユーザ空間でお絵かきをすれば、最後にPostScriptインタプリタがデバイスの情報に従ってデバイス空間に直して印刷してくれる。 このときに座標変換が必要だが、このときに使われる変換行列はdefaultmatrixで得られる。 ところで、変換行列って何よ?

変換行列

 英語ではtransformation-matrix。 PLRMでは4.3.3に書いてある。 高校の数学に出てくる一次変換の行列そのもの。 ゆとりで消えたり復活したりしてるみたいだが、消えてしまうのは困る。 高校生で理解するのは難しいとか言ってるみたいだが、理系で進学すれば大学1年次でも必要となる場面は多いし、現にここでも必要なわけだ。 3D CGなんて行列演算の塊ですよ? で、高校3年生でできないものが、大学1年生になって急にできるようになるかというと、そんなことはないでしょ、普通。 高校でできないヤツはきっと一生できん。 できないと思ってたヤツでも必要になれば絶対理解できる。 学問なんてそんなもん。

 高校数学の行列の教育方針が悪いんだろう。 計算のテクニックとかより、実際にこういう場面で役に立っているという実例を多くするべきなんだと思う。 高校の数学では一生懸命行列の計算をするが、大学に行くと計算はほとんどやらない。 一次方程式にしても、座標変換にしても、実際の計算はコンピュータがやるから。 じゃあ行列の何が大事なのかというと「積が定義されている」ことなんですよ。 たとえば「拡大・縮小」「斜めにする」「回転する」というのは単純な行列で書ける。 これを順番に適用して・・・つまり積をとることで、複雑な変換を実現できる。 人間は簡単な行列を作って、それを並べるだけでいい。 計算するのはコンピュータ。

 だいたい行列の積なんて、手で計算してたら面倒なだけ。 大学の行列の教科書は3次以上の行列も平気で扱うが、中身を全部書くことはしない。 「・・・」で省略して書く。 「・・・」で書いたって紙面が足りないというのに、全部書いてたらきりがない。 つまり、何行もの数式を書くのが面倒だから行列で書いたのであって、それを手で計算するのは本末転倒。 手で計算したら面倒で当たり前。 大学に行ってから気付きましたよ(計算は苦手だった)。

 話を元に戻そう。 高校の一次変換では原点が動かない変換を扱う(斉一次変換)が、PostScript言語では原点も動かす変換を使う。 アフィン変換と呼ばれることが多く、3D CGでも普通に使われている。 PostScript座標系は二次元だが、ひとつ余計に1を加えて3次元にすることで、移動も含めて行列の形で書けるようになる。

\begin{align*} ( x'\ y'\ 1 ) = (x\ y\ 1) \begin{pmatrix} a & b & 0 \\ c & d & 0 \\ t_x & t_y & 1 \end{pmatrix} \end{align*}

 実はこの書き方はあんまり一般的ではなくて、普通は転置した形

\begin{align*} \begin{pmatrix}x' \\ y' \\ 1\end{pmatrix} = \begin{pmatrix} a & c & t_x \\ b & d & t_y \\ 0 & 0 & 1 \end{pmatrix} \begin{pmatrix} x \\ y \\ 1 \end{pmatrix} \end{align*}

で書くことが多い。 行列を \(M\) にしてしまって、ベクトルを \(( x, y, 1 )\) で書くと前者のほうが場所を食わないが、ベクトルもまとめて \(\bvec{x}\) と書いてしまえばどちらも食うスペースは同じ。 後者は \(\bvec{x}' = M \bvec{x}\) と \(M\) を係数とした一次関数のように書けるので、こちらの方が一般的なのだろう。 高校の一次変換(高校でやれば、ですけどね)もこの形だったはず。 前者なら \(\bvec{x}' = \bvec{x} M\) になる。 いずれにせよ、 \(\bvec{x}\) の形を決めると \(\bvec{x}\) と \(M\) の順序は決まってしまい、入れ替えることはできない。 次数が合わなくなるからだ(積の計算自体ができなくなる)。

 どっちにしろ、行列を計算(こんなの一度だけ計算すれば十分!)してみればすぐ分かるとおり、

\begin{align*} \left\{ \begin{aligned} x' &= ax + cy + t_x \eol y' &= bx + dy + t_y \end{aligned} \right. \end{align*}

になる。 つまり、この変換を行うと、x軸正方向のベクトル \(( x,\ 0 )\) は \(( ax + t_x,\ bx + t_y )\) に、y軸性方向のベクトル\(( 0,\ y )\) は \(( cy + t_x,\ dy + t_y )\) になる。 これは原点が \(( t_x,\ t_y )\) に移った上で、単位ベクトル ( 1, 0 ) と ( 0, 1 ) が \(( a,\ b )\) と \(( c,\ d )\) に移ることを示している。

 \(t_x\cdot t_y\) だけを設定すれば平行移動、\(b\)と\(c\)を0にして\(a\)と\(d\)を同じ値にすると拡大縮小になる。 さらに\(b\)を設定すると斜体を書くことができる。 \(d\)を負にすれば上下方向に反転するから、Y軸の正方向が下になる。 変換行列はあちこちに出てくる。 全体の座標系変換、画像描画時の変換行列、それと文字列描画時の、つまりフォントに適用されるのも変換行列だ。

 PostScriptプログラム中に書くときは、決まりきっている0と1をいちいち書くのは面倒なので、[ a b c d tx ty ] と書く。 つまり、「X方向の単位ベクトルの行先」「Y方向の単位ベクトルの行先」「原点の移動量」を並べて書く。 [ ]は配列を示し、内部のオペレータは全部実行されるから、オペレータsincosを直接書くこともできて、 [ 45 cos 45 sin -45 sin 45 cos 0 0 ]と書けば、正方向(注: X座標の正方向からY座標の正方向へ回る方向。デフォルトでは反時計回りだが、Y座標の正負を逆にしていると時計回りになる)に回る変換行列になる。

 ちなみにPostScript言語の三角関数は度を扱う。 それと、どうでもいいことだがtanがない。 dup sin exch cos divと書けと。 atanはあるが度単位で返してくるので、πを定義するときにお手軽に1 atan 4 mulとはできなかったりする。 ついでにexpはCで言うところのpowなので、\(e\)を定義するのに1 expともできない(スタックアンダーフローになる)。 どちらも自分でベタっと数字を書く必要がある。

 実際にGhostscript(以下GS)でやってみる。

$ gs -q -dNODISPLAY
GS>matrix defaultmatrix ==
[1.0 0.0 0.0 -1.0 0.0 792.0]

この場合はユーザ空間とデバイス空間の単位は一対一で対応しているが、Y軸方向は上下逆だということが分かる。 先ほどの式に当てはめればユーザ空間の ( 0, 0 ) はデバイス空間の ( 0, 792 ) に移る。 792=11×72だから、デバイス空間の原点から11インチだけY軸の正方向に行ったところに対応する。 これはGSのデフォルトのページサイズがLetterサイズ(8.5インチ×11インチ)だからだ。 このデバイスの原点がページ左上だとすれば、ユーザ空間の ( 0, 0 ) はデバイス空間できちんと左下に対応している。

 GSではコマンドラインの-rオプションで解像度を指定できる。

$ gs -q -dNODISPLAY -r36x144
GS>matrix defaultmatrix ==
[0.5 0.0 0.0 -2.0 0.0 1584.0]

この例では横を36dpi、縦を144dpiに設定している。 ユーザ空間の原点はデバイス空間の ( 0, 1584 ) に、( 0, 1 ) は ( 0.5, 0 ) に、( 0, 1 ) は ( 0, 1582 ) に移る。 36dpiでは1動くと1/36インチ動くのだから、0.5動いたところは確かに1/72インチだ。 144dpiでは1動くと1/144インチ動くから、1/72インチ動かすには2動かさないといけない。 1584=11×144だから、ユーザ空間の原点はやはりデバイス空間の原点から11インチの位置、つまり、デバイス空間で左下に対応する。

座標系の操作

 PostScriptインタープリタはカレント変換行列(Current Transformation Matrix、略してCTM)を保持している。 CTMはcurrentmatrixで得られる。 オペレータで何か座標が指定されるたび、その座標をCTMを使ってデバイス座標系に変換している。 だから、実行直後は

$ gs -q -dNODISPLAY -r36x144
GS>matrix defaultmatrix ==
[0.5 0.0 0.0 -2.0 0.0 1584.0]
GS>matrix currentmatrix ==
[0.5 0.0 0.0 -2.0 0.0 1584.0]

defaultmatrixと同じになっている。 [ 1 0 0 1 0 0 ]とは限らない点に注意。

 concatオペレータを使うとCTMにさらに変換行列を適用することができる。 行列の乗算は結合法則は成り立つが、可換ではない。 つまり、 \(M_1\cdot M_2\cdot M_3 \cdot M_4\) を \(M_1\cdot(M_2\cdot(M_3\cdot M_4))\) と計算したり、 \((M_1\cdot M_2)\cdot(M_3 \cdot M_4)\) と計算したりすることは許されるが、 \(M_4\cdot M_2\cdot M_3\cdot M_1\) のように順序を変えることは許されない(これも高校で習う知識だ)。 したがって、CTMのどちら側から新しい変換行列を適用するかが重要になるが、これは \(M\) と \(\bvec{x}\) の間に入る。

 つまり、追加で適用したい変換行列を \(N\) とすると、PostScript言語仕様の様に \(\bvec{x}' = \bvec{x} \cdot M\) と書くなら \(\bvec{x}' = \bvec{x}\cdot N\cdot M\) となるので、新しいCTMは \(N \cdot M\) になる。 普通書くように \(\bvec{x}' = M \cdot \bvec{x}\) とするなら、 \(\bvec{x}' = M \cdot N \cdot \bvec{x}\) となるので、 新しいCTMは \(M \cdot N\) になる。 書き方によって逆になってしまうのがややこしいが、どちらにせよ結合法則で計算の順序を変えれば、つまり、CTMを先に計算するのではなく、 \(\bvec{x}\) の方から順に計算していけば、 \(\bvec{x}' = ( \bvec{x} N ) M\) あるいは \(\bvec{x}' = M ( N \bvec{x} )\) という計算をすることになるので、ユーザ空間で表された座標をデバイス空間に変換するときは、後からconcatした変換行列を先に適用するということだ。

 実際にはいちいち行列を書くのは面倒なので、便利なオペレータが用意されていて、PLRMの8.1のCoordinate System and Matrix Operatorsにまとめられている。 よく使うのはtranslatescalerotateだろう。 tx ty translate は点を ( tx, ty ) だけ動いた位置に移動する。 sx sy scale は点のX座標をsx倍、Y座標をsy倍にした位置に移動する。 angle rotate は点を原点周りにangle度だけ回したところに移動する。 そういう変換行列を作ってconcatまでやってくれる。

 最後に、CTMはいままでconcatで指定した変換行列をすべて覚えているわけではない。 concatされた時点で積を計算して、つまり、 \(N M\) (あるいは \(M N\) )を計算して新しいCTMとしてしまって、古いのは忘れてしまう。 したがって、あとで元に戻す必要がある場合は、gsaveを実行してからconcatを実行し、描画が済んだらgrestoreで戻す必要がある。

座標系変換

 今、 10 10 scale としてみる。 描画位置 ( 1, 0 ) はデフォルトユーザ空間の ( 10, 0 ) に、 ( 0, 1 ) は ( 0, 10 ) へ移る。 もし、デフォルトユーザ空間に目盛を書いて、 10 10 scale してからもう一度目盛を書くと、デフォルトユーザ空間の目盛を縦横10倍に引き伸ばしたものと、 10 10 scale を実行した後に書いた目盛が一致する。

 元の状態に戻してから、今度は 20 30 translate とする。 描画位置 ( 0, 0 ) はデフォルトユーザ空間の ( 20, 30 ) に移る。 同じように目盛を書くと、デフォルトユーザ空間の目盛を右へ20、左へ30動かしたものが 20 30 translate を実行した後の目盛になるだろう。

 では、 10 10 scale 20 30 translate とした後はどうか。 後に書いたほうが先に適用されるのだから、 ( 0, 0 ) はtranslateで ( 20, 30 )に移ってからscaleで ( 200, 300 ) に移る。 ( 1, 0 ) はtranslateで ( 21, 30 )に移ってからscaleで ( 210, 300 ) に移る。 ( 0, 1 ) はtranslateで ( 20, 31 )に移ってからscaleで ( 200, 310 ) に移る。

 目盛を書くことを考えると、まず、translate前に目盛を書いて、その上にtranslateしてから目盛を書くと、translate後の目盛の原点は、translate前の目盛の ( 20, 30 ) になるだろう。X軸・Y軸の方向と大きさはtranslate前後で同じである。 さらに、scaleする前、つまり、デフォルトユーザ空間でも目盛を書いてみると、scale後=translate前に書いた目盛の原点はデフォルトユーザ空間の原点と同じだ。 scale後の空間で ( 1, 0 ) に目盛を打つと、それはデフォルトユーザ空間の ( 10, 0 ) に描画されるから、デフォルトユーザ空間の「10」と書いた目盛の上にscale後の空間の「1」という目盛が載ることになる。 つまり、scale後の目盛はデフォルトユーザ空間の目盛を10倍に引き伸ばしたものになっている。

 このとき大事なのは、translate前の空間の状態を知らなくても、translate前後の座標の(相対的な)関係が分かる、ということである。 大きさが変わっても、回っていても、ひしゃげていても、とにかくtranslate前の座標でtranslate後の空間を表すことができ、それはいつも同じ関係になる。 だから、translate前に好きなように空間をゆがめておいて、最後に 20 30 translate とすれば、新しい座標系の原点は常に今の座標系の ( 20, 30 ) に移るわけだ。

 これはscalerotateconcatでも同じで、また、デフォルトユーザ空間・デバイス空間に至るまで追いかけていくことができる。 逆にたどればデバイス空間あるいはデフォルトユーザ空間から現在の空間を知ることができる。 つまり、デフォルトユーザ空間に目盛を書いて、それを10倍に引き伸ばし、さらにこの座標系の原点を引き伸ばした後の目盛で右へ20、上へ30動かしたものが最終的な目盛になる。 実際、この目盛の原点は、重なっているデフォルトユーザ空間の目盛を見ると ( 200, 300 ) に、( 1, 0 ) は ( 210, 300 ) に、( 0, 1 ) は ( 200, 310 ) になっているはずである。

 逆に、 20 30 translate 10 10 scale とした場合、( 0, 0 ) は ( 20, 30 ) へ、( 1, 0 ) は ( 30, 30 ) へ、 ( 0, 1 ) は ( 20, 40 ) へ移る。 変換を逆にすると同じ点に移らない。 これが「行列の積は可換ではない」ということである。 目盛はまずデフォルトユーザ空間の目盛を右へ20、上へ30動かし、この新しい目盛を10倍に引き伸ばしたものになる。 この目盛の ( 0, 0 ) はデフォルトユーザ空間の目盛の ( 20, 30 ) に、( 1, 0 ) は ( 30, 30 ) に、( 0, 1 ) は ( 20, 40 ) になっているはずだ。

 まとめると、描画したオブジェクトに適用される変換は後に指定したものが先なのだが、デフォルトユーザ空間から目盛を書き換えていくと、つまり、今の座標系がどうなっているかは、先に指定したものを先に適用することになる。 使う側としては今の座標系がどうなっているかが重要なので、結局、座標系に対して指定した順番で変換を適用していけばよい。 高校の1次変換でも「点がどこへ移るか」と「元の座標系がどうなるか」でこんがらがっちゃう人は多いだろう。 これは仕方がないことだと思うが、いつでも「変換前の座標系で書いた目盛の上に、原点・( 1 , 0 )・( 0, 1 ) を1次変換して目盛を書いたらどうなるか?」を考えればよい。

 少し実例を挙げよう。 PostScript言語では原点がページの左下にあるが、用紙をA4縦(横210mm・縦297mm)にして、原点を左上に移し、座標系をmm単位にして、Y軸の下方向を正にしたければ、

/mm2pt  { 25.4  div  72  mul } def
<< /PageSize [ 210  mm2pt  297  mm2pt ] >> setpagedevice
1  mm2pt  dup  neg  scale
0  -297  translate

とすればよい。 まず、mmをPostScriptポイントに直すプロシージャを定義している。 PageSizeの指定は今の座標系ではなく、デフォルトユーザ空間で指定するので注意。 つまりポイント単位で指定しなければいけないが、手で計算するのではなくてこのようにPostScriptインタープリタに計算させたほうが楽。 続いて1mmをポイントに直して、座標系を引き伸ばす。 このとき、Y軸のほうは値をマイナスにする。 これで座標系は1mm単位になり下が正になるが、原点はページ左下のままだ。 最後に原点を297mmだけ上に動かすのだが、新しい座標系で指定するので、上はマイナスである。

 この操作をscaletranslateを逆に、つまり、translateを先にやると、移動量はデフォルトユーザ空間で指定するのだから、Y座標の移動量はプラスになり、297mmをポイントに変換する必要がある。 つまり、この場合は先にscaleしてしまった方が楽だ。 このように、scaletranslateを連続して行う場合、たいていの場合はscaleを先にやってしまったほうが楽である。


Copyright (C) 2012-2024 akamoz.jp