home / uni / ps / png PostScript言語 / PNG画像

 PostScript自体はどんな形式でもデコードできるはずだが、ここではとりえあず256色以下のインデックスカラーを扱う。 基本はRFC2083なので無料で取得できる。 最新はISO/IEC15948だが、W3C側が無料で公開しているようだ。 この辺から取得できる。 PNGはFlateDecodeフィルタでデコードできるのだが、FlateDecodeにはデータ本体のみを渡さなければならない。 この部分を自分で作る必要がある。

home / uni / ps / png PNGファイルフォーマット

 まず先頭に「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 0x0A8シグネチャ(0x80+タブ、PNG、CRLF、Ctrl+Z、LF)
chunklen4IHDRチャンク
"IHDR"4
Width4画像の幅
Height4画像の高さ
Bit depth1各成分1サンプルあたりのビット数
Colour type1画像のタイプ、インデックスカラーは3
Compression method1圧縮方式、現在はdeflate/inflateのみなので0
Filter method1予測フィルタの方式、現在は0のみ
Interlace method1インターレース方式、0でインターレースなし
CRC4CRC
PLTE以外のチャンク(0個〜繰り返し)
chunklen4PLTEチャンク
"PLTE"4
Red(0)1赤成分の値
Green(0)1緑成分の値
Blue(0)1青成分の値
(サイズで決まる長さだけ繰り返し)
CRC4CRC
IDAT以外のチャンク(0個〜繰り返し)
chunklen4IDATチャンク
"IDAT"4
image datachunklenデータ本体
CRC4CRC
(1個〜繰り返し)

 チャンクの後ろに必ずCRCが入るので注意。 IHDRは13バイトになるのだが、将来拡張されることがあるかどうかまでの言及はないようだ。 画像の幅と高さは4バイトである。 パレットの実際の色数はPLTEチャンクのサイズで決まる。 PLTEチャンクのサイズが3の倍数ではない場合や、1サンプルあたりのビット数から決まる色数を超えたパレットデータがある場合はエラー扱い。 IDATチャンクに実際のデータが入るが、複数のIDATに分割できる。 FlateDecodeフィルタにはIDATチャンクの中身をつなげて渡す必要がある。

 あとどうでもいいことだけれど、colorのつづりがイギリス風。

home / uni / ps / png preparepng

 以上を元に適当に作ったのがこれ。

% 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

 errvokputreadbytesbepreparejpgeと共通。 PNGでは4バイトの数値が多いため、readwordbereaddwordbe(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 $