diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs
new file mode 100644
index 0000000000..dae8f25e58
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs
@@ -0,0 +1,270 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+// Uncomment this to turn unit tests into benchmarks:
+// #define BENCHMARKING
+using System;
+
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.Formats.Png.Filters;
+using SixLabors.ImageSharp.Tests.Formats.Png.Utils;
+using SixLabors.ImageSharp.Tests.TestUtilities;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Png
+{
+ [Trait("Format", "Png")]
+ public partial class PngFilterTests : MeasureFixture
+ {
+#if BENCHMARKING
+ public const int Times = 1000000;
+#else
+ public const int Times = 1;
+#endif
+
+ public PngFilterTests(ITestOutputHelper output)
+ : base(output)
+ {
+ }
+
+ public const int Size = 64;
+
+ [Fact]
+ public void Average()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Average, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.DisableSIMD);
+ }
+
+ [Fact]
+ public void AverageSse2()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Average, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableSSSE3);
+ }
+
+ [Fact]
+ public void AverageSsse3()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Average, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2);
+ }
+
+ [Fact]
+ public void AverageAvx2()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Average, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.AllowAll);
+ }
+
+ [Fact]
+ public void Paeth()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Paeth, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.DisableSIMD);
+ }
+
+ [Fact]
+ public void PaethSimd()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Paeth, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.AllowAll);
+ }
+
+ [Fact]
+ public void Up()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Up, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.DisableSIMD);
+ }
+
+ [Fact]
+ public void UpSimd()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Up, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.AllowAll);
+ }
+
+ [Fact]
+ public void Sub()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Sub, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.DisableSIMD);
+ }
+
+ [Fact]
+ public void SubSimd()
+ {
+ static void RunTest()
+ {
+ var data = new TestData(PngFilterMethod.Sub, Size);
+ data.TestFilter();
+ }
+
+ FeatureTestRunner.RunWithHwIntrinsicsFeature(
+ RunTest,
+ HwIntrinsics.AllowAll);
+ }
+
+ public class TestData
+ {
+ private readonly PngFilterMethod filter;
+ private readonly int bpp;
+ private readonly byte[] previousScanline;
+ private readonly byte[] scanline;
+ private readonly byte[] expectedResult;
+ private readonly int expectedSum;
+ private readonly byte[] resultBuffer;
+
+ public TestData(PngFilterMethod filter, int size, int bpp = 4)
+ {
+ this.filter = filter;
+ this.bpp = bpp;
+ this.previousScanline = new byte[size * size * bpp];
+ this.scanline = new byte[size * size * bpp];
+ this.expectedResult = new byte[1 + (size * size * bpp)];
+ this.resultBuffer = new byte[1 + (size * size * bpp)];
+
+ var rng = new Random(12345678);
+ byte[] tmp = new byte[6];
+ for (int i = 0; i < this.previousScanline.Length; i += bpp)
+ {
+ rng.NextBytes(tmp);
+
+ this.previousScanline[i + 0] = tmp[0];
+ this.previousScanline[i + 1] = tmp[1];
+ this.previousScanline[i + 2] = tmp[2];
+ this.previousScanline[i + 3] = 255;
+
+ this.scanline[i + 0] = tmp[3];
+ this.scanline[i + 1] = tmp[4];
+ this.scanline[i + 2] = tmp[5];
+ this.scanline[i + 3] = 255;
+ }
+
+ switch (this.filter)
+ {
+ case PngFilterMethod.Sub:
+ ReferenceImplementations.EncodeSubFilter(
+ this.scanline, this.expectedResult, this.bpp, out this.expectedSum);
+ break;
+
+ case PngFilterMethod.Up:
+ ReferenceImplementations.EncodeUpFilter(
+ this.previousScanline, this.scanline, this.expectedResult, out this.expectedSum);
+ break;
+
+ case PngFilterMethod.Average:
+ ReferenceImplementations.EncodeAverageFilter(
+ this.previousScanline, this.scanline, this.expectedResult, this.bpp, out this.expectedSum);
+ break;
+
+ case PngFilterMethod.Paeth:
+ ReferenceImplementations.EncodePaethFilter(
+ this.previousScanline, this.scanline, this.expectedResult, this.bpp, out this.expectedSum);
+ break;
+
+ case PngFilterMethod.None:
+ case PngFilterMethod.Adaptive:
+ default:
+ throw new InvalidOperationException();
+ }
+ }
+
+ public void TestFilter()
+ {
+ int sum;
+ switch (this.filter)
+ {
+ case PngFilterMethod.Sub:
+ SubFilter.Encode(this.scanline, this.resultBuffer, this.bpp, out sum);
+ break;
+
+ case PngFilterMethod.Up:
+ UpFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, out sum);
+ break;
+
+ case PngFilterMethod.Average:
+ AverageFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, this.bpp, out sum);
+ break;
+
+ case PngFilterMethod.Paeth:
+ PaethFilter.Encode(this.previousScanline, this.scanline, this.resultBuffer, this.bpp, out sum);
+ break;
+
+ case PngFilterMethod.None:
+ case PngFilterMethod.Adaptive:
+ default:
+ throw new InvalidOperationException();
+ }
+
+ Assert.Equal(this.expectedSum, sum);
+ Assert.Equal(this.expectedResult, this.resultBuffer);
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs b/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs
new file mode 100644
index 0000000000..dd8ecc096d
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Png/ReferenceImplementations.cs
@@ -0,0 +1,229 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// ReSharper disable InconsistentNaming
+namespace SixLabors.ImageSharp.Tests.Formats.Png.Utils
+{
+ ///
+ /// This class contains reference implementations to produce verification data for unit tests
+ ///
+ internal static partial class ReferenceImplementations
+ {
+ ///
+ /// Encodes the scanline
+ ///
+ /// The scanline to encode
+ /// The previous scanline.
+ /// The filtered scanline result.
+ /// The bytes per pixel.
+ /// The sum of the total variance of the filtered row
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void EncodePaethFilter(Span scanline, Span previousScanline, Span result, int bytesPerPixel, out int sum)
+ {
+ DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
+
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
+ ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
+ sum = 0;
+
+ // Paeth(x) = Raw(x) - PaethPredictor(Raw(x-bpp), Prior(x), Prior(x - bpp))
+ resultBaseRef = 4;
+
+ int x = 0;
+ for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
+ {
+ byte scan = Unsafe.Add(ref scanBaseRef, x);
+ byte above = Unsafe.Add(ref prevBaseRef, x);
+ ++x;
+ ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
+ res = (byte)(scan - PaethPredictor(0, above, 0));
+ sum += Numerics.Abs(unchecked((sbyte)res));
+ }
+
+ for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
+ {
+ byte scan = Unsafe.Add(ref scanBaseRef, x);
+ byte left = Unsafe.Add(ref scanBaseRef, xLeft);
+ byte above = Unsafe.Add(ref prevBaseRef, x);
+ byte upperLeft = Unsafe.Add(ref prevBaseRef, xLeft);
+ ++x;
+ ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
+ res = (byte)(scan - PaethPredictor(left, above, upperLeft));
+ sum += Numerics.Abs(unchecked((sbyte)res));
+ }
+
+ sum -= 4;
+ }
+
+ ///
+ /// Encodes the scanline
+ ///
+ /// The scanline to encode
+ /// The filtered scanline result.
+ /// The bytes per pixel.
+ /// The sum of the total variance of the filtered row
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void EncodeSubFilter(Span scanline, Span result, int bytesPerPixel, out int sum)
+ {
+ DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
+
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
+ sum = 0;
+
+ // Sub(x) = Raw(x) - Raw(x-bpp)
+ resultBaseRef = 1;
+
+ int x = 0;
+ for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
+ {
+ byte scan = Unsafe.Add(ref scanBaseRef, x);
+ ++x;
+ ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
+ res = scan;
+ sum += Numerics.Abs(unchecked((sbyte)res));
+ }
+
+ for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
+ {
+ byte scan = Unsafe.Add(ref scanBaseRef, x);
+ byte prev = Unsafe.Add(ref scanBaseRef, xLeft);
+ ++x;
+ ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
+ res = (byte)(scan - prev);
+ sum += Numerics.Abs(unchecked((sbyte)res));
+ }
+
+ sum -= 1;
+ }
+
+ ///
+ /// Encodes the scanline
+ ///
+ /// The scanline to encode
+ /// The previous scanline.
+ /// The filtered scanline result.
+ /// The sum of the total variance of the filtered row
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void EncodeUpFilter(Span scanline, Span previousScanline, Span result, out int sum)
+ {
+ DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
+
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
+ ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
+ sum = 0;
+
+ // Up(x) = Raw(x) - Prior(x)
+ resultBaseRef = 2;
+
+ int x = 0;
+
+ for (; x < scanline.Length; /* Note: ++x happens in the body to avoid one add operation */)
+ {
+ byte scan = Unsafe.Add(ref scanBaseRef, x);
+ byte above = Unsafe.Add(ref prevBaseRef, x);
+ ++x;
+ ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
+ res = (byte)(scan - above);
+ sum += Numerics.Abs(unchecked((sbyte)res));
+ }
+
+ sum -= 2;
+ }
+
+ ///
+ /// Encodes the scanline
+ ///
+ /// The scanline to encode
+ /// The previous scanline.
+ /// The filtered scanline result.
+ /// The bytes per pixel.
+ /// The sum of the total variance of the filtered row
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void EncodeAverageFilter(Span scanline, Span previousScanline, Span result, int bytesPerPixel, out int sum)
+ {
+ DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ DebugGuard.MustBeSizedAtLeast(result, scanline, nameof(result));
+
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
+ ref byte resultBaseRef = ref MemoryMarshal.GetReference(result);
+ sum = 0;
+
+ // Average(x) = Raw(x) - floor((Raw(x-bpp)+Prior(x))/2)
+ resultBaseRef = 3;
+
+ int x = 0;
+ for (; x < bytesPerPixel; /* Note: ++x happens in the body to avoid one add operation */)
+ {
+ byte scan = Unsafe.Add(ref scanBaseRef, x);
+ byte above = Unsafe.Add(ref prevBaseRef, x);
+ ++x;
+ ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
+ res = (byte)(scan - (above >> 1));
+ sum += Numerics.Abs(unchecked((sbyte)res));
+ }
+
+ for (int xLeft = x - bytesPerPixel; x < scanline.Length; ++xLeft /* Note: ++x happens in the body to avoid one add operation */)
+ {
+ byte scan = Unsafe.Add(ref scanBaseRef, x);
+ byte left = Unsafe.Add(ref scanBaseRef, xLeft);
+ byte above = Unsafe.Add(ref prevBaseRef, x);
+ ++x;
+ ref byte res = ref Unsafe.Add(ref resultBaseRef, x);
+ res = (byte)(scan - Average(left, above));
+ sum += Numerics.Abs(unchecked((sbyte)res));
+ }
+
+ sum -= 3;
+ }
+
+ ///
+ /// Calculates the average value of two bytes
+ ///
+ /// The left byte
+ /// The above byte
+ /// The
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static int Average(byte left, byte above) => (left + above) >> 1;
+
+ ///
+ /// Computes a simple linear function of the three neighboring pixels (left, above, upper left), then chooses
+ /// as predictor the neighboring pixel closest to the computed value.
+ ///
+ /// The left neighbor pixel.
+ /// The above neighbor pixel.
+ /// The upper left neighbor pixel.
+ ///
+ /// The .
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static byte PaethPredictor(byte left, byte above, byte upperLeft)
+ {
+ int p = left + above - upperLeft;
+ int pa = Numerics.Abs(p - left);
+ int pb = Numerics.Abs(p - above);
+ int pc = Numerics.Abs(p - upperLeft);
+
+ if (pa <= pb && pa <= pc)
+ {
+ return left;
+ }
+
+ if (pb <= pc)
+ {
+ return above;
+ }
+
+ return upperLeft;
+ }
+ }
+}