diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
index 83c638934..44a16f154 100644
--- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
@@ -20,9 +20,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
internal static class AverageFilter
{
///
- /// Decodes the scanline
+ /// Decodes a scanline, which was filtered with the average filter.
///
- /// The scanline to decode
+ /// The scanline to decode.
/// The previous scanline.
/// The bytes per pixel.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -30,11 +30,66 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ // The Avg filter predicts each pixel as the (truncated) average of a and b:
+ // Average(x) + floor((Raw(x-bpp)+Prior(x))/2)
+ // With pixels positioned like this:
+ // prev: c b
+ // row: a d
+#if SUPPORTS_RUNTIME_INTRINSICS
+ if (Sse2.IsSupported && bytesPerPixel is 4)
+ {
+ DecodeSse2(scanline, previousScanline);
+ }
+ else
+#endif
+ {
+ DecodeScalar(scanline, previousScanline, bytesPerPixel);
+ }
+ }
+
+#if SUPPORTS_RUNTIME_INTRINSICS
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void DecodeSse2(Span scanline, Span previousScanline)
+ {
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
- // Average(x) + floor((Raw(x-bpp)+Prior(x))/2)
- int x = 1;
+ Vector128 d = Vector128.Zero;
+ var ones = Vector128.Create((byte)1);
+
+ int rb = scanline.Length;
+ nint offset = 1;
+ while (rb >= 4)
+ {
+ ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
+ Vector128 a = d;
+ Vector128 b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte();
+ d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte();
+
+ // PNG requires a truncating average, so we can't just use _mm_avg_epu8,
+ // but we can fix it up by subtracting off 1 if it rounded up.
+ Vector128 avg = Sse2.Average(a, b);
+ Vector128 xor = Sse2.Xor(a, b);
+ Vector128 and = Sse2.And(xor, ones);
+ avg = Sse2.Subtract(avg, and);
+ d = Sse2.Add(d, avg);
+
+ // Store the result.
+ Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32());
+
+ rb -= 4;
+ offset += 4;
+ }
+ }
+#endif
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void DecodeScalar(Span scanline, Span previousScanline, int bytesPerPixel)
+ {
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
+
+ nint x = 1;
for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x)
{
ref byte scan = ref Unsafe.Add(ref scanBaseRef, x);
@@ -52,13 +107,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
}
///
- /// Encodes the scanline
+ /// Encodes a scanline with the average filter applied.
///
- /// The scanline to encode
+ /// 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
+ /// The sum of the total variance of the filtered row.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum)
{
diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
index 6a89a1122..0553eb46a 100644
--- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
@@ -22,9 +22,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
internal static class PaethFilter
{
///
- /// Decodes the scanline
+ /// Decodes a scanline, which was filtered with the paeth filter.
///
- /// The scanline to decode
+ /// The scanline to decode.
/// The previous scanline.
/// The bytes per pixel.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -32,6 +32,86 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+ // Paeth tries to predict pixel d using the pixel to the left of it, a,
+ // and two pixels from the previous row, b and c:
+ // prev: c b
+ // row: a d
+ // The Paeth function predicts d to be whichever of a, b, or c is nearest to
+ // p = a + b - c.
+#if SUPPORTS_RUNTIME_INTRINSICS
+ if (Sse41.IsSupported && bytesPerPixel is 4)
+ {
+ DecodeSse41(scanline, previousScanline);
+ }
+ else
+#endif
+ {
+ DecodeScalar(scanline, previousScanline, bytesPerPixel);
+ }
+ }
+
+#if SUPPORTS_RUNTIME_INTRINSICS
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void DecodeSse41(Span scanline, Span previousScanline)
+ {
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
+
+ Vector128 b = Vector128.Zero;
+ Vector128 d = Vector128.Zero;
+
+ int rb = scanline.Length;
+ nint offset = 1;
+ while (rb >= 4)
+ {
+ ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
+
+ // It's easiest to do this math (particularly, deal with pc) with 16-bit intermediates.
+ Vector128 c = b;
+ Vector128 a = d;
+ b = Sse2.UnpackLow(
+ Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(),
+ Vector128.Zero);
+ d = Sse2.UnpackLow(
+ Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(),
+ Vector128.Zero);
+
+ // (p-a) == (a+b-c - a) == (b-c)
+ Vector128 pa = Sse2.Subtract(b.AsInt16(), c.AsInt16());
+
+ // (p-b) == (a+b-c - b) == (a-c)
+ Vector128 pb = Sse2.Subtract(a.AsInt16(), c.AsInt16());
+
+ // (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c)
+ Vector128 pc = Sse2.Add(pa.AsInt16(), pb.AsInt16());
+
+ pa = Ssse3.Abs(pa.AsInt16()).AsInt16(); /* |p-a| */
+ pb = Ssse3.Abs(pb.AsInt16()).AsInt16(); /* |p-b| */
+ pc = Ssse3.Abs(pc.AsInt16()).AsInt16(); /* |p-c| */
+
+ Vector128 smallest = Sse2.Min(pc, Sse2.Min(pa, pb));
+
+ // Paeth breaks ties favoring a over b over c.
+ Vector128 mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte());
+ Vector128 nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte());
+
+ // Note `_epi8`: we need addition to wrap modulo 255.
+ d = Sse2.Add(d, nearest);
+
+ // Store the result.
+ Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(Sse2.PackUnsignedSaturate(d.AsInt16(), d.AsInt16()).AsInt32());
+
+ rb -= 4;
+ offset += 4;
+ }
+ }
+
+#endif
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void DecodeScalar(Span scanline, Span previousScanline, int bytesPerPixel)
+ {
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@@ -56,13 +136,13 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
}
///
- /// Encodes the scanline
+ /// Encodes a scanline and applies the paeth filter.
///
/// 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
+ /// The sum of the total variance of the filtered row.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, int bytesPerPixel, out int sum)
{
diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs
index c28b877e4..eaa4dc034 100644
--- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs
@@ -21,17 +21,57 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
internal static class SubFilter
{
///
- /// Decodes the scanline
+ /// Decodes a scanline, which was filtered with the sub filter.
///
- /// The scanline to decode
+ /// The scanline to decode.
/// The bytes per pixel.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Decode(Span scanline, int bytesPerPixel)
+ {
+ // The Sub filter predicts each pixel as the previous pixel.
+#if SUPPORTS_RUNTIME_INTRINSICS
+ if (Sse2.IsSupported && bytesPerPixel is 4)
+ {
+ DecodeSse2(scanline);
+ }
+ else
+#endif
+ {
+ DecodeScalar(scanline, bytesPerPixel);
+ }
+ }
+
+#if SUPPORTS_RUNTIME_INTRINSICS
+ private static void DecodeSse2(Span scanline)
+ {
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+
+ Vector128 d = Vector128.Zero;
+
+ int rb = scanline.Length;
+ nint offset = 1;
+ while (rb >= 4)
+ {
+ ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
+ Vector128 a = d;
+ d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte();
+
+ d = Sse2.Add(d, a);
+
+ Unsafe.As(ref scanRef) = Sse2.ConvertToInt32(d.AsInt32());
+
+ rb -= 4;
+ offset += 4;
+ }
+ }
+#endif
+
+ private static void DecodeScalar(Span scanline, int bytesPerPixel)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
// Sub(x) + Raw(x-bpp)
- int x = bytesPerPixel + 1;
+ nint x = bytesPerPixel + 1;
Unsafe.Add(ref scanBaseRef, x);
for (; x < scanline.Length; ++x)
{
@@ -42,12 +82,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
}
///
- /// Encodes the scanline
+ /// Encodes a scanline with the sup filter applied.
///
- /// The scanline to encode
+ /// The scanline to encode.
/// The filtered scanline result.
/// The bytes per pixel.
- /// The sum of the total variance of the filtered row
+ /// The sum of the total variance of the filtered row.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan scanline, ReadOnlySpan result, int bytesPerPixel, out int sum)
{
diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs
index 7e0286991..0d24d9c5d 100644
--- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs
+++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs
@@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
internal static class UpFilter
{
///
- /// Decodes the scanline
+ /// Decodes a scanline, which was filtered with the up filter.
///
/// The scanline to decode
/// The previous scanline.
@@ -30,6 +30,91 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
{
DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline));
+#if SUPPORTS_RUNTIME_INTRINSICS
+ if (Avx2.IsSupported)
+ {
+ DecodeAvx2(scanline, previousScanline);
+ }
+ else if (Sse2.IsSupported)
+ {
+ DecodeSse2(scanline, previousScanline);
+ }
+ else
+#endif
+ {
+ DecodeScalar(scanline, previousScanline);
+ }
+ }
+
+#if SUPPORTS_RUNTIME_INTRINSICS
+ private static void DecodeAvx2(Span scanline, Span previousScanline)
+ {
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
+
+ // Up(x) + Prior(x)
+ int rb = scanline.Length;
+ nint offset = 1;
+ const int bytesPerBatch = 32;
+ while (rb >= bytesPerBatch)
+ {
+ ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
+ Vector256 current = Unsafe.As>(ref scanRef);
+ Vector256 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset));
+
+ Vector256 sum = Avx2.Add(up, current);
+ Unsafe.As>(ref scanRef) = sum;
+
+ offset += bytesPerBatch;
+ rb -= bytesPerBatch;
+ }
+
+ // Handle left over.
+ for (nint i = offset; i < scanline.Length; i++)
+ {
+ ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset);
+ byte above = Unsafe.Add(ref prevBaseRef, offset);
+ scan = (byte)(scan + above);
+ offset++;
+ }
+ }
+
+ private static void DecodeSse2(Span scanline, Span previousScanline)
+ {
+ ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
+ ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
+
+ // Up(x) + Prior(x)
+ int rb = scanline.Length;
+ nint offset = 1;
+ const int bytesPerBatch = 16;
+ while (rb >= bytesPerBatch)
+ {
+ ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
+ Vector128 current = Unsafe.As>(ref scanRef);
+ Vector128 up = Unsafe.As>(ref Unsafe.Add(ref prevBaseRef, offset));
+
+ Vector128 sum = Sse2.Add(up, current);
+ Unsafe.As>(ref scanRef) = sum;
+
+ offset += bytesPerBatch;
+ rb -= bytesPerBatch;
+ }
+
+ // Handle left over.
+ for (nint i = offset; i < scanline.Length; i++)
+ {
+ ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset);
+ byte above = Unsafe.Add(ref prevBaseRef, offset);
+ scan = (byte)(scan + above);
+ offset++;
+ }
+ }
+#endif
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void DecodeScalar(Span scanline, Span previousScanline)
+ {
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
@@ -43,12 +128,12 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters
}
///
- /// Encodes the scanline
+ /// Encodes a scanline with the up filter applied.
///
- /// The scanline to encode
+ /// The scanline to encode.
/// The previous scanline.
/// The filtered scanline result.
- /// The sum of the total variance of the filtered row
+ /// The sum of the total variance of the filtered row.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Encode(ReadOnlySpan scanline, ReadOnlySpan previousScanline, Span result, out int sum)
{
diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs
index 6517bf3c4..5f91a050e 100644
--- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs
+++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs
@@ -16,16 +16,18 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
private byte[] filter1;
private byte[] filter2;
private byte[] filter3;
- private byte[] filter4;
+ private byte[] averageFilter3bpp;
+ private byte[] averageFilter4bpp;
[GlobalSetup]
public void ReadImages()
{
this.filter0 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter0));
- this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter1));
- this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter2));
- this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter3));
- this.filter4 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4));
+ this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.SubFilter3BytesPerPixel));
+ this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.UpFilter));
+ this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.PaethFilter3BytesPerPixel));
+ this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3BytesPerPixel));
+ this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4BytesPerPixel));
}
[Benchmark(Baseline = true, Description = "None-filtered PNG file")]
@@ -40,13 +42,17 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs
public Size PngFilter2()
=> LoadPng(this.filter2);
- [Benchmark(Description = "Average-filtered PNG file")]
- public Size PngFilter3()
- => LoadPng(this.filter3);
+ [Benchmark(Description = "Average-filtered PNG file (3bpp)")]
+ public Size PngAvgFilter1()
+ => LoadPng(this.averageFilter3bpp);
+
+ [Benchmark(Description = "Average-filtered PNG file (4bpp)")]
+ public Size PngAvgFilter2()
+ => LoadPng(this.averageFilter4bpp);
[Benchmark(Description = "Paeth-filtered PNG file")]
public Size PngFilter4()
- => LoadPng(this.filter4);
+ => LoadPng(this.filter3);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Size LoadPng(byte[] bytes)
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs
new file mode 100644
index 000000000..edfff19a4
--- /dev/null
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs
@@ -0,0 +1,179 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using SixLabors.ImageSharp.Formats.Png.Filters;
+using SixLabors.ImageSharp.Tests.TestUtilities;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.Formats.Png
+{
+ [Trait("Format", "Png")]
+ public class PngDecoderFilterTests
+ {
+ private static void RunAverageFilterTest()
+ {
+ // arrange
+ byte[] scanline =
+ {
+ 3, 39, 39, 39, 0, 4, 4, 4, 0, 1, 1, 1, 0, 1, 1, 1, 0, 2, 2, 2, 0, 2, 2, 2, 0, 2, 2, 2, 0, 4, 4, 4,
+ 0, 2, 2, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 2, 2, 2, 0,
+ 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 1, 1, 1, 0, 3, 3, 3, 0, 3, 3, 3, 0, 254, 254, 254,
+ 0, 6, 6, 6, 14, 71, 71, 71, 157, 254, 254, 254, 28, 251, 251, 251, 0, 4, 4, 4, 0, 2, 2, 2, 0, 11,
+ 11, 11, 0, 226, 226, 226, 0, 255, 128, 234
+ };
+
+ byte[] previousScanline =
+ {
+ 3, 74, 74, 74, 0, 73, 73, 73, 0, 73, 73, 73, 0, 74, 74, 74, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72,
+ 72, 0, 72, 72, 72, 0, 73, 73, 73, 0, 74, 74, 74, 0, 73, 73, 73, 0, 72, 72, 72, 0, 72, 72, 72, 0, 74,
+ 74, 74, 0, 72, 72, 72, 0, 73, 73, 73, 0, 75, 75, 75, 0, 73, 73, 73, 0, 74, 74, 74, 0, 72, 72, 72, 0,
+ 73, 73, 73, 0, 73, 73, 73, 0, 72, 72, 72, 0, 74, 74, 74, 0, 61, 61, 61, 0, 101, 101, 101, 78, 197,
+ 197, 197, 251, 152, 152, 152, 255, 155, 155, 155, 255, 162, 162, 162, 255, 175, 175, 175, 255, 160,
+ 160, 160, 255, 139, 128, 134
+ };
+
+ byte[] expected =
+ {
+ 3, 76, 76, 76, 0, 78, 78, 78, 0, 76, 76, 76, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76, 76,
+ 76, 0, 78, 78, 78, 0, 77, 77, 77, 0, 78, 78, 78, 0, 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 76,
+ 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 77, 77, 77, 0, 78, 78, 78, 0, 77, 77, 77, 0, 77, 77, 77, 0,
+ 76, 76, 76, 0, 77, 77, 77, 0, 77, 77, 77, 0, 73, 73, 73, 0, 73, 73, 73, 14, 158, 158, 158, 203, 175,
+ 175, 175, 255, 158, 158, 158, 255, 160, 160, 160, 255, 163, 163, 163, 255, 180, 180, 180, 255, 140,
+ 140, 140, 255, 138, 6, 115
+ };
+
+ // act
+ AverageFilter.Decode(scanline, previousScanline, 4);
+
+ // assert
+ Assert.Equal(expected, scanline);
+ }
+
+ private static void RunUpFilterTest()
+ {
+ // arrange
+ byte[] scanline =
+ {
+ 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23,
+ 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220,
+ 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254
+ };
+
+ byte[] previousScanline =
+ {
+ 214, 103, 135, 26, 133, 179, 134, 168, 175, 114, 118, 99, 167, 129, 55, 105, 129, 154, 173, 235,
+ 179, 191, 41, 137, 253, 0, 81, 198, 159, 228, 224, 245, 14, 113, 5, 45, 126, 239, 233, 179, 229, 62,
+ 66, 155, 207, 117, 128, 56, 181, 190, 160, 96, 11, 248, 74, 23, 62, 253, 29, 132, 98, 192, 9, 202
+ };
+
+ byte[] expected =
+ {
+ 62, 126, 65, 176, 51, 183, 83, 227, 72, 248, 20, 185, 151, 46, 246, 163, 240, 81, 250, 16, 8, 214,
+ 134, 85, 107, 139, 90, 218, 246, 126, 144, 43, 221, 71, 45, 56, 49, 182, 240, 142, 147, 48, 178,
+ 119, 100, 122, 137, 166, 28, 41, 135, 81, 24, 62, 34, 62, 248, 234, 68, 166, 93, 121, 237, 200
+ };
+
+ // act
+ UpFilter.Decode(scanline, previousScanline);
+
+ // assert
+ Assert.Equal(expected, scanline);
+ }
+
+ private static void RunSubFilterTest()
+ {
+ // arrange
+ byte[] scanline =
+ {
+ 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23,
+ 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220,
+ 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254
+ };
+
+ byte[] expected =
+ {
+ 62, 23, 186, 150, 174, 27, 135, 209, 71, 161, 37, 39, 55, 78, 228, 97, 166, 5, 49, 134, 251, 28,
+ 142, 82, 105, 167, 151, 102, 192, 65, 71, 156, 143, 23, 111, 167, 66, 222, 118, 130, 240, 208, 230,
+ 94, 133, 213, 239, 204, 236, 64, 214, 189, 249, 134, 174, 228, 179, 115, 213, 6, 174, 44, 185, 4
+ };
+
+ // act
+ SubFilter.Decode(scanline, 4);
+
+ // assert
+ Assert.Equal(expected, scanline);
+ }
+
+ private static void RunPaethFilterTest()
+ {
+ // arrange
+ byte[] scanline =
+ {
+ 62, 23, 186, 150, 174, 4, 205, 59, 153, 134, 158, 86, 240, 173, 191, 58, 111, 183, 77, 37, 85, 23,
+ 93, 204, 110, 139, 9, 20, 87, 154, 176, 54, 207, 214, 40, 11, 179, 199, 7, 219, 174, 242, 112, 220,
+ 149, 5, 9, 110, 103, 107, 231, 241, 13, 70, 216, 39, 186, 237, 39, 34, 251, 185, 228, 254
+ };
+
+ byte[] previousScanline =
+ {
+ 214, 103, 135, 26, 133, 179, 134, 168, 175, 114, 118, 99, 167, 129, 55, 105, 129, 154, 173, 235,
+ 179, 191, 41, 137, 253, 0, 81, 198, 159, 228, 224, 245, 14, 113, 5, 45, 126, 239, 233, 179, 229, 62,
+ 66, 155, 207, 117, 128, 56, 181, 190, 160, 96, 11, 248, 74, 23, 62, 253, 29, 132, 98, 192, 9, 202
+ };
+
+ byte[] expected =
+ {
+ 62, 126, 65, 176, 51, 183, 14, 235, 30, 248, 172, 254, 14, 165, 53, 56, 125, 92, 250, 16, 8, 177,
+ 10, 220, 118, 139, 50, 240, 205, 126, 144, 43, 221, 71, 45, 54, 144, 182, 240, 142, 147, 48, 178,
+ 106, 40, 122, 187, 166, 143, 41, 162, 151, 24, 111, 34, 135, 248, 92, 68, 169, 243, 21, 1, 200
+ };
+
+ // act
+ PaethFilter.Decode(scanline, previousScanline, 4);
+
+ // assert
+ Assert.Equal(expected, scanline);
+ }
+
+ [Fact]
+ public void AverageFilter_Works() => RunAverageFilterTest();
+
+ [Fact]
+ public void UpFilter_Works() => RunUpFilterTest();
+
+ [Fact]
+ public void SubFilter_Works() => RunSubFilterTest();
+
+ [Fact]
+ public void PaethFilter_Works() => RunPaethFilterTest();
+
+#if SUPPORTS_RUNTIME_INTRINSICS
+ [Fact]
+ public void AverageFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll);
+
+ [Fact]
+ public void AverageFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic);
+
+ [Fact]
+ public void UpFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.AllowAll);
+
+ [Fact]
+ public void UpFilter_WithoutAVX2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.DisableAVX2);
+
+ [Fact]
+ public void UpFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunUpFilterTest, HwIntrinsics.DisableHWIntrinsic);
+
+ [Fact]
+ public void SubFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubFilterTest, HwIntrinsics.AllowAll);
+
+ [Fact]
+ public void SubFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSubFilterTest, HwIntrinsics.DisableHWIntrinsic);
+
+ [Fact]
+ public void PaethFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.AllowAll);
+
+ [Fact]
+ public void PaethFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunPaethFilterTest, HwIntrinsics.DisableHWIntrinsic);
+#endif
+ }
+}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index 0af5d9995..752036126 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -111,6 +111,49 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
image.CompareToOriginal(provider, ImageComparer.Exact);
}
+ [Theory]
+ [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba32)]
+ public void Decode_WithAverageFilter(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Png.SubFilter3BytesPerPixel, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.SubFilter4BytesPerPixel, PixelTypes.Rgba32)]
+ public void Decode_WithSubFilter(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Png.UpFilter, PixelTypes.Rgba32)]
+ public void Decode_WithUpFilter(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact);
+ }
+
+ [Theory]
+ [WithFile(TestImages.Png.PaethFilter3BytesPerPixel, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Png.PaethFilter4BytesPerPixel, PixelTypes.Rgba32)]
+ public void Decode_WithPaethFilter(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ImageComparer.Exact);
+ }
+
[Theory]
[WithFile(TestImages.Png.GrayA8Bit, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Gray1BitTrans, PixelTypes.Rgba32)]
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs
similarity index 98%
rename from tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs
rename to tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs
index 9b6119380..11e3fbb23 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngFilterTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs
@@ -13,7 +13,7 @@ using Xunit.Abstractions;
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
[Trait("Format", "Png")]
- public partial class PngFilterTests : MeasureFixture
+ public class PngEncoderFilterTests : MeasureFixture
{
#if BENCHMARKING
public const int Times = 1000000;
@@ -21,7 +21,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
public const int Times = 1;
#endif
- public PngFilterTests(ITestOutputHelper output)
+ public PngEncoderFilterTests(ITestOutputHelper output)
: base(output)
{
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 7ae0ac6e2..aa4314b8e 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -66,10 +66,13 @@ namespace SixLabors.ImageSharp.Tests
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
- public const string Filter1 = "Png/filter1.png";
- public const string Filter2 = "Png/filter2.png";
- public const string Filter3 = "Png/filter3.png";
- public const string Filter4 = "Png/filter4.png";
+ public const string SubFilter3BytesPerPixel = "Png/filter1.png";
+ public const string SubFilter4BytesPerPixel = "Png/SubFilter4Bpp.png";
+ public const string UpFilter = "Png/filter2.png";
+ public const string AverageFilter3BytesPerPixel = "Png/filter3.png";
+ public const string AverageFilter4BytesPerPixel = "Png/AverageFilter4Bpp.png";
+ public const string PaethFilter3BytesPerPixel = "Png/filter4.png";
+ public const string PaethFilter4BytesPerPixel = "Png/PaethFilter4Bpp.png";
// Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string PalettedTwoColor = "Png/basn3p01.png";
@@ -154,15 +157,6 @@ namespace SixLabors.ImageSharp.Tests
public const string ColorTypeOne = "Png/xc1n0g08.png";
public const string ColorTypeNine = "Png/xc9n2c08.png";
}
-
- public static readonly string[] All =
- {
- P1, Pd, Blur, Splash, Cross,
- Powerpoint, SplashInterlaced, Interlaced,
- Filter0, Filter1, Filter2, Filter3, Filter4,
- FilterVar, VimImage1, VimImage2, VersioningImage1,
- VersioningImage2, Ratio4x1, Ratio1x4
- };
}
public static class Jpeg
diff --git a/tests/Images/Input/Png/AverageFilter4Bpp.png b/tests/Images/Input/Png/AverageFilter4Bpp.png
new file mode 100644
index 000000000..728b6cfaf
--- /dev/null
+++ b/tests/Images/Input/Png/AverageFilter4Bpp.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7add6fba794bc76ccea2ee3a311b4050cf17f4f78b69a50785f7739b8b35919e
+size 181108
diff --git a/tests/Images/Input/Png/PaethFilter4Bpp.png b/tests/Images/Input/Png/PaethFilter4Bpp.png
new file mode 100644
index 000000000..64c9f96ec
--- /dev/null
+++ b/tests/Images/Input/Png/PaethFilter4Bpp.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8b2b0a1190854577d5181fe40af61c421d615a1a2727cf9be5ebe727eaafd00d
+size 8624
diff --git a/tests/Images/Input/Png/SubFilter4Bpp.png b/tests/Images/Input/Png/SubFilter4Bpp.png
new file mode 100644
index 000000000..d9f2c7fa2
--- /dev/null
+++ b/tests/Images/Input/Png/SubFilter4Bpp.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:053fac72ff62c66dacb41a6251efa249d5b31567e0222efbf5b1bef912c0bf77
+size 13013