WPFヘルパー:SimplePngEncoder.cs – 簡易PNGエンコーダー

コンピュータ

ImageBufferオブジェクトをPNG形式で保存するエンコーダーです。

SimplePngEncoder.cs

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

namespace Maywork.WPF.Helpers;

public static class SimplePngEncoder
{
    public static void Save(this ImageBuffer src, string path)
    {
        using var fs = File.Create(path);
        Save(src, fs);
    }

    public static void Save(this ImageBuffer src, Stream stream)
    {
        if (src.Width <= 0 || src.Height <= 0)
            throw new ArgumentException("Invalid image size.");

        if (src.Channels is not (1 or 3 or 4))
            throw new NotSupportedException($"Unsupported channels:{src.Channels}");

        using var bw = new BinaryWriter(stream, Encoding.ASCII, leaveOpen: true);

        // =========================
        // PNGシグネチャ
        // =========================
        bw.Write(new byte[]
        {
            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
        });

        // =========================
        // IHDR
        // =========================
        byte colorType = src.Channels switch
        {
            1 => (byte)0, // Gray
            3 => (byte)2, // RGB
            4 => (byte)6, // RGBA
            _ => throw new NotSupportedException()
        };

        byte[] ihdr = new byte[13];
        WriteInt32BE(ihdr, 0, src.Width);
        WriteInt32BE(ihdr, 4, src.Height);
        ihdr[8] = 8;          // bit depth
        ihdr[9] = colorType;  // color type
        ihdr[10] = 0;         // compression method
        ihdr[11] = 0;         // filter method
        ihdr[12] = 0;         // interlace method

        WriteChunk(bw, "IHDR", ihdr);

        // =========================
        // 画像データ作成
        // 各行の先頭に FilterType=0 を付ける
        // =========================
        byte[] rawImage = BuildRawImage(src);

        // =========================
        // zlib圧縮
        // =========================
        byte[] compressed;
        using (var ms = new MemoryStream())
        {
            using (var z = new ZLibStream(ms, CompressionLevel.Optimal, leaveOpen: true))
            {
                z.Write(rawImage, 0, rawImage.Length);
            }
            compressed = ms.ToArray();
        }

        WriteChunk(bw, "IDAT", compressed);

        // =========================
        // IEND
        // =========================
        WriteChunk(bw, "IEND", Array.Empty<byte>());
    }

    static byte[] BuildRawImage(ImageBuffer src)
    {
        int width = src.Width;
        int height = src.Height;
        int ch = src.Channels;

        int srcStride = src.Stride;
        int dstStride = 1 + (width * ch); // filter byte + row pixels
        byte[] dst = new byte[height * dstStride];

        int di = 0;

        for (int y = 0; y < height; y++)
        {
            // Filter Type = 0 (None)
            dst[di++] = 0;

            int si = y * srcStride;

            if (ch == 1)
            {
                // Gray はそのまま
                Buffer.BlockCopy(src.Pixels, si, dst, di, width);
                di += width;
            }
            else if (ch == 3)
            {
                // ImageBuffer: BGR -> PNG: RGB
                for (int x = 0; x < width; x++)
                {
                    byte b = src.Pixels[si++];
                    byte g = src.Pixels[si++];
                    byte r = src.Pixels[si++];

                    dst[di++] = r;
                    dst[di++] = g;
                    dst[di++] = b;
                }
            }
            else if (ch == 4)
            {
                // ImageBuffer: BGRA -> PNG: RGBA
                for (int x = 0; x < width; x++)
                {
                    byte b = src.Pixels[si++];
                    byte g = src.Pixels[si++];
                    byte r = src.Pixels[si++];
                    byte a = src.Pixels[si++];

                    dst[di++] = r;
                    dst[di++] = g;
                    dst[di++] = b;
                    dst[di++] = a;
                }
            }
            else
            {
                throw new NotSupportedException($"Unsupported channels:{ch}");
            }
        }

        return dst;
    }

    static void WriteChunk(BinaryWriter bw, string type, byte[] data)
    {
        byte[] typeBytes = Encoding.ASCII.GetBytes(type);
        if (typeBytes.Length != 4)
            throw new ArgumentException("Chunk type must be 4 chars.", nameof(type));

        WriteInt32BE(bw, data.Length);
        bw.Write(typeBytes);
        bw.Write(data);

        uint crc = Crc32(typeBytes, data);
        WriteUInt32BE(bw, crc);
    }

    static void WriteInt32BE(BinaryWriter bw, int value)
    {
        bw.Write(new byte[]
        {
            (byte)((value >> 24) & 0xFF),
            (byte)((value >> 16) & 0xFF),
            (byte)((value >> 8) & 0xFF),
            (byte)(value & 0xFF),
        });
    }

    static void WriteUInt32BE(BinaryWriter bw, uint value)
    {
        bw.Write(new byte[]
        {
            (byte)((value >> 24) & 0xFF),
            (byte)((value >> 16) & 0xFF),
            (byte)((value >> 8) & 0xFF),
            (byte)(value & 0xFF),
        });
    }

    static void WriteInt32BE(byte[] buffer, int offset, int value)
    {
        buffer[offset + 0] = (byte)((value >> 24) & 0xFF);
        buffer[offset + 1] = (byte)((value >> 16) & 0xFF);
        buffer[offset + 2] = (byte)((value >> 8) & 0xFF);
        buffer[offset + 3] = (byte)(value & 0xFF);
    }

    static uint Crc32(byte[] typeBytes, byte[] data)
    {
        uint crc = 0xFFFFFFFF;

        for (int i = 0; i < typeBytes.Length; i++)
            crc = UpdateCrc32(crc, typeBytes[i]);

        for (int i = 0; i < data.Length; i++)
            crc = UpdateCrc32(crc, data[i]);

        return ~crc;
    }

    static uint UpdateCrc32(uint crc, byte b)
    {
        crc ^= b;
        for (int k = 0; k < 8; k++)
        {
            if ((crc & 1) != 0)
                crc = (crc >> 1) ^ 0xEDB88320u;
            else
                crc >>= 1;
        }
        return crc;
    }
}
/*
// 使い方
var img = SimplePngDecoder.Load("input.png");
SimplePngEncoder.Save("output.png", img);
*/
Download

ImageBufferHelper.cs
ImageConverter.cs

コメント