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<object[]> 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<Rgba32>(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<Rgba32>(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<IOException>(() => BrowserImageDecoder.Decode(changed, 1));
Assert.Throws<IOException>(() => BrowserImageDecoder.Decode(image[..^3], 1));
Assert.Throws<BrowserLimitException>(() => 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();
}
}
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<object[]> 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<Rgba32>(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<Rgba32>(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<IOException>(() => BrowserImageDecoder.Decode(changed, 1));
Assert.Throws<IOException>(() => BrowserImageDecoder.Decode(image[..^3], 1));
Assert.Throws<BrowserLimitException>(() => 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();
}
}