やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

PNGファイルの中身(バイナリー)を覗いてみる

みなさんはバイナリデータの中身を覗いたことがありますか。
僕は普段、WEBアプリケーションの開発に携わっているのですが、どうしてもこういった領域とは接点がありません。一度、WireSharkというアプリケーションを使って自身のPCから外部に送信されているパケット(バイナリデータ)の中身を覗いたことがあるぐらいです。

先日、システムコールを使ってTCPサーバーを実装したように、最近は触れたことのない領域にトライしています。
今回はPNGファイルを読み込んで、中身を覗いてみたいと思います。 具体的にはPNGファイルを指定バイトずつ読み込んで、どんな値が指定されているのかを確認していきます。
PNGファイルを選択したのはフォーマットが比較的シンプルで、たまたまデスクトップに.png形式のデータが転がっていたからです。

PNGファイルのフォーマットについて

まずはPNGファイルのフォーマットについて知ることから始めます。
ここでいうフォーマットとは先頭からNバイト分はAについての情報、次のYバイト分はBについての情報...といったようにISOなどの国政標準化機構によって定められたものです。
PNGはPortable Network Graphicsの略称であり、やはり国際基準に従っています。

Portable Network Graphics - Wikipedia

PNGファイルは次の順序で値が記録されています。
フォーマットについてはこちらのサイトを参考にさせて頂きました。

  • PNGファイルシグネチャ: 8バイト
  • IHDRチャンク(イメージヘッダー): 25バイト
  • 補助チャンク(必須ではない)
  • IDATチャンク(イメージデータ本体): 可変長
  • IENDチャンク(終端を示す): 12バイト

つまり、先頭から8バイト分だけ読み込めばPNGファイルのシグネチャを値が取得できます。
同様に次の25バイト分を読み込めばIHDRチャンク(イメージヘッダー)の値も取得できそうです。
読み込んだIHDRチャンク25バイト分のバイナリーには画像の幅・高さといった値がそれぞれ指定されています。
ただし、今回は補助チャンクは扱いません。
理由としては、補助チャンクまで対応するとデータの用意が面倒ですし、処理が複雑になるためです。

用意したデータ

適当に自身のプロフィール画像にしました。
詳細情報から確認できた情報は以下になります。

  • サイズ: 2090バイト
  • 大きさ: 64x64
  • 色空間: Gray
  • アルファチャンネル: はい

いざ覗いてみる

事前知識も得られたので、いざPNGファイルの中身を覗いていきます。
前回、音声ファイルをTCPサーバーから配信した際にNバイトずつ読み込んだのと全く同じ方法でいけました。
先ほどのフォーマットに従い、指定バイト分ずつバイナリーを読み込んでいきます。

func ReadBinary(file *os.File, x int) ([]byte, error) {
    data := make([]byte, x)
    if _, err := file.Read(data); err != nil {
        return nil, err
    }
    return data, nil
}

ただし、IHDRチャンクに関しては可変長のため、どれだけバイナリを読み込めば良いのか分かりません。
そのためファイルサイズから先頭のPNGファイルシグネチャ(8バイト)とIHDRチャンク(25バイト)、終端のIENDチャンク(12バイト)の差分の分だけ読み込むようにしました。

const (
    HEADER_SIZE = 8
    IHDR_SIZE = 25
    IEND_SIZE = 12
)

png, err := os.Open("./profile.png")
:
fileInfo, err := png.Stat()
:
fileSize := fileInfo.Size()
offset := fileSize - (HEADER_SIZE + IHDR_SIZE + IEND_SIZE)
idat, err := ReadBinary(png, int(offset))

断片的にコードを紹介しましたが、全体はGithubにて公開しています。
最終的に読み込んだバイナリーは特に使う予定はありませんが、構造体に変換しています。

github.com

読み込んだバイナリーの検証

PNGファイルをそれぞれのチャンクに分割して読み込んでみた所、以下のようになりました。
IDATチャンクの本体データに関しては文字数の都合上、省略しています。

--Signature---------
0x89,0x50,0x4e,0x47,0xd,0xa,0x1a,0xa

--Ihdr---------
Length: 0x0,0x0,0x0,0xd,
ChunkType: 0x49,0x48,0x44,0x52,
ChunkWidth: 0x0,0x0,0x0,0x40,
ChunkHeight: 0x0,0x0,0x0,0x40,
BitDepth: 0x8
ColorType: 0x4
BitDepth: 0x8
Compression: 0x0
Filter: 0x0
Interrace: 0x0
Crc: 0x0,0x60,0xb9,0x55

--Idat---------
Length: 0x0,0x0,0x7,0xf1,
ChunkType: 0x49,0x44,0x41,0x54,
ChunkData: [xxxxxxxxx]
Crc: 0xac,0x17,0xa7,0x92

--Iend---------
Length: 0x0,0x0,0x0,0x0,
ChunkType: 0x49,0x45,0x4e,0x44,
Crc: 0xae,0x42,0x60,0x82

まずsignatureがPNGであることを示す89 50 4E 47 0D 0A 1A 0Aと一致していることが確認できます。
残りのチャンクについては期待値が明確なものだけを選抜して確認しました。

チャンク 名称 結果 期待値
IHDR Length 0x0,0x0,0x0,0xd(13) 13(0xd)
ChunkType 0x49,0x48,0x44,0x52 49 48 44 52
Width 0x0,0x0,0x0,0x40(65) 64
Height 0x0,0x0,0x0,0x40(65) 64
ColorType 0x4 4(αチャンネル)
IDAT ChunkType 0x49,0x44,0x41,0x54 49 44 41 54
IEND ChunkType 0x49,0x45,0x4e,0x44 49 45 4E 44

画像サイズが1だけ異なるのが気になりますが、概ね期待通り、バイナリーを読み取ることができたようです。
今回は補助チャンクの対応を行っていないので、単に上から指定バイト分だけ読み取る操作を繰り返すだけでしたが、実際には補助チャンクのAがあるとBが...CがあるとDが...と処理は複雑になると思います。

最後に

過去にプロトコルを自作する書籍に取り組んだことがありますが、よく分かりませんでした。
先頭から4バイトにはプロトコルの種類を記録して...とバイトで値を表現するという知識が当時の自分にはありませんでした。 今回は簡単なPNGファイルを覗いて、実際にバイナリーで表現された値を確認することができました。
どのように設計するかは非常に難しそうですが、バイナリーさえ作成してしまえばTCPなりで送受して解析すれば良いだけなので、やれることは多そうです。
少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。

参考文献