PostScript自体はどんな形式でもデコードできるはずだが、ここではとりえあず256色以下のインデックスカラーを扱う。 基本はRFC2083なので無料で取得できる。 最新はISO/IEC15948だが、W3C側が無料で公開しているようだ。 この辺から取得できる。 PNGはFlateDecodeフィルタでデコードできるのだが、FlateDecodeにはデータ本体のみを渡さなければならない。 この部分を自分で作る必要がある。
まず先頭に「0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A」という8バイトのシグネチャが入っている。 この後ろにPNGのチャンクが並ぶ。 PNGのチャンクはまず4バイトの長さがあり、続いて4バイトのチャンクタイプ、チャンクデータ本体があり、最後にCRC。 数値は全部ビッグエンディアンで、チャンクの長さはデータ本体だけを数える。 いわゆるIFF形式とはIDと長さが逆で、最後にCRCが付いている。 パディングも入れない。
最初に出てくるチャンクはIHDR。 ここにタイプ、画像の幅、高さ、色成分ごとのビット数などが入っている。 インデックスカラーの場合はパレットがPLTEチャンクに入っていて、これは必ずIDATよりも前にある。 データ本体はIDATチャンクに含まれるが、複数のIDATチャンクに分割することができる。 分割した場合、IDATチャンクは連続して(他のチャンクを挟まずに)置かなければならない。 とりあえずインデックスカラーだけを考えると以下のようになっていることが分かる。
データ | サイズ [bytes] | 備考 |
---|---|---|
0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A | 8 | シグネチャ(0x80+タブ、PNG、CRLF、Ctrl+Z、LF) |
chunklen | 4 | IHDRチャンク |
"IHDR" | 4 | |
Width | 4 | 画像の幅 |
Height | 4 | 画像の高さ |
Bit depth | 1 | 各成分1サンプルあたりのビット数 |
Colour type | 1 | 画像のタイプ、インデックスカラーは3 |
Compression method | 1 | 圧縮方式、現在はdeflate/inflateのみなので0 |
Filter method | 1 | 予測フィルタの方式、現在は0のみ |
Interlace method | 1 | インターレース方式、0でインターレースなし |
CRC | 4 | CRC |
PLTE以外のチャンク(0個〜繰り返し) | ||
chunklen | 4 | PLTEチャンク |
"PLTE" | 4 | |
Red(0) | 1 | 赤成分の値 |
Green(0) | 1 | 緑成分の値 |
Blue(0) | 1 | 青成分の値 |
(サイズで決まる長さだけ繰り返し) | ||
CRC | 4 | CRC |
IDAT以外のチャンク(0個〜繰り返し) | ||
chunklen | 4 | IDATチャンク |
"IDAT" | 4 | |
image data | chunklen | データ本体 |
CRC | 4 | CRC |
(1個〜繰り返し) |
チャンクの後ろに必ずCRCが入るので注意。 IHDRは13バイトになるのだが、将来拡張されることがあるかどうかまでの言及はないようだ。 画像の幅と高さは4バイトである。 パレットの実際の色数はPLTEチャンクのサイズで決まる。 PLTEチャンクのサイズが3の倍数ではない場合や、1サンプルあたりのビット数から決まる色数を超えたパレットデータがある場合はエラー扱い。 IDATチャンクに実際のデータが入るが、複数のIDATに分割できる。 FlateDecodeフィルタにはIDATチャンクの中身をつなげて渡す必要がある。
あとどうでもいいことだけれど、colorのつづりがイギリス風。
以上を元に適当に作ったのがこれ。
% errobj errname [err] - /err { errordict exch get exec } def % value obj key [ vokput ] - /vokput { 3 -1 roll put } def % file bytes [ readbytesbe ] (int true | false ) /readbytesbe { string readstring { 0 exch { exch 256 mul add } forall true } { false } ifelse } def % file [ readdwordbe ] ( int true | false ) /readdwordbe { 4 readbytesbe } def % file str [ findpngchunk ] ( len true | false ) /findpngchunk { 4 dict begin /chunkid exch def /src exch def /idbuf 4 string def { src readdwordbe not { false } { /len exch def src idbuf readstring not { pop flase } { chunkid eq { exit } if src fileposition len add 4 add src exch setfileposition } ifelse } ifelse } loop len true end } def % a b [minimum] min /minimum { 2 copy lt { pop } { exch pop } ifelse } def % file [preparepng] imgdict /preparepng { 5 dict begin /src exch def /imgdict << /ImageType 1 /Width 0 /Height 0 /ImageMatrix matrix /MultipleDataSources false /DataSource null /BitsPerComponent 8 /Decode [ 0 255 ] /Interpolate false >> def /buf 256 string def /datarest 0 def { src buf 0 8 getinterval readstring not { pop stop } if (\211PNG\r\n\032\n) ne { stop } if % header src (IHDR) findpngchunk not { stop } if 13 ne { stop } if src readdwordbe not { stop } if imgdict /Width vokput src readdwordbe not { stop } if imgdict /Height vokput src read not { stop } if imgdict /BitsPerComponent vokput src read not { stop } if 3 ne { stop } if % indexed src read not { stop } if 0 ne { stop } if % deflate src read not { stop } if 0 ne { stop } if % filter-predict src read not { stop } if 0 ne { stop } if % non-interlaced src readdwordbe not { stop } if pop % CRC imgdict /ImageMatrix [ imgdict /Width get 0 0 imgdict /Height get 0 0 ] put % read palette and set color space src (PLTE) findpngchunk not { stop } if dup 3 mod 0 ne { stop } if 3 idiv /numcolors exch def 1 imgdict /BitsPerComponent get bitshift dup 1 sub imgdict /Decode get 1 vokput numcolors lt { stop } if src numcolors 3 mul string readstring not { stop } if [ /Indexed /DeviceRGB numcolors 1 sub 5 -1 roll ] setcolorspace src readdwordbe not { stop } if pop % CRC % search image data body src (IDAT) findpngchunk not { stop } if /datarest exch def % procedure to read data [ currentdict { begin datarest 0 eq { src readdwordbe not { /pngfilter /ioerror err } if pop % CRC src (IDAT) findpngchunk { /datarest exch def } if } if datarest 0 eq { () } { src buf 0 datarest 256 minimum getinterval readstring not { pop /pngfilter /ioerror err } if dup length datarest exch sub /datarest exch def } ifelse end } /exec cvx ] cvx % create FlateDecode filter << /Predictor 10 /Colors 1 /Columns imgdict /Width get /BitsPerComponent imgdict /BitsPerComponent get >> /FlateDecode filter imgdict /DataSource vokput } stopped { $error /newerror get { stop } { /preparepng /ioerror err } ifelse } { imgdict } ifelse end } def
err・vokput・readbytesbeはpreparejpgeと共通。 PNGでは4バイトの数値が多いため、readwordbeはreaddwordbe(word → dword)になっている。 findpngchunkは次のPNGチャンクを探す。 minimumは最小値を返す。
そしてpreparepngが本体。 パレットを読む必要があるのと、データ本体を抜き出してつなげなければならないのでちょっと長い。 このため、FlateDecodeフィルタにはプロシージャを渡している。 このプロシージャに状態変数を持たせるために、ローカルな辞書を作り、配列の中に置いて、配列をプロシージャに変換、ということをやっている。 この辺の事情はファイルとフィルタに書いた。 効率を考えると読み込みバッファはこのプロシージャの中で確保するのではなく、あらかじめ確保しておいた方がよい(PLRM 3.7.4)。 このバッファも辞書の中に放り込んでいる。 この例では256バイトずつ読んでいるが、最後だけは半端になる可能性があるため、getintervalで長さを調節する、ということをやっている。 データがなくなり、後続のIDATもない場合はデータ終了なので空の文字列を返す。
最後にFlateDecodeフィルタを作って、画像辞書のDataSourceへ突っ込んで終わり。 FlateDecodeも辞書がいるが、これはこちらで保持しておく必要はない(おそらくFlateDecodeが参照している間は消滅しないはずである)。 Predictorを10〜15にするとPNGになる。 エンコーダではそれぞれの数値に意味があるのだが、デコーダではこのうちのどれを指定しても大丈夫である。
使い方はpreparejpegと同じなので省略。 一応、1・2・4・8bppのインデックスカラーのPNGファイルが読めるはず。 Cygwinの場合は画像データが置いてあるディレクトリをバイナリマウントしておくのを忘れないように。
Copyright (C) 2012 You SUZUKI
$Id: ps-png.htm,v 1.1 2012/11/03 07:52:28 you Exp $