home / uni / ps / image-basic PostScript言語 / 画像を扱う

 とりあえず画像で四角を書いてみる。

%!PS-Adobe-3.0
400 300 8 [ 1 0 0 1 0 0 ] <987654> false 3 colorimage

意外と簡単でしょ? 最初の400・300は画像の幅と高さでピクセル単位。 次の8は1ピクセル・成分あたりのビット数。 次の配列は変換行列で、今は座標変換なしを指定。 次の<987654>がデータで、とりあえず16進数2桁ずつでRGB。 次のfalseはデータソースがイッコしかないよ、ということを言っていて、3はRGBで3プレーンあるよ、ということ。 つまり、画像の形式としては24bppのRGBで、RGBRGB・・・の順にひとつのデータソースからデータを読み込む。 今はデフォルトユーザ空間から座標変換をしてないから、400/72インチ×300/72インチの小豆アイスみたいな色の四角が書かれたはず。

 今、データは<987654>と指定した。 これ、実は文字列を16進数で表しただけ。 試しに普通の文字列(abc)としてもちゃんと表示される。 画像データとしては400×300×3=360,000バイトいるはずだが、3文字・・・つまり3バイトしか指定してない。 文字列で指定した場合、データが足りなかったら文字列を繰り返し読み込むことになっている。 だから3文字指定すると単色の四角になるわけだ。 でもこれならrectfillした方が早い。

 じゃぁ、こんなのはどうだろう。

%!PS-Adobe-3.0
/r 0 def /g 0 def /b 128 def
256 256 8 [ 1 0 0 1 0 0 ] 
{
	3 string dup 0 r put dup 1 g put dup 2 b put
	/r r 1 add def r 256 eq {
		/g g 1 add def /r 0 def
	} if
}
false 3 colorimage

文字列のところをプロシージャにしてみた。 プロシージャの場合はデータが必要になるたびに呼び出される。 引数は何もなく、データはやはり文字列を作ってその中に返す。 データがもうない場合は空の文字列() を返せば(間違って空白を入れないように)、そこで読み込みも描画も打ち切ってくれる(エラーにはならない)。 特に何バイト返さなければならないという規則はなく、ここでは3文字の文字列を返している。 呼ばれるたびにrをインクリメントし、256まで行ったら0に戻してgをインクリメント、そして、rgbの順に文字列にputして返している。 つまり、青がちょっと混ざった赤と緑のグラデーションになるわけだ。

 colorimageにはファイルも渡せる。 これが本命なのだが、別のページで説明する。

home / uni / ps / image-basic 画像の形式と色空間

 PostScript言語の画像形式の指定は、色成分の数と、各色成分ごとのピクセルのビット数で決まる(PLRM 4.10.2)。 普通扱うのは8bppのグレースケール、8bbp×3(24bpp)のフルカラー、4bpp・8bppのインデックスカラー、1bppの白黒だろう。 グレースケール・インデックスカラー・白黒は色成分がひとつ、フルカラーはRGBで3つである。 インデックスカラーもデータ形式としての成分数はひとつ。 最終的に各ピクセルは実数で0から1の値を持つ。 元のピクセルの値は整数だから、基本的には整数の最小値、つまり0が実数値の0に、最大値、たとえば8bppなら255が実数値の1に対応する。

 画像データはバイト列として表現される。 各ラインはMSB側から各ピクセルが詰めて置かれる。 フルカラーの場合、普通はRGBの順にピクセルを詰めて並べる。 各ラインの開始位置はバイト単位になっていなければならず、バイト単位にならない場合は0を埋める。 この部分は当然だが無視される。 ちなみに、最初のラインが上になるか下になるかは、後で説明するイメージ変換行列とCTMによって変わる。

 実際にどの色形式になるかは、オペレータによって異なる(PLRM 4.10.4)。 今紹介したcolorimageではパラメータで指定した成分の数で色形式が決まる。1ならグレースケール、3ならRGB、4ならCMYKになる。 画像を表示するオペレータにはimageもあるが、このオペレータはパラメータをバラバラにスタックに積んで指定する方法と、辞書にまとめて指定する方法がある。 バラバラに指定する場合はかならずグレースケールになる。 辞書にまとめて指定する場合は現在選択されている色空間で決まる。 なので辞書で指定するパラメータの中に成分数を明示的に指定するパラメータはない。

 色空間というなにやらまたよく分からないものが出てきたが、思ったほど難しくない(カラーマッチングとかやらなければ、ね。PLRM 4.8)。 色空間の指定はsetcolorspaceオペレータで行う。 現在の色空間を取得するのはcurrentcolorspaceだ。 辞書を引数とするimageを使う場合、image実行前に白黒なら/DeviceGray setcolorspace、RGBフルカラーなら/DeviceRGB setcolorspaceを実行しておけばよい。 インデックスカラーの場合は後述。 このほかにsetrgbcolorといったオペレータもあるが、これらのオペレータは色も一緒に指定しなければならない。 色を指定しなければいけない場合は便利だが、画像の場合は色を指定する必要はないので、setcolorspaceを使った方が悩まなくていい。

 デフォルトでは色空間はDeviceGrayになっている。 にもかかわらず最初の例でcolorimageがきちんとカラーで描画しているのはcolorimageは色成分の数によって勝手に色空間を切り替えてくれるから。 このとき、現在の色空間は変更されずそのまま。 色空間はgsavegrestoreでの保存・復帰の対象になる。 colorimageを使うのでなければsetcolorspaceの前にgsaveしておくとよい。

home / uni / ps / image-basic インデックスカラー

 インデックスカラーにはパレットが必要だ。 自分でパレットを管理してDeviceRGBのまま色インデックスをRGBに変換してもいいが、Indexed色空間にしておけばPostScriptインタープリタが展開してくれる。 そのときにパレットをインタープリタに教える必要があるが、これはsetcolorspaceのときに設定する(PLRM 4.8.4)。

[ /Indexed base hival lookup ] setcolorspace

つまり、配列の中にパラメータをまとめてsetcolorspaceに渡す。 baseはベースとなる色空間で、普通はDeviceGrayDeviceRGBだろう。 このパラメータによってlookupの解釈の仕方が決まる。 hivalはインデックスの最大値。16色なら15、256色なら255。 lookupがパレットで、ベースとなる色空間の色成分数× ( hival + 1 ) バイトの文字列を指定する(プロシージャにすることもできる)。 16色で白黒なら16バイト、カラーなら48バイトということだ。 内容は各インデックスに対応する色成分の値を、setcolorで指定する順に詰めたもの。 要するにRGBRGB...というパレットテーブル。

 以下に赤・青・緑とも6階調とした場合(6×6×6=216色)のパレット設定の例を示す。255÷5がちょうど51になるので、0, 51, 102, 153, 204, 255 という6つの値を組み合わせて設定することになる。 コード中、numclrが色数で216になる。 setcolorspaceに指定するときは1を引く必要があるので注意。

/numclr 6 6 6 mul mul def
/pal numclr 3 mul string def
/r 0 def /g 0 def /b 0 def /idx 0 def
6 { 6 { 6 {
            pal idx r put /idx idx 1 add def
            pal idx g put /idx idx 1 add def
            pal idx b put /idx idx 1 add def
            /r r 51 add def
        } repeat /r 0 def /g g 51 add def
    } repeat /g 0 def /b b 51 add def
} repeat
[ /Indexed /DeviceRGB numclr 1 sub pal ] setcolorspace

home / uni / ps / image-basic 辞書形式のimage

 このパレットを実際に使ってみる。 辞書形式のimageを使う必要がある。

36 36 scale
/idx 0 def
<<  /ImageType 1 /Width 12 /Height 18 /BitsPerComponent 8
    /Decode [ 0 255 ] /ImageMatrix [ 1 0 0 1 0 0 ]
    /DataSource
    {
        1 string dup 0 idx put
        /idx idx 1 add def
    }
>> image

 辞書形式のimageはオペランドとして辞書(画像辞書、PLRM 4.10.5)をひとつとる。 この辞書の内容は以下の通り。
ImageType普通の画像は1。ちなみに3は明示的マスク、4はカラーキー(クロマキー)マスク。
Width画像の幅。ピクセル単位。
Height画像の高さ。ピクセル単位。
BitsPerComponent色成分あたりのビット数。
Decode後述。
ImageMatrix画像変換行列。
DataSource画像データ。文字列・プロシージャ・ファイルのいずれか。
MultipleDataSourceデータソースが複数ある場合にtrueを指定する。ひとつの場合はfalse(省略可)。
Interpolate拡大時に補間をしたい場合にtrue。しない場合はfalse(省略可)。

パラメータ自体は大して変わらない。 ImageTypeDecodeが増えているが、色成分数は色空間から取得するので設定項目がない。 逆に言うと色空間と画像データの形式があってないと正しく動かないということだ。 辞書はキーを指定する必要があるのが面倒だが、スタックに積むのと違って、あとから値を参照したり書き換えたりしたいときは便利だ(スタックだとindexに渡す値が変わってしまって面倒だからね)。 そういう例もあとで出すつもり。

 Decodeは画像データの整数値と描画する実数値の対応を示す配列で、要素数は色成分数の2倍になる。この配列はほとんど指定方法が決まっていて、以下のようになる。
グレースケール[ 0 1 ]
RGB[ 0 1 0 1 0 1 ]
8bppインデックス[ 0 255 ]
4bppインデックス[ 0 15 ]
白黒(1bpp)[ 0 1 ]

今の場合、パレット自体は216色だが8bppのデータなので[ 0 255 ]を指定する。

 DataSourceはプロシージャを指定している。 この画像は12×18ピクセルと非常に小さいので、scaleで引き伸ばして表示している。

home / uni / ps / image-basic 座標変換

 画像には画像の座標系がある(PLRM 4.10.3)。 まぁ、普通に画像の始まりが ( 0, 0 )、反対側の端が ( w, h ) なだけ。 これをユーザ空間に描画するときに使うのが「画像変換行列」。 CTMがユーザ空間をデバイス空間へ移すのに使われるのに対し、画像変換行列はユーザ空間を画像座標系に移すために使われる。 向きに注意すること。 だから、CTMに[ 2 0 0 2 0 0 ]concatすると描画される図形は縦横2倍になるのに対し、画像変換行列を[ 2 0 0 2 0 0 ]にすると画像は縦横半分になってしまう。

 始まりは座標の小さい方、終わりは大きい方と決まっているので、画像の上下左右はCTMと画像変換行列で変わるわけだ。 画像変換行列を[ 1 0 0 1 0 0 ]にしてデフォルトユーザ空間で画像を書くと、始まりが左下になり、72dpiで描画される。 これを上下逆さにしたいのなら、画像変換行列を[ 1 0 0 -1 0 0 ]にするか、1 -1 scaleとするか、どちらかをやればよい。 両方やると元に戻っちゃうからね。

 PLRMでは次のような方法が推奨されている。 画像の幅をw(ピクセル)、高さをh(ピクセル)としたとき、画像変換行列を[ w 0 0 h 0 0 ]とすると、ユーザ空間で画像の始まりが ( 0, 0 )、終わりが ( 1, 1 ) になる。 これをCTMで、つまりscaletranslateで大きさ・位置を調整する。 この方法はユーザ空間での位置を決めるのは楽だが、画像変換行列を適用したところで画像の縦横比(アスペクト比)の情報がなくなってしまう欠点がある。

 そこでちょっとしたプロシージャを作ってみた。

% imgdict w' [ setimagewidth ] imgdict
/setimagewidth {
	dup	% imgdict w' w'
	2 index /Width get div	% imgdict w' w'/w
	2 index /Height get mul	% imgdict w' h*w'/w
	scale
} def

% imgdict h' [ setimageheight ] imgdict
/setimageheight {
	dup	% imgdict h' h'
	2 index /Height get div	% imgdict h' h'/h
	2 index /Width get mul	% imgdict h' w*h'/h
	exch scale
} def

変換行列を[ w 0 0 h 0 0 ]とした上で、ユーザ空間で描画したい幅(setimagewidth)か高さ(setimageheight)を指定すると、CTM(辞書の画像変換行列ではないので注意)を変更してくれる。 このプロシージャは辞書が引数だから楽に書けているという側面があって、たとえばcolorimageの引数列に対して計算することもできるけれども、もう少しややこしいプロシージャになる。 それに、colorimage用に作ってしまうと、5オペランド型(辞書をオペランドとはしない)のimage用にはまた作り直さないといけない。

 このプロシージャはこんな風に使う。 さっきと同じ画像を表示する場合、さっきの辞書から画像変換行列を書き換えて、imageの前にsetimagewidthを入れる。 scalesetimagewidthの操作に含まれてしまう。 CTMをいじってしまうので、gsavegrestoreも忘れないこと。

gsave
/idx 0 def
<<  /ImageType 1 /Width 12 /Height 18 /BitsPerComponent 8
    /Decode [ 0 255 ] /ImageMatrix [ 12 0 0 18 0 0 ]
    /DataSource
    {
        1 string dup 0 idx put
        /idx idx 1 add def
    }
>>
36 12 mul setimagewidth
image
grestore
gsaveを辞書の前に入れているのは、あとでpreparejpegなんかを作ったときに、このプロシージャが辞書を作ると同時に色空間を変更するからだ。 つまり、この場合は辞書を作る前に色空間を保存しないと間に合わないわけだ。

 表示の位置を変えたい場合はtranslateを入れればいいのだが、この方法には面白い特徴がある。 setimagewidthの前のtranslateは当然その時点でのユーザ空間での移動になるが、setimagewidthの後のtranslateは、横方向は画像の横幅を1、縦方向は画像の高さを1とした空間での指定になる。 たとえばLetterサイズ(8.5インチ×11インチ)の中央に画像を出したければ、画像の幅と高さから表示開始位置をいちいち計算するのではなく、

gsave
<< ... >>
8.5 36 mul 11 36 mul translate
36 12 mul setimagewidth
-0.5 -0.5 translate
image
grestore

でいい。 つまり、現在位置をページ中央に持っていってから、setimagewidthで画像の幅を設定して、この状態で画像の縦横半分だけ位置をずらして描画させるわけだ。 これで設定した位置と画像中央の位置が一致するようになるから、画像はページ中央に描かれるようになる。 デフォルトユーザ空間で画像を右上にぴったりくっつけたい場合も、

gsave
<< ... >>
8.5 72 mul 11 72 mul translate
36 12 mul setimagewidth
-1 -1 translate
image
grestore

でいい。 画像のサイズを縦横0.9倍にして周囲にフチを付けるなら、0.05 0.05 translate 0.9 0.9 scaleとすればいいわけだ。

 上下を逆にしたい場合、CTMをいじってもいいが、画像変換行列の方を [ w 0 0 -h 0 h ]とした方が楽だろう。 これで画像の始まりが ( 0, 1 )、つまりデフォルトユーザ空間の左上に、終わりが ( 1, 0 ) 、つまり右下に対応するようになる。 ユーザ空間の方をすでに上下逆さにしている場合は、当然上下の対応が逆になる。 こんなプロシージャを作っておくといいのかもしれない。

% imgdict [ vflipimage ] imgdict'
/vflipimage {
	1 dict begin
	/mtx 1 index /ImageMatrix get def
	mtx 5 mtx 3 get mtx 5 get add put
	mtx 3 mtx 3 get neg put
	dup /ImageMatrix mtx put
	end
} def

Copyright (C) 2012 You SUZUKI

$Id: image-basic.htm,v 1.4 2013/10/09 03:51:27 you Exp $