From 204f94e84bc01975c191ce0ed3611e69c1d9425f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 18 Feb 2022 14:43:10 +0100 Subject: [PATCH 01/28] Add tiff decoder option to only decode first frame --- src/ImageSharp/Formats/Gif/GifDecoder.cs | 1 - src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs | 7 +++++++ src/ImageSharp/Formats/Tiff/TiffDecoder.cs | 6 ++++++ src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs | 14 ++++++++++++-- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 196d77ad77..c31a2c1c94 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -5,7 +5,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.IO; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs index 5372384397..d6d1bb8a4c 100644 --- a/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs +++ b/src/ImageSharp/Formats/Tiff/ITiffDecoderOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using SixLabors.ImageSharp.Metadata; + namespace SixLabors.ImageSharp.Formats.Tiff { /// @@ -12,5 +14,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// bool IgnoreMetadata { get; } + + /// + /// Gets the decoding mode for multi-frame images. + /// + FrameDecodingMode DecodingMode { get; } } } diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs index 9d52e34dfe..b4d7520199 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoder.cs @@ -4,6 +4,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -18,6 +19,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// public bool IgnoreMetadata { get; set; } + /// + /// Gets or sets the decoding mode for multi-frame images. + /// + public FrameDecodingMode DecodingMode { get; set; } + /// public Image Decode(Configuration configuration, Stream stream) where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 05c5358f59..cd06282f18 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -13,7 +13,6 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; -using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -33,6 +32,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff /// private readonly bool ignoreMetadata; + /// + /// Gets the decoding mode for multi-frame images + /// + private FrameDecodingMode decodingMode; + /// /// The stream to decode from. /// @@ -59,6 +63,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff this.Configuration = configuration ?? Configuration.Default; this.ignoreMetadata = options.IgnoreMetadata; + this.decodingMode = options.DecodingMode; this.memoryAllocator = this.Configuration.MemoryAllocator; } @@ -160,11 +165,16 @@ namespace SixLabors.ImageSharp.Formats.Tiff cancellationToken.ThrowIfCancellationRequested(); ImageFrame frame = this.DecodeFrame(ifd, cancellationToken); frames.Add(frame); + + if (this.decodingMode is FrameDecodingMode.First) + { + break; + } } ImageMetadata metadata = TiffDecoderMetadataCreator.Create(frames, this.ignoreMetadata, reader.ByteOrder, reader.IsBigTiff); - // TODO: Tiff frames can have different sizes + // TODO: Tiff frames can have different sizes. ImageFrame root = frames[0]; this.Dimensions = root.Size(); foreach (ImageFrame frame in frames) From a963fa09adb839d9ddba40f2524cb6ed00b4ed13 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 18 Feb 2022 14:53:00 +0100 Subject: [PATCH 02/28] Add test for decoding only the first frame --- .../Formats/Tiff/TiffDecoderTests.cs | 12 ++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Tiff/SKC1H3.tiff | 3 +++ 3 files changed, 16 insertions(+) create mode 100644 tests/Images/Input/Tiff/SKC1H3.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ea0544acf4..51d38dc580 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -4,6 +4,7 @@ // ReSharper disable InconsistentNaming using System; using System.IO; +using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -365,6 +366,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff public void TiffDecoder_CanDecode_PackBitsCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(MultiFrameMipMap, PixelTypes.Rgba32)] + public void CanDecodeJustOneFrame(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = provider.GetImage(new TiffDecoder() { DecodingMode = FrameDecodingMode.First })) + { + Assert.Equal(1, image.Frames.Count); + } + } + [Theory] [WithFile(RgbJpegCompressed, PixelTypes.Rgba32)] [WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8b943194a5..d7ac9c3641 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -867,6 +867,7 @@ namespace SixLabors.ImageSharp.Tests public const string MultiframeDeflateWithPreview = "Tiff/multipage_deflate_withPreview.tiff"; public const string MultiframeDifferentSize = "Tiff/multipage_differentSize.tiff"; public const string MultiframeDifferentVariants = "Tiff/multipage_differentVariants.tiff"; + public const string MultiFrameMipMap = "Tiff/SKC1H3.tiff"; public const string LittleEndianByteOrder = "Tiff/little_endian.tiff"; diff --git a/tests/Images/Input/Tiff/SKC1H3.tiff b/tests/Images/Input/Tiff/SKC1H3.tiff new file mode 100644 index 0000000000..9f9a50fdd6 --- /dev/null +++ b/tests/Images/Input/Tiff/SKC1H3.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:938bbf1c0f8bdbea0c632bb8d51c1150f757f88b3779d7fa18c296a3a3f61e9b +size 13720193 From a01ff4a6427f4ea25d1f132424cdff7525599357 Mon Sep 17 00:00:00 2001 From: Titus Date: Sat, 19 Feb 2022 10:14:46 -0500 Subject: [PATCH 03/28] Fix issue in PNG identify method to skip "uninteresting" chunks, including sBIT. --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index f5fc86ee4d..a7f4d5e8d9 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -336,6 +336,9 @@ namespace SixLabors.ImageSharp.Formats.Png break; case PngChunkType.End: goto EOF; + default: + this.SkipChunkDataAndCrc(chunk); + break; } } finally From b0b7ae641be7123c25a5e75d9ac5937643fc45f4 Mon Sep 17 00:00:00 2001 From: Titus Date: Sat, 19 Feb 2022 13:53:04 -0500 Subject: [PATCH 04/28] Moved SkipChunkDataAndCrc call to "correct" location for this issue --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index a7f4d5e8d9..8d0b492611 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -336,9 +336,6 @@ namespace SixLabors.ImageSharp.Formats.Png break; case PngChunkType.End: goto EOF; - default: - this.SkipChunkDataAndCrc(chunk); - break; } } finally @@ -1401,6 +1398,8 @@ namespace SixLabors.ImageSharp.Formats.Png { chunk = new PngChunk(length, type); + this.SkipChunkDataAndCrc(chunk); + return true; } From d4180a92c5b691cf082caba03b30de3e2c2262b4 Mon Sep 17 00:00:00 2001 From: Titus Date: Sat, 19 Feb 2022 13:59:20 -0500 Subject: [PATCH 05/28] cleaned up whitespace --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 8d0b492611..56ad4ad86d 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -336,6 +336,14 @@ namespace SixLabors.ImageSharp.Formats.Png break; case PngChunkType.End: goto EOF; + + default: + if (this.colorMetadataOnly) + { + this.SkipChunkDataAndCrc(chunk); + } + + break; } } finally @@ -1398,8 +1406,6 @@ namespace SixLabors.ImageSharp.Formats.Png { chunk = new PngChunk(length, type); - this.SkipChunkDataAndCrc(chunk); - return true; } From 747422cf6d0075f03b1d10b0d7b07a2063d7a0c9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 22 Feb 2022 13:46:41 +0100 Subject: [PATCH 06/28] Add Sse2 version of Average png filter --- .../Formats/Png/Filters/AverageFilter.cs | 126 +++++++++++++++--- 1 file changed, 110 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 83c6389348..94c4fb4d1a 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)] @@ -33,32 +33,126 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + // The Avg filter predicts each pixel as the (truncated) average of a and b: // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) - int x = 1; - for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported && bytesPerPixel is 3 or 4) { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + (above >> 1)); - } + if (bytesPerPixel is 3) + { + Vector128 a = Vector128.Zero; + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + + Span scratch = stackalloc byte[4]; + ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); + int rb = scanline.Length; + int offset = 0; + while (rb >= 4) + { + a = d; + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); + + d = AverageSubtractAdd(a, b, d, ones); + + // Store the result. + int result = Sse2.ConvertToInt32(d.AsInt32()); + Unsafe.As(ref scratchRef) = result; + scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); + + rb -= 3; + offset += 3; + } + + if (rb is 3) + { + a = d; + scratch[3] = 0; + previousScanline.Slice(offset, 3).CopyTo(scratch); + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); + scanline.Slice(offset, 3).CopyTo(scratch); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); + + d = AverageSubtractAdd(a, b, d, ones); + + // Store the result. + int result = Sse2.ConvertToInt32(d.AsInt32()); + Unsafe.As(ref scratchRef) = result; + scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); + } + } + else + { + Vector128 a = Vector128.Zero; + Vector128 b = Vector128.Zero; + Vector128 d = Vector128.Zero; + var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + + Span scratch = stackalloc byte[4]; + ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); + int rb = scanline.Length; + int offset = 0; + while (rb >= 4) + { + a = d; + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); - for (; x < scanline.Length; ++x) + d = AverageSubtractAdd(a, b, d, ones); + + // Store the result. + int result = Sse2.ConvertToInt32(d.AsInt32()); + Unsafe.As(ref scratchRef) = result; + scratch.CopyTo(scanline.Slice(offset, 4)); + + rb -= 4; + offset += 4; + } + } + } + else +#endif { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + Average(left, above)); + int x = 1; + for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + (above >> 1)); + } + + for (; x < scanline.Length; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + Average(left, above)); + } } } +#if SUPPORTS_RUNTIME_INTRINSICS + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AverageSubtractAdd(Vector128 a, Vector128 b, Vector128 d, Vector128 ones) + { + // 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); + avg = Sse2.Subtract(avg, Sse2.And(Sse2.Xor(a, b), ones)); + return Sse2.Add(d, avg); + } +#endif + /// - /// 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) { From 6016c5cc7fb3508ea8b658c9d13c1b9b68bca718 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 22 Feb 2022 17:38:47 +0100 Subject: [PATCH 07/28] Add note about multiframe images --- src/ImageSharp/Formats/Tiff/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Tiff/README.md b/src/ImageSharp/Formats/Tiff/README.md index 488701e318..51e84ef558 100644 --- a/src/ImageSharp/Formats/Tiff/README.md +++ b/src/ImageSharp/Formats/Tiff/README.md @@ -25,7 +25,7 @@ ## Implementation Status -- The Decoder currently only supports a single frame per image. +- The Decoder currently only supports decoding multiframe images, which have the same dimensions. - Some compression formats are not yet supported. See the list below. ### Deviations from the TIFF spec (to be fixed) From 71520887acf61dd8d09716aee7f273d5135e5f59 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 22 Feb 2022 17:43:14 +0100 Subject: [PATCH 08/28] No need for the scratch buffer for 4 bytes per pixel --- .../Formats/Png/Filters/AverageFilter.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 94c4fb4d1a..50058f01d2 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -51,16 +51,16 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters int offset = 0; while (rb >= 4) { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); a = d; b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); + d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); d = AverageSubtractAdd(a, b, d, ones); // Store the result. int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scratchRef) = result; - scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); + Unsafe.As(ref scanRef) = result; rb -= 3; offset += 3; @@ -90,22 +90,20 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters Vector128 d = Vector128.Zero; var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - Span scratch = stackalloc byte[4]; - ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); int rb = scanline.Length; int offset = 0; while (rb >= 4) { + ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); a = d; - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); + b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); d = AverageSubtractAdd(a, b, d, ones); // Store the result. int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scratchRef) = result; - scratch.CopyTo(scanline.Slice(offset, 4)); + Unsafe.As(ref scanRef) = result; rb -= 4; offset += 4; From 18548f7eff6f815f9969ee6298810a5761e9499b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 22 Feb 2022 22:32:32 +0100 Subject: [PATCH 09/28] Dont use intrinsics for 3 bytes per pixel --- .../Formats/Png/Filters/AverageFilter.cs | 152 +++++++----------- 1 file changed, 56 insertions(+), 96 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 50058f01d2..7747d14ea2 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -30,119 +30,79 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); - ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); - ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - // The Avg filter predicts each pixel as the (truncated) average of a and b: // Average(x) + floor((Raw(x-bpp)+Prior(x))/2) #if SUPPORTS_RUNTIME_INTRINSICS - if (Sse2.IsSupported && bytesPerPixel is 3 or 4) + if (Sse2.IsSupported && bytesPerPixel is 4) { - if (bytesPerPixel is 3) - { - Vector128 a = Vector128.Zero; - Vector128 b = Vector128.Zero; - Vector128 d = Vector128.Zero; - var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - - Span scratch = stackalloc byte[4]; - ref byte scratchRef = ref MemoryMarshal.GetReference(scratch); - int rb = scanline.Length; - int offset = 0; - while (rb >= 4) - { - ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); - a = d; - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); - - d = AverageSubtractAdd(a, b, d, ones); - - // Store the result. - int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scanRef) = result; - - rb -= 3; - offset += 3; - } - - if (rb is 3) - { - a = d; - scratch[3] = 0; - previousScanline.Slice(offset, 3).CopyTo(scratch); - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); - scanline.Slice(offset, 3).CopyTo(scratch); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scratchRef)).AsByte(); - - d = AverageSubtractAdd(a, b, d, ones); - - // Store the result. - int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scratchRef) = result; - scratch.Slice(0, 3).CopyTo(scanline.Slice(offset, 3)); - } - } - else - { - Vector128 a = Vector128.Zero; - Vector128 b = Vector128.Zero; - Vector128 d = Vector128.Zero; - var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); - - int rb = scanline.Length; - int offset = 0; - while (rb >= 4) - { - ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); - a = d; - b = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref scanRef)).AsByte(); - d = Sse2.ConvertScalarToVector128Int32(Unsafe.As(ref Unsafe.Add(ref scanBaseRef, offset))).AsByte(); - - d = AverageSubtractAdd(a, b, d, ones); - - // Store the result. - int result = Sse2.ConvertToInt32(d.AsInt32()); - Unsafe.As(ref scanRef) = result; - - rb -= 4; - offset += 4; - } - } + DecodeSse2(scanline, previousScanline); } else #endif { - int x = 1; - for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + (above >> 1)); - } - - for (; x < scanline.Length; ++x) - { - ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); - byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); - byte above = Unsafe.Add(ref prevBaseRef, x); - scan = (byte)(scan + Average(left, above)); - } + DecodeScalar(scanline, previousScanline, bytesPerPixel); } } #if SUPPORTS_RUNTIME_INTRINSICS [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 AverageSubtractAdd(Vector128 a, Vector128 b, Vector128 d, Vector128 ones) + private static void DecodeSse2(Span scanline, Span previousScanline) { - // 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); - avg = Sse2.Subtract(avg, Sse2.And(Sse2.Xor(a, b), ones)); - return Sse2.Add(d, avg); + ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); + ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); + + Vector128 d = Vector128.Zero; + var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + + int rb = scanline.Length; + int 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); + + int x = 1; + for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + (above >> 1)); + } + + for (; x < scanline.Length; ++x) + { + ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); + byte left = Unsafe.Add(ref scanBaseRef, x - bytesPerPixel); + byte above = Unsafe.Add(ref prevBaseRef, x); + scan = (byte)(scan + Average(left, above)); + } + } + /// /// Encodes a scanline with the average filter applied. /// From 9eb71a20af6676afdb9c4cd07005d21cd022817d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 18:21:21 +0100 Subject: [PATCH 10/28] Add average filter tests --- .../Formats/Png/PngDecoderFilterTests.cs | 62 +++++++++++++++++++ ...ilterTests.cs => PngEncoderFilterTests.cs} | 4 +- 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs rename tests/ImageSharp.Tests/Formats/Png/{PngFilterTests.cs => PngEncoderFilterTests.cs} (98%) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs new file mode 100644 index 0000000000..a901fc6cdf --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -0,0 +1,62 @@ +// 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 +{ + 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 + }; + + 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 + }; + + 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, 255 + }; + + // act + AverageFilter.Decode(scanline, previousScanline, 4); + + // assert + Assert.Equal(scanline, expected); + } + + [Fact] + public void AverageInverse_Works() => RunAverageFilterTest(); + +#if SUPPORTS_RUNTIME_INTRINSICS + [Fact] + public void AverageInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); + + [Fact] + public void AverageInverse__WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableSSE2); +#endif + } +} 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 9b6119380a..11e3fbb230 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) { } From a921c57efc84a24b8d81843f61895b850a869077 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 18:33:52 +0100 Subject: [PATCH 11/28] Add benchmark file for average filter with 4bpp --- .../Codecs/Png/DecodeFilteredPng.cs | 20 ++++++++++++------- tests/ImageSharp.Tests/TestImages.cs | 6 ++++-- tests/Images/Input/Png/AverageFilter4Bpp.png | 3 +++ 3 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 tests/Images/Input/Png/AverageFilter4Bpp.png diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs index 6517bf3c48..373b563c82 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs @@ -16,7 +16,8 @@ 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() @@ -24,8 +25,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs 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.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4)); + this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3bpp)); + this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4bpp)); } [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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5bce99ce12..2f3bb49f61 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -68,9 +68,11 @@ namespace SixLabors.ImageSharp.Tests 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 AverageFilter3bpp = "Png/filter3.png"; public const string Filter4 = "Png/filter4.png"; + public const string AverageFilter4bpp = "Png/AverageFilter4Bpp.png"; + // Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string PalettedTwoColor = "Png/basn3p01.png"; public const string PalettedFourColor = "Png/basn3p02.png"; @@ -159,7 +161,7 @@ namespace SixLabors.ImageSharp.Tests { P1, Pd, Blur, Splash, Cross, Powerpoint, SplashInterlaced, Interlaced, - Filter0, Filter1, Filter2, Filter3, Filter4, + Filter0, Filter1, Filter2, AverageFilter3bpp, Filter4, FilterVar, VimImage1, VimImage2, VersioningImage1, VersioningImage2, Ratio4x1, Ratio1x4 }; diff --git a/tests/Images/Input/Png/AverageFilter4Bpp.png b/tests/Images/Input/Png/AverageFilter4Bpp.png new file mode 100644 index 0000000000..728b6cfaff --- /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 From 3ab1ba6bb304f52ef163c341ac8e22a82d62841a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 19:02:43 +0100 Subject: [PATCH 12/28] Use DisableHWIntrinsic in average filter test --- tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index a901fc6cdf..ce6cd05266 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void AverageInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); [Fact] - public void AverageInverse__WithoutSSE2_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableSSE2); + public void AverageInverse_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); #endif } } From 9f322555a96d7b920740608ca736bc5f8bffc911 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 19:17:44 +0100 Subject: [PATCH 13/28] Rename average filter tests --- tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index ce6cd05266..a096c5eb6c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -49,14 +49,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } [Fact] - public void AverageInverse_Works() => RunAverageFilterTest(); + public void AverageFilter_Works() => RunAverageFilterTest(); #if SUPPORTS_RUNTIME_INTRINSICS [Fact] - public void AverageInverse_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); + public void AverageFilter_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.AllowAll); [Fact] - public void AverageInverse_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); + public void AverageFilter_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunAverageFilterTest, HwIntrinsics.DisableHWIntrinsic); #endif } } From 4f6b807e389b7a2cdb20329065fe1d1d51654590 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 19:50:55 +0100 Subject: [PATCH 14/28] Fix average test data --- .../ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index a096c5eb6c..310f53c516 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -7,6 +7,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Png { + [Trait("Format", "Png")] public class PngDecoderFilterTests { private static void RunAverageFilterTest() @@ -18,7 +19,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 + 11, 11, 0, 226, 226, 226, 0, 255, 128, 234 }; byte[] previousScanline = @@ -28,7 +29,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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 + 160, 160, 255, 139, 128, 134 }; byte[] expected = @@ -38,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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, 255 + 140, 140, 255, 138, 6, 115 }; // act From c01001fddb1eee22d316ce8648db295a37a7f26c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 20:07:09 +0100 Subject: [PATCH 15/28] Add comment about pixel layout --- src/ImageSharp/Formats/Png/Filters/AverageFilter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 7747d14ea2..a99b93adb5 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -32,6 +32,9 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters // 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) { From e1f96f2647d6a9ccfef3a61c9d339381e7f3c4f9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 23 Feb 2022 21:40:51 +0100 Subject: [PATCH 16/28] Additional png decoder tests for average filter --- .../Codecs/Png/DecodeFilteredPng.cs | 4 ++-- tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs | 11 +++++++++++ tests/ImageSharp.Tests/TestImages.cs | 6 +++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs index 373b563c82..92eb4af07e 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs @@ -26,8 +26,8 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs this.filter1 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter1)); this.filter2 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter2)); this.filter3 = File.ReadAllBytes(TestImageFullPath(TestImages.Png.Filter4)); - this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3bpp)); - this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4bpp)); + this.averageFilter3bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter3BytesPerPixel)); + this.averageFilter4bpp = File.ReadAllBytes(TestImageFullPath(TestImages.Png.AverageFilter4BytesPerPixel)); } [Benchmark(Baseline = true, Description = "None-filtered PNG file")] diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 0af5d99950..7d47087990 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -111,6 +111,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba64)] + [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba64)] + 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.GrayA8Bit, PixelTypes.Rgba32)] [WithFile(TestImages.Png.Gray1BitTrans, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2f3bb49f61..ad50fc5c71 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -68,10 +68,10 @@ namespace SixLabors.ImageSharp.Tests public const string Filter0 = "Png/filter0.png"; public const string Filter1 = "Png/filter1.png"; public const string Filter2 = "Png/filter2.png"; - public const string AverageFilter3bpp = "Png/filter3.png"; + public const string AverageFilter3BytesPerPixel = "Png/filter3.png"; public const string Filter4 = "Png/filter4.png"; - public const string AverageFilter4bpp = "Png/AverageFilter4Bpp.png"; + public const string AverageFilter4BytesPerPixel = "Png/AverageFilter4Bpp.png"; // Paletted images also from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string PalettedTwoColor = "Png/basn3p01.png"; @@ -161,7 +161,7 @@ namespace SixLabors.ImageSharp.Tests { P1, Pd, Blur, Splash, Cross, Powerpoint, SplashInterlaced, Interlaced, - Filter0, Filter1, Filter2, AverageFilter3bpp, Filter4, + Filter0, Filter1, Filter2, AverageFilter3BytesPerPixel, Filter4, FilterVar, VimImage1, VimImage2, VersioningImage1, VersioningImage2, Ratio4x1, Ratio1x4 }; From 8716c51d3044c40eaa46c6406f1f581afd699112 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 11:53:50 +0100 Subject: [PATCH 17/28] Add SSE2 version of up filter --- .../Formats/Png/Filters/UpFilter.cs | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 7e0286991b..2d2a442a19 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,55 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters { DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); +#if SUPPORTS_RUNTIME_INTRINSICS + if (Sse2.IsSupported) + { + DecodeSse2(scanline, previousScanline); + } + else +#endif + { + DecodeScalar(scanline, previousScanline); + } + } + +#if SUPPORTS_RUNTIME_INTRINSICS + 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; + int 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 (int 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 +92,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) { From 0c8dbeae8c6854e75ccefa3902df85f01160f2dc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 13:58:16 +0100 Subject: [PATCH 18/28] Add Avx version of up filter --- .../Formats/Png/Filters/UpFilter.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 2d2a442a19..20a828e520 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -31,7 +31,11 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters DebugGuard.MustBeSameSized(scanline, previousScanline, nameof(scanline)); #if SUPPORTS_RUNTIME_INTRINSICS - if (Sse2.IsSupported) + if (Avx2.IsSupported) + { + DecodeAvx2(scanline, previousScanline); + } + else if (Sse2.IsSupported) { DecodeSse2(scanline, previousScanline); } @@ -43,6 +47,38 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } #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; + int 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 (int 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); From 849e866221556474e4790e18ae6898e43de7e2f2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 16:22:29 +0100 Subject: [PATCH 19/28] Add SSE2 version of sub filter --- .../Formats/Png/Filters/SubFilter.cs | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index c28b877e41..7985977548 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -21,12 +21,52 @@ 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; + int 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); @@ -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) { From 670105f17b5102e9d2e7a1370dc4efe0a04c69dd Mon Sep 17 00:00:00 2001 From: Brian Popow <38701097+brianpopow@users.noreply.github.com> Date: Thu, 24 Feb 2022 18:22:34 +0100 Subject: [PATCH 20/28] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- src/ImageSharp/Formats/Png/Filters/AverageFilter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index a99b93adb5..5d5c4a79d1 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); Vector128 d = Vector128.Zero; - var ones = Vector128.Create((byte)1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1); + var ones = Vector128.Create((byte)1); int rb = scanline.Length; int offset = 1; @@ -89,7 +89,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); - int x = 1; + nint x = 1; for (; x <= bytesPerPixel /* Note the <= because x starts at 1 */; ++x) { ref byte scan = ref Unsafe.Add(ref scanBaseRef, x); From 04219b5f74c39bd9134a10b12346f82a5b7affe8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 18:24:02 +0100 Subject: [PATCH 21/28] Use nint for offset --- src/ImageSharp/Formats/Png/Filters/AverageFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs index 5d5c4a79d1..44a16f154e 100644 --- a/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/AverageFilter.cs @@ -58,7 +58,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters var ones = Vector128.Create((byte)1); int rb = scanline.Length; - int offset = 1; + nint offset = 1; while (rb >= 4) { ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); From 8432b9fa4d76cfd13fad3221e77ab43f67ecc6cb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 18:29:07 +0100 Subject: [PATCH 22/28] Use nint for offset --- src/ImageSharp/Formats/Png/Filters/SubFilter.cs | 4 ++-- src/ImageSharp/Formats/Png/Filters/UpFilter.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs index 7985977548..eaa4dc0344 100644 --- a/src/ImageSharp/Formats/Png/Filters/SubFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/SubFilter.cs @@ -49,7 +49,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters Vector128 d = Vector128.Zero; int rb = scanline.Length; - int offset = 1; + nint offset = 1; while (rb >= 4) { ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); @@ -71,7 +71,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters 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) { diff --git a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs index 20a828e520..0d24d9c5d5 100644 --- a/src/ImageSharp/Formats/Png/Filters/UpFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/UpFilter.cs @@ -54,7 +54,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters // Up(x) + Prior(x) int rb = scanline.Length; - int offset = 1; + nint offset = 1; const int bytesPerBatch = 32; while (rb >= bytesPerBatch) { @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } // Handle left over. - for (int i = offset; i < scanline.Length; i++) + for (nint i = offset; i < scanline.Length; i++) { ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); byte above = Unsafe.Add(ref prevBaseRef, offset); @@ -86,7 +86,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters // Up(x) + Prior(x) int rb = scanline.Length; - int offset = 1; + nint offset = 1; const int bytesPerBatch = 16; while (rb >= bytesPerBatch) { @@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp.Formats.Png.Filters } // Handle left over. - for (int i = offset; i < scanline.Length; i++) + for (nint i = offset; i < scanline.Length; i++) { ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset); byte above = Unsafe.Add(ref prevBaseRef, offset); From 06843f2a7d81a00da7be04d615ef26dc0f5389e2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 24 Feb 2022 23:01:53 +0100 Subject: [PATCH 23/28] Add SSE version of paeth filter --- .../Formats/Png/Filters/PaethFilter.cs | 88 ++++++++++++++++++- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index 6a89a1122a..0553eb46a9 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) { From 88b75da62b2c74172f69930b555335bc59fdde9b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 25 Feb 2022 13:05:59 +0100 Subject: [PATCH 24/28] Add tests for png filters with and without intrinsics --- .../Formats/Png/PngDecoderFilterTests.cs | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs index 310f53c516..edfff19a4b 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderFilterTests.cs @@ -46,18 +46,134 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png AverageFilter.Decode(scanline, previousScanline, 4); // assert - Assert.Equal(scanline, expected); + 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 } } From 26a742eb92bdea04268069ca96bfe9db0b90f298 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 25 Feb 2022 13:41:38 +0100 Subject: [PATCH 25/28] Additional tests for decoding png's with filter --- .../Codecs/Png/DecodeFilteredPng.cs | 6 ++-- .../Formats/Png/PngDecoderTests.cs | 36 +++++++++++++++++-- tests/ImageSharp.Tests/TestImages.cs | 18 +++------- tests/Images/Input/Png/PaethFilter4Bpp.png | 3 ++ tests/Images/Input/Png/SubFilter4Bpp.png | 3 ++ 5 files changed, 48 insertions(+), 18 deletions(-) create mode 100644 tests/Images/Input/Png/PaethFilter4Bpp.png create mode 100644 tests/Images/Input/Png/SubFilter4Bpp.png diff --git a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs index 92eb4af07e..5f91a050ee 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Png/DecodeFilteredPng.cs @@ -23,9 +23,9 @@ namespace SixLabors.ImageSharp.Benchmarks.Codecs 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.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)); } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 7d47087990..752036126f 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -112,8 +112,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } [Theory] - [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba64)] - [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba64)] + [WithFile(TestImages.Png.AverageFilter3BytesPerPixel, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.AverageFilter4BytesPerPixel, PixelTypes.Rgba32)] public void Decode_WithAverageFilter(TestImageProvider provider) where TPixel : unmanaged, IPixel { @@ -122,6 +122,38 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png 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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index ad50fc5c71..9ea3c09f16 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -66,12 +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 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 Filter4 = "Png/filter4.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"; @@ -156,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, AverageFilter3BytesPerPixel, Filter4, - FilterVar, VimImage1, VimImage2, VersioningImage1, - VersioningImage2, Ratio4x1, Ratio1x4 - }; } public static class Jpeg diff --git a/tests/Images/Input/Png/PaethFilter4Bpp.png b/tests/Images/Input/Png/PaethFilter4Bpp.png new file mode 100644 index 0000000000..64c9f96ec8 --- /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 0000000000..d9f2c7fa25 --- /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 From 3f9331856e5ecaed01744cd88bdba781bafe440a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 25 Feb 2022 18:53:27 +0100 Subject: [PATCH 26/28] Add support for decoding gray tiff with jpeg compression --- .../GrayJpegSpectralConverter.cs | 29 +++++++++ .../Decompressors/JpegTiffCompression.cs | 59 +++++++++++++++---- .../Decompressors/RgbJpegSpectralConverter.cs | 1 - 3 files changed, 77 insertions(+), 12 deletions(-) create mode 100644 src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs new file mode 100644 index 0000000000..5b793c35de --- /dev/null +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/GrayJpegSpectralConverter.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; +using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors +{ + /// + /// Spectral converter for gray TIFF's which use the JPEG compression. + /// + /// The type of the pixel. + internal sealed class GrayJpegSpectralConverter : SpectralConverter + where TPixel : unmanaged, IPixel + { + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + public GrayJpegSpectralConverter(Configuration configuration) + : base(configuration) + { + } + + /// + protected override JpegColorConverterBase GetColorConverter(JpegFrame frame, IRawJpegData jpegData) => JpegColorConverterBase.GetConverter(JpegColorSpace.Grayscale, frame.Precision); + } +} diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index ce7820ccf9..f73fe508c5 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -9,7 +9,6 @@ using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors @@ -55,17 +54,43 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors { using var jpegDecoder = new JpegDecoderCore(this.configuration, new JpegDecoder()); - // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space. - // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB). - using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? - new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); - var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); - jpegDecoder.LoadTables(this.jpegTables, scanDecoder); - scanDecoder.ResetInterval = 0; - jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None); + switch (this.photometricInterpretation) + { + case TiffPhotometricInterpretation.BlackIsZero: + case TiffPhotometricInterpretation.WhiteIsZero: + { + using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration); + var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None); + jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); + scanDecoderGray.ResetInterval = 0; + jpegDecoder.ParseStream(stream, scanDecoderGray, CancellationToken.None); - // TODO: Should we pass through the CancellationToken from the tiff decoder? - CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None)); + // TODO: Should we pass through the CancellationToken from the tiff decoder? + CopyImageBytesToBuffer(buffer, spectralConverterGray.GetPixelBuffer(CancellationToken.None)); + break; + } + + // If the PhotometricInterpretation is YCbCr we explicitly assume the JPEG data is in RGB color space. + // There seems no other way to determine that the JPEG data is RGB colorspace (no APP14 marker, componentId's are not RGB). + case TiffPhotometricInterpretation.YCbCr: + case TiffPhotometricInterpretation.Rgb: + { + using SpectralConverter spectralConverter = this.photometricInterpretation == TiffPhotometricInterpretation.YCbCr ? + new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); + var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); + jpegDecoder.LoadTables(this.jpegTables, scanDecoder); + scanDecoder.ResetInterval = 0; + jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None); + + // TODO: Should we pass through the CancellationToken from the tiff decoder? + CopyImageBytesToBuffer(buffer, spectralConverter.GetPixelBuffer(CancellationToken.None)); + break; + } + + default: + TiffThrowHelper.ThrowNotSupported($"Jpeg compressed tiff with photometric interpretation {this.photometricInterpretation} is not supported"); + break; + } } else { @@ -86,6 +111,18 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors } } + private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pixelBuffer) + { + int offset = 0; + for (int y = 0; y < pixelBuffer.Height; y++) + { + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); + Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); + rgbBytes.CopyTo(buffer.Slice(offset)); + offset += rgbBytes.Length; + } + } + /// protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs index 001480542f..a83518064d 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/RgbJpegSpectralConverter.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Threading; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder.ColorConverters; using SixLabors.ImageSharp.PixelFormats; From 3dcb4db49a5034c8d04e5b841b84b12c1e38eb72 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 25 Feb 2022 18:56:47 +0100 Subject: [PATCH 27/28] Add test for gray tiff jpeg compressed --- tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Tiff/JpegCompressedGray.tiff | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 tests/Images/Input/Tiff/JpegCompressedGray.tiff diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index ea0544acf4..2748900668 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -370,6 +370,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [WithFile(RgbWithStripsJpegCompressed, PixelTypes.Rgba32)] [WithFile(YCbCrJpegCompressed, PixelTypes.Rgba32)] [WithFile(RgbJpegCompressedNoJpegTable, PixelTypes.Rgba32)] + [WithFile(GrayscaleJpegCompressed, PixelTypes.Rgba32)] public void TiffDecoder_CanDecode_JpegCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider, useExactComparer: false); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 5bce99ce12..4830a43083 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -772,6 +772,7 @@ namespace SixLabors.ImageSharp.Tests public const string GrayscaleDeflateMultistrip = "Tiff/grayscale_deflate_multistrip.tiff"; public const string GrayscaleUncompressed = "Tiff/grayscale_uncompressed.tiff"; + public const string GrayscaleJpegCompressed = "Tiff/JpegCompressedGray.tiff"; public const string PaletteDeflateMultistrip = "Tiff/palette_grayscale_deflate_multistrip.tiff"; public const string PaletteUncompressed = "Tiff/palette_uncompressed.tiff"; public const string RgbDeflate = "Tiff/rgb_deflate.tiff"; diff --git a/tests/Images/Input/Tiff/JpegCompressedGray.tiff b/tests/Images/Input/Tiff/JpegCompressedGray.tiff new file mode 100644 index 0000000000..e7feed15a8 --- /dev/null +++ b/tests/Images/Input/Tiff/JpegCompressedGray.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:868afd018d025ed7636f1155c1b1f64ba8a36153b56c7598e8dee18ce770cd5a +size 539660 From 2fbf56646c6cb1eb86572279d3525133ec20eef4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Feb 2022 19:13:36 +0100 Subject: [PATCH 28/28] Remove unnecessary setting ResetInterval to zero --- .../Tiff/Compression/Decompressors/JpegTiffCompression.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index f73fe508c5..cfbc32f4f6 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -62,7 +62,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors using SpectralConverter spectralConverterGray = new GrayJpegSpectralConverter(this.configuration); var scanDecoderGray = new HuffmanScanDecoder(stream, spectralConverterGray, CancellationToken.None); jpegDecoder.LoadTables(this.jpegTables, scanDecoderGray); - scanDecoderGray.ResetInterval = 0; jpegDecoder.ParseStream(stream, scanDecoderGray, CancellationToken.None); // TODO: Should we pass through the CancellationToken from the tiff decoder? @@ -79,7 +78,6 @@ namespace SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors new RgbJpegSpectralConverter(this.configuration) : new SpectralConverter(this.configuration); var scanDecoder = new HuffmanScanDecoder(stream, spectralConverter, CancellationToken.None); jpegDecoder.LoadTables(this.jpegTables, scanDecoder); - scanDecoder.ResetInterval = 0; jpegDecoder.ParseStream(stream, scanDecoder, CancellationToken.None); // TODO: Should we pass through the CancellationToken from the tiff decoder?