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();
*/


コメント