using System.Buffers.Binary; using System.IO.Compression; using System.Text; using ICSharpCode.SharpZipLib.Checksum; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; namespace XFESystem.Browser.Tests; public sealed class BrowserPngDecoderTests { public static IEnumerable Formats() { foreach (var color in new[] { PngColorType.Grayscale, PngColorType.Palette, PngColorType.Rgb, PngColorType.GrayscaleWithAlpha, PngColorType.RgbWithAlpha }) foreach (var depth in new[] { PngBitDepth.Bit1, PngBitDepth.Bit2, PngBitDepth.Bit4, PngBitDepth.Bit8, PngBitDepth.Bit16 }) if ((color is PngColorType.Grayscale or PngColorType.Palette || depth is PngBitDepth.Bit8 or PngBitDepth.Bit16) && !(color == PngColorType.Palette && depth == PngBitDepth.Bit16)) foreach (bool interlace in new[] { false, true }) yield return [color, depth, interlace]; } [Theory] [MemberData(nameof(Formats))] public void ManagedPngMatchesEncodedPixelsAcrossDepthsTransparencyAndAdam7(PngColorType color, PngBitDepth depth, bool interlace) { using var original = new Image(13, 11); for (int y = 0; y < original.Height; y++) for (int x = 0; x < original.Width; x++) original[x, y] = new((byte)(x % 4 * 85), (byte)(y % 4 * 85), (byte)((x + y) % 4 * 85), (byte)(x % 4 == 0 ? 0 : 255)); using var encoded = new MemoryStream(); original.SaveAsPng(encoded, new PngEncoder { ColorType = color, BitDepth = depth, InterlaceMethod = interlace ? PngInterlaceMode.Adam7 : PngInterlaceMode.None }); byte[] bytes = encoded.ToArray(); var actual = BrowserImageDecoder.Decode(bytes, 143); using var reference = Image.Load(bytes); for (int y = 0; y < original.Height; y++) for (int x = 0; x < original.Width; x++) { var expected = reference[x, y]; uint value = actual.Pixel(x, y); Assert.Equal(expected.A, (byte)(value >> 24)); Assert.InRange(Math.Abs(expected.R - (byte)(value >> 16)), 0, 1); Assert.InRange(Math.Abs(expected.G - (byte)(value >> 8)), 0, 1); Assert.InRange(Math.Abs(expected.B - (byte)value), 0, 1); } } [Fact] public void CrcTruncationAndCompressedOutputBeyondDeclaredRowsAreRejected() { byte[] image = MakePng(new byte[] { 0, 255, 0, 0, 255 }); Assert.Equal(0xffff0000u, BrowserImageDecoder.Decode(image, 1).Pixel(0, 0)); byte[] changed = (byte[])image.Clone(); changed[changed.Length - 1] ^= 1; Assert.Throws(() => BrowserImageDecoder.Decode(changed, 1)); Assert.Throws(() => BrowserImageDecoder.Decode(image[..^3], 1)); Assert.Throws(() => BrowserImageDecoder.Decode(MakePng(new byte[10000]), 1)); } private static byte[] MakePng(byte[] raw) { using var output = new MemoryStream(); output.Write(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }); void Chunk(string kind, byte[] bytes) { var type = Encoding.ASCII.GetBytes(kind); var crc = new Crc32(); crc.Update(type); crc.Update(bytes); byte[] number = new byte[4]; BinaryPrimitives.WriteInt32BigEndian(number, bytes.Length); output.Write(number); output.Write(type); output.Write(bytes); BinaryPrimitives.WriteUInt32BigEndian(number, (uint)crc.Value); output.Write(number); } byte[] header = new byte[13]; BinaryPrimitives.WriteInt32BigEndian(header, 1); BinaryPrimitives.WriteInt32BigEndian(header.AsSpan(4), 1); header[8] = 8; header[9] = 6; Chunk("IHDR", header); using var compressed = new MemoryStream(); using (var zlib = new ZLibStream(compressed, CompressionLevel.SmallestSize, true)) zlib.Write(raw); Chunk("IDAT", compressed.ToArray()); Chunk("IEND", []); return output.ToArray(); } }