WPFヘルパー:SimplePngDecoder.cs – 簡易PNGデコーダー

コンピュータ

C#で簡易的なPNGデコーダを実装してみました。
zlibによる圧縮データの展開や、フィルタ処理まで最低限対応しています。
実験的な性質が強いため、実用には難があると予想されますが、
比較的短めのコード量で全体の流れを把握できる内容になっています。

SimplePngDecoder.cs

Download
using System.IO;
using System.IO.Compression;
using ImageBuffer = Maywork.WPF.Helpers.ImageBufferHelper.ImageBuffer;

namespace Maywork.WPF.Helpers;


public static class SimplePngDecoder
{
    public static ImageBuffer Load(string path)
    {
        using var fs = File.OpenRead(path);
        using var br = new BinaryReader(fs);

        // =========================
        // シグネチャ
        // =========================
        var sig = br.ReadBytes(8);
        if (sig[0] != 0x89 || sig[1] != 0x50)
            throw new Exception("Not PNG");

        int width = 0;
        int height = 0;
        int bpp = 3;
        //bool hasAlpha = false;

        var idat = new MemoryStream();

        // =========================
        // チャンクループ
        // =========================
        while (true)
        {
            int length = ReadInt32BE(br);
            string type = new string(br.ReadChars(4));
            byte[] data = br.ReadBytes(length);
            br.ReadUInt32(); // CRCスキップ

            if (type == "IHDR")
            {
                width = ReadInt32BE(data, 0);
                height = ReadInt32BE(data, 4);

                byte bitDepth = data[8];
                byte colorType = data[9];


                bpp = colorType switch
                {
                    // Gray
                    0 => 1,
                    // RGB
                    2 => 3,
                    // Gray + Alpha
                    4 => 2,
                    // RGBA
                    6 => 4,
                    _ => throw new NotSupportedException($"Unsupported colorType:{colorType}"),
                };
                // if (colorType == 6)
                //    hasAlpha = true;
            }
            else if (type == "IDAT")
            {
                idat.Write(data, 0, data.Length);
            }
            else if (type == "IEND")
            {
                break;
            }
        }

        // =========================
        // zlib解凍
        // =========================
        idat.Position = 0;
        using var z = new ZLibStream(idat, CompressionMode.Decompress);
        using var raw = new MemoryStream();
        z.CopyTo(raw);

        byte[] decompressed = raw.ToArray();

        // =========================
        // フィルタ解除
        // =========================
        int stride = width * bpp;
        var pixels = new byte[height * stride];

        int srcIndex = 0;
        int dstIndex = 0;

        for (int y = 0; y < height; y++)
        {
            byte filter = decompressed[srcIndex++];

            switch (filter)
            {
                case 0: // None
                    Array.Copy(decompressed, srcIndex, pixels, dstIndex, stride);
                    break;

                case 1: // Sub
                    for (int x = 0; x < stride; x++)
                    {
                        byte left = x >= bpp ? pixels[dstIndex + x - bpp] : (byte)0;
                        pixels[dstIndex + x] = (byte)(decompressed[srcIndex + x] + left);
                    }
                    break;

                case 2: // Up
                    for (int x = 0; x < stride; x++)
                    {
                        byte up = y > 0 ? pixels[dstIndex + x - stride] : (byte)0;
                        pixels[dstIndex + x] = (byte)(decompressed[srcIndex + x] + up);
                    }
                    break;
                case 3: // Average
                    for (int x = 0; x < stride; x++)
                    {
                        byte left = x >= bpp ? pixels[dstIndex + x - bpp] : (byte)0;
                        byte up = y > 0 ? pixels[dstIndex + x - stride] : (byte)0;

                        byte avg = (byte)((left + up) / 2);

                        pixels[dstIndex + x] = (byte)(decompressed[srcIndex + x] + avg);
                    }
                    break;
                case 4: // Paeth
                    for (int x = 0; x < stride; x++)
                    {
                        byte left = x >= bpp ? pixels[dstIndex + x - bpp] : (byte)0;
                        byte up = y > 0 ? pixels[dstIndex + x - stride] : (byte)0;
                        byte upLeft = (x >= bpp && y > 0) ? pixels[dstIndex + x - stride - bpp] : (byte)0;

                        byte predict = Paeth(left, up, upLeft);

                        pixels[dstIndex + x] = (byte)(decompressed[srcIndex + x] + predict);
                    }
                    break;
                default:
                    throw new NotSupportedException($"Filter {filter}");
            }

            srcIndex += stride;
            dstIndex += stride;
        }

        // PNGのRGB(A) -> ImageBufferのBGR(A/BGRA)
        byte[] finalPixels = ConvertToImageBufferFormat(pixels, width, height, bpp);
        int finalBpp = bpp switch
        {
            1 => 1,
            2 => 1, // ←ここがポイント(Gray+Alpha → Gray)
            3 => 3,
            4 => 4,
            _ => throw new Exception()
        };

        int finalStride = width * finalBpp;
        return new ImageBuffer(width, height, finalStride, finalBpp, finalPixels);
    }

    // =========================
    // ヘルパ
    // =========================
    static int ReadInt32BE(BinaryReader br)
    {
        var b = br.ReadBytes(4);
        return (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3];
    }

    static int ReadInt32BE(byte[] b, int offset)
    {
        return (b[offset] << 24) | (b[offset + 1] << 16) | (b[offset + 2] << 8) | b[offset + 3];
    }
    // RGB→BGR 変換
    static byte[] ConvertToImageBufferFormat(byte[] pixels, int width, int height, int bpp)
    {
        int pixelCount = width * height;

        // =========================
        // Gray → そのまま
        // =========================
        if (bpp == 1)
        {
            return pixels;
        }

        // =========================
        // Gray + Alpha → Gray(α破棄)
        // =========================
        if (bpp == 2)
        {
            var dst = new byte[pixelCount];

            int si = 0;
            int di = 0;

            for (int i = 0; i < pixelCount; i++)
            {
                byte g = pixels[si++];
                si++; // α捨てる

                dst[di++] = g;
            }

            return dst;
        }

        // =========================
        // RGB → BGR
        // =========================
        if (bpp == 3)
        {
            for (int i = 0; i < pixels.Length; i += 3)
            {
                byte r = pixels[i + 0];
                byte g = pixels[i + 1];
                byte b = pixels[i + 2];

                pixels[i + 0] = b;
                pixels[i + 1] = g;
                pixels[i + 2] = r;
            }

            return pixels;
        }

        // =========================
        // RGBA → BGRA
        // =========================
        if (bpp == 4)
        {
            for (int i = 0; i < pixels.Length; i += 4)
            {
                byte r = pixels[i + 0];
                byte g = pixels[i + 1];
                byte b = pixels[i + 2];
                byte a = pixels[i + 3];

                pixels[i + 0] = b;
                pixels[i + 1] = g;
                pixels[i + 2] = r;
                pixels[i + 3] = a;
            }

            return pixels;
        }

        throw new NotSupportedException();
    }
    static byte Paeth(byte a, byte b, byte c)
    {
        int p = a + b - c;
        int pa = Math.Abs(p - a);
        int pb = Math.Abs(p - b);
        int pc = Math.Abs(p - c);

        if (pa <= pb && pa <= pc) return a;
        if (pb <= pc) return b;
        return c;
    }
}
/*
// 使い方
var img = SimplePngDecoder.Load("input.png");
MyImage.Source = buff.ToBitmapSource();
*/

ImageBufferHelper.cs
ImageConverter.cs

コメント