🖼

【C#】PNG画像サイズの取得方法

2022/03/18に公開

C#で画像サイズに合わせて処理を切り替える必要があり、
Rustではメタデータから見れば取れそうだけど
C#はどう取るんだろう?と疑問に思い調べてみた内容です。

※使用ランタイムは「.NET 6.0」
※画像は「.png」です

どうもIHDRチャンクとやらの情報を取得すれば
高さと幅がわかるそうです。
サンプルソースもあったので
自分流に書き換えて実装してみます。

static void GetImageSize(string filePath, ref int width, ref int height)
{

    uint w, h;

    using(FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    {
        fs.Seek(16, SeekOrigin.Begin);
        byte[] buf = new byte[8];
        fs.Read(buf, 0, 8);

        w = (((uint)buf[0] << 24) | ((uint)buf[1] << 16) | ((uint)buf[2] << 8) | ((uint)buf[3]));
        h = (((uint)buf[4] << 24) | ((uint)buf[5] << 16) | ((uint)buf[6] << 8) | ((uint)buf[7]));

    }

    width = (int)w;
    height = (int)h;

}

いやいやSeekとか使ってるしよくわからんが?

ですよね!!!!!
自分もそう思いました!!!!!

いやソースさえ分かればいいし詳しくはいいやという方はここまで、
詳しく見たい方はもう少しお付き合いください。

※以下正しい知識ではない部分が含まれているかと思いますが
 イメージとしてはこんな感じ!
 という感じで書いております。

FileStreamで画像データを読み込んだとき
以下の画像のような感じで読み込まれています。
(あくまでイメージです)

禁断症状が出そうな画像ですね。

PNGファイルは最初の8byteは必ず同じ値が入っています。
これは「このデータはPNGファイルやで!」と教えてくれているイメージです。
以下の画像の青い部分です。

その次ぐらいのところから
「このデータはこんな感じのデータやで」というように
データに関する情報が記入されています。
これをチャンクと呼ぶそうです。
PNGファイルには
「IHDR」「IDAT」「IEND」というチャンクが
必ず記入されています。

そのうち「IHDR」の部分に
ファイルの幅と高さに関する情報が書かれているわけです。

ちなみに以下の画像では青い部分から「IHDR」が書かれています。
「ここからIHDRじゃよ」という感じですね。

さて、そうすると
最初の16byte分はどうも関係のない情報が書かれているっぽいですね。
ここは読み込まなくていいので飛ばしてしまいましょうというのが
以下のコードの部分になります。

fs.Seek(16, SeekOrigin.Begin);

データの頭のところから16byte目のところに
読み込み開始地点を移動させているイメージです。

高さと幅に関する情報は
この16byteの後すぐの4byteずつに書かれています。

最初の4byteに幅、次の4byteに高さが書かれています。
つまるところ次の8byte分に高さと幅があるわけです。
それを読み込む処理が以下の部分です。

byte[] buf = new byte[8];
fs.Read(buf, 0, 8);

これは16進数で書かれています。
幅の部分は「00 00 00 10」となっているので、
10進数に直すと

buf[0] = 0
buf[1] = 0
buf[2] = 0
buf[3] = 16

が入っていることになります。

また、各byteごとにシフト演算が必要です。
buf[0]buf[2] には0が入っているのでどれだけ左シフトをしても0です。
buf[3]はシフト演算なしてそのまま使うので
0 + 0 + 0 + 16で
16ピクセルということがわかりました。
コードでは以下の部分

w = (((uint)buf[0] << 24) | ((uint)buf[1] << 16) | ((uint)buf[2] << 8) | ((uint)buf[3]));

さて高さの部分は「00 00 01 50」で10進数にすると

buf[4] = 0
buf[5] = 0
buf[6] = 1
buf[7] = 80

が入っています。
buf[6]は8回左シフトした10進数になるので
「256」が入ることになります。
なので
0 + 0 + 256 + 80で
336ピクセルということがわかります。
これもコードでは以下の部分です。

h = (((uint)buf[4] << 24) | ((uint)buf[5] << 16) | ((uint)buf[6] << 8) | ((uint)buf[7]));

これで画像の幅と高さが取れました!


ところでシフト演算って何?

buf[6]の部分を例に上げてみましょう。

buf[6]には1が入っていました。
これは2進数にしても1ですね?

そして「左シフト」というのは
この2進数の状態で桁を繰り上げてあげることです。
繰り上げて空いたところには0を入れます。

つまり「1」を1回左シフトすると「10」になります。
これは10進数でいうところの「2」です。

これを8回やってください!
というのがbuf[6] << 8です。

1を8回左シフトすると
「1」→「1 0000 0000」になります。
「1 0000 0000」は10進数では「256」です。
(本当にそうなるか気になる方は
 PCの電卓を開いてプログラマー電卓モードで
 1を入力した後8回0を押してみてください)

逆に右シフトは桁を繰り下げる処理です。

調べていて知恵熱が出る内容でした……。
絶対便利なメソッドとかあるでしょ……。

Discussion