PNGの規格を簡単に説明する

PNGの規格を勉強する機会があったので、その内容を簡単に説明します*1

PNGはいくつかの"チャンク"が集まって構成されています。例えば、IHDRチャンクやIDATチャンク、PLTEチャンクなどがあります。では、PNGファイルという単なるバイト列から、どのようにチャンクを抽出すれば良いのでしょうか?

これは、PNGファイルの構造を知ることでわかります。

PNGファイルの構造

PNGファイルの構造は、以下のようになっています:

  • ファイル先頭の8byteは 必ず 137, 80, 78, 71, 13, 10, 26, 10である
    • PNGファイルであることを識別するため
  • それ以降のバイト列は、次の構造の繰り返しである
    • 先頭の4byteは、チャンクのサイズを表す(Length)
    • 次の4byteは、チャンクの種類を表す(Chunk Type)
      • 例えばIHDRとかIDATとかPLTEになったりする
    • 次のnbyteは、チャンクのデータを表す(Chunk Data)
      • nはLengthの値
    • 次の4byteは、CRCを表す(CRC)
      • いわゆる誤り検出のためのデータ

この構造に従ってPNGファイルをパースすると、複数のチャンクを取りだすことができます。
あとはこれらのチャンクを画像として解釈すればいっちょう上がりというわけです。

重要なチャンク

画像データを取得するために必須のチャンクについて解説します。

IHDRチャンク

まず、チャンクの中から、IHDRチャンクを探してきましょう。こいつはいわゆるヘッダーです。
Width, Height, Color Typeの3つのデータが含まれています*2

このColor Typeというのが何を表しているのかというと、

  • "パレット"を使っているか
  • 白黒画像なのか、カラー画像なのか
  • アルファチャンネルが使用されているか(透過画像かどうか)

という3つの情報を表しています。

PLTEチャンク

Color Typeを見て、"パレット"が使われているというのであれば、必ずPLTEチャンクが存在します。

このPLTEチャンクというのは、「画像内で使われ得るすべての色」の情報を持っています。 例えば、赤一色の画像であればrgb = [255,0,0]という値だけを持ちますし、赤と白の二色だけの画像であればrgb = [255, 0, 0]に加えてrgb = [255,255,255]という値も持ちます。また、アルファチャンネルが使用されている場合は、RGBの三色に加えてもう一つ「透過度」を表すbyteが存在します。例えば、「赤色で全く透明ではない色」は[255, 0, 0, 255]になるわけですね。

PLTEチャンクのバイト列から色情報を取得するのは簡単で、RGBなら3つづつ(RGB+αなら4つづつ)先頭のバイトからグループ化していけば良いです。


なんでこんなものが存在するのかというと、その方が画像のサイズを落とせるからです。例えば、3色しか使われない画像を圧縮するとき、すべてのピクセルごとに色情報を持つのは容量の無駄です。
それよりも、その3色にそれぞれ番号を振っておいて、「このピクセルはn番の色、このピクセルはm番の色、このピクセルは...」というようにデータを持っておくほうが、使う色が少ない場合は容量が少なくて済みます。

IDATチャンク

ここが画像データの本体です。IDATチャンクには、実際の画像データが格納されています*3

IDATチャンクに格納されている画像データはDeflate圧縮されています。そのため、IDATチャンクから画像データを取得するためには展開する必要があります*4。展開されたデータは、👇のように、左上から右下に向かって1ピクセルづつ色の情報が並んでいます*5

f:id:threetea0407:20171022184226p:plain

画像データを取得する

ここまでのチャンクを解釈して画像データを取得するわけですが、結構複雑です。

"パレット"を使用する場合

この場合は簡単で、画像データを1byteづつ読んで、その値を「パレットのインデックス」として解釈します。

例えば、PLTEのデータがRGBs = [[255, 0, 0], [0, 255, 0], [0, 0, 255]]であり、IDATのデータがpixels = [0, 1, 2, 0, 1, 2]であれば、画像の色データはcolors = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 0, 0], [0, 255, 0], [0, 0, 255]]になるということです*6

"パレット"を使用しない場合

この場合は、IDATのデータを直接解釈します。ここが結構複雑です。

まず、IDATのバイト列をHeightの数に分割します。例えば、Heightが100pixelの画像であれば、100個の等しい長さのバイト列に分割するわけです。
そして、ここがややこしいのですが、各バイト列の先頭1byteはfilter methodを表しています。従って、色情報そのものは2byte目から始まるわけです。

filter methodRFCにおいて定義されておりNone, Sub, Up, Average, Paethの5種類があるのですが、一番単純なNoneについてはPLTEチャンクを解釈したときと全く同様に解釈すれば良いです。つまり、2バイト目から3byteづつ(αチャンネルがあるときは4byteづつ)グループにしてしまえば、それぞれのグループが1つのピクセルを表すことになります。簡単ですね。

ところで、filter methodは各行ごとに設定されています。従って、各行ごとに違うfilter methodを使うということができます。
実際に、Portable Network Graphics - Wikipediaにデモ画像として掲載されているサイコロの画像(👇)は、行ごとに異なるfilter methodが設定されているようです。

まとめ

PNGファイルの規格の説明は以上です。
あとはプログラムを書けば、実際のPNGファイルを解釈することができます。

*1:PNGの規格自体はRFC公開されている

*2:本当はもっとある

*3: IDATチャンクは1つのファイルに2つ以上含まれることがあります。これは、チャンクの長さに制限がある一方で、画像データのサイズには制限がないからです。

*4:Deflateのアルゴリズムに関してはこの記事の解説すべき範囲を外れるので特に解説しないが、実際にはDeflateを取り扱うライブラリが多くの言語で用意されているのでそれに突っ込めば良い。

*5:インターレースとかが出てくると例外はあります

*6:従って、IDATに含まれる各byteの値は、PLTEのデータの長さを超えることはありません。