using System.IO.Compression; using System.Text; namespace Engine.Render; /// /// Minimal PNG encoder: 8-bit RGBA, uncompressed-filter scanlines (filter /// type 0, "None"), one IDAT chunk. No external dependency — /// SixLabors.ImageSharp was considered and rejected: its license isn't /// MIT or Apache (it's revenue-gated), and pulling that into an MIT /// engine's own screenshot tooling would be exactly the kind of surprise /// a downstream user shouldn't have to discover later. (BCL, since .NET 6) does the one genuinely hard /// part — a correctly zlib-wrapped DEFLATE stream — so what's left here /// (chunk framing, CRC32) is small enough to get right and to verify by /// actually opening a file this writes. /// internal static class PngWriter { private static readonly byte[] Signature = [137, 80, 78, 71, 13, 10, 26, 10]; private static readonly uint[] Crc32Table = BuildCrc32Table(); /// /// is * /// * 4 bytes, row 0 first (top of the /// image). OpenGL's ReadPixels returns rows bottom-first — flip /// before calling this, not after; this writer has no opinion about /// where the bytes came from. /// public static void Write(string path, int width, int height, ReadOnlySpan topDownRgba) { using var file = File.Create(path); file.Write(Signature); WriteChunk(file, "IHDR", BuildIhdr(width, height)); WriteChunk(file, "IDAT", BuildIdat(width, height, topDownRgba)); WriteChunk(file, "IEND", []); } private static byte[] BuildIhdr(int width, int height) { var ihdr = new byte[13]; WriteUInt32BE(ihdr, 0, (uint)width); WriteUInt32BE(ihdr, 4, (uint)height); ihdr[8] = 8; // bit depth ihdr[9] = 6; // color type: RGBA ihdr[10] = 0; // compression method: deflate (the only one PNG defines) ihdr[11] = 0; // filter method: adaptive (per-scanline filter byte) ihdr[12] = 0; // interlace method: none return ihdr; } private static byte[] BuildIdat(int width, int height, ReadOnlySpan rgba) { var stride = width * 4; using var compressed = new MemoryStream(); using (var zlib = new ZLibStream(compressed, CompressionLevel.Optimal, leaveOpen: true)) { var filterByte = new byte[1]; // 0 = "None" — every scanline, unfiltered for (var y = 0; y < height; y++) { zlib.Write(filterByte); zlib.Write(rgba.Slice(y * stride, stride)); } } return compressed.ToArray(); } private static void WriteChunk(Stream stream, string type, byte[] data) { var typeBytes = Encoding.ASCII.GetBytes(type); Span length = stackalloc byte[4]; WriteUInt32BE(length, 0, (uint)data.Length); stream.Write(length); stream.Write(typeBytes); stream.Write(data); Span crc = stackalloc byte[4]; WriteUInt32BE(crc, 0, Crc32(typeBytes, data)); stream.Write(crc); } private static void WriteUInt32BE(Span buffer, int offset, uint value) { buffer[offset] = (byte)(value >> 24); buffer[offset + 1] = (byte)(value >> 16); buffer[offset + 2] = (byte)(value >> 8); buffer[offset + 3] = (byte)value; } private static uint[] BuildCrc32Table() { var table = new uint[256]; for (uint n = 0; n < 256; n++) { var c = n; for (var k = 0; k < 8; k++) c = (c & 1) != 0 ? 0xEDB88320 ^ (c >> 1) : c >> 1; table[n] = c; } return table; } private static uint Crc32(byte[] type, byte[] data) { var crc = 0xFFFFFFFFu; foreach (var b in type) crc = Crc32Table[(crc ^ b) & 0xFF] ^ (crc >> 8); foreach (var b in data) crc = Crc32Table[(crc ^ b) & 0xFF] ^ (crc >> 8); return crc ^ 0xFFFFFFFFu; } }