From 406240eec7e6b499dcee716ebb1b5bf7f21bd9e5 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 12 Mar 2022 13:58:34 +0100 Subject: [PATCH 01/30] Unrelated fix for compiler warnings --- .../Formats/Jpg/JpegDecoderTests.cs | 2 +- .../Memory/Buffer2DTests.SwapOrCopyContent.cs | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 7a24469597..ccb3e3fb97 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -21,7 +21,7 @@ using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Formats.Jpg { // TODO: Scatter test cases into multiple test classes - [Trait("Format", "Jpg")] + [Trait("Format", "Jpg")] public partial class JpegDecoderTests { public const PixelTypes CommonNonDefaultPixelTypes = PixelTypes.Rgba32 | PixelTypes.Argb32 | PixelTypes.Bgr24 | PixelTypes.RgbaVector; diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs index f58136f738..5b6938df11 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs @@ -13,13 +13,13 @@ namespace SixLabors.ImageSharp.Tests.Memory { public class SwapOrCopyContent { - private readonly TestMemoryAllocator MemoryAllocator = new TestMemoryAllocator(); + private readonly TestMemoryAllocator memoryAllocator = new TestMemoryAllocator(); [Fact] public void SwapOrCopyContent_WhenBothAllocated() { - using (Buffer2D a = this.MemoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean)) - using (Buffer2D b = this.MemoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean)) + using (Buffer2D a = this.memoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean)) + using (Buffer2D b = this.memoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean)) { a[1, 3] = 666; b[1, 3] = 444; @@ -46,7 +46,7 @@ namespace SixLabors.ImageSharp.Tests.Memory using var destData = MemoryGroup.Wrap(new int[100]); using var dest = new Buffer2D(destData, 10, 10); - using (Buffer2D source = this.MemoryAllocator.Allocate2D(10, 10, AllocationOptions.Clean)) + using (Buffer2D source = this.memoryAllocator.Allocate2D(10, 10, AllocationOptions.Clean)) { source[0, 0] = 1; dest[0, 0] = 2; @@ -68,9 +68,9 @@ namespace SixLabors.ImageSharp.Tests.Memory [Fact] public void WhenBothAreMemoryOwners_ShouldSwap() { - this.MemoryAllocator.BufferCapacityInBytes = sizeof(int) * 50; - using Buffer2D a = this.MemoryAllocator.Allocate2D(48, 2); - using Buffer2D b = this.MemoryAllocator.Allocate2D(50, 2); + this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * 50; + using Buffer2D a = this.memoryAllocator.Allocate2D(48, 2); + using Buffer2D b = this.memoryAllocator.Allocate2D(50, 2); Memory a0 = a.FastMemoryGroup[0]; Memory a1 = a.FastMemoryGroup[1]; @@ -90,8 +90,8 @@ namespace SixLabors.ImageSharp.Tests.Memory [Fact] public void WhenBothAreMemoryOwners_ShouldReplaceViews() { - using Buffer2D a = this.MemoryAllocator.Allocate2D(100, 1); - using Buffer2D b = this.MemoryAllocator.Allocate2D(100, 2); + using Buffer2D a = this.memoryAllocator.Allocate2D(100, 1); + using Buffer2D b = this.memoryAllocator.Allocate2D(100, 2); a.FastMemoryGroup[0].Span[42] = 1; b.FastMemoryGroup[0].Span[33] = 2; @@ -121,7 +121,7 @@ namespace SixLabors.ImageSharp.Tests.Memory using var destOwner = new TestMemoryManager(data); using var dest = new Buffer2D(MemoryGroup.Wrap(destOwner.Memory), 21, 1); - using Buffer2D source = this.MemoryAllocator.Allocate2D(21, 1); + using Buffer2D source = this.memoryAllocator.Allocate2D(21, 1); source.FastMemoryGroup[0].Span[10] = color; @@ -145,7 +145,7 @@ namespace SixLabors.ImageSharp.Tests.Memory using var destOwner = new TestMemoryManager(data); using var dest = new Buffer2D(MemoryGroup.Wrap(destOwner.Memory), 21, 1); - using Buffer2D source = this.MemoryAllocator.Allocate2D(22, 1); + using Buffer2D source = this.memoryAllocator.Allocate2D(22, 1); source.FastMemoryGroup[0].Span[10] = color; From 199a9e24311282cf6978b1f5ec6c326fea05fc6f Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 13 Mar 2022 10:05:16 +0100 Subject: [PATCH 02/30] Implement border wrapping modes --- .../Convolution/BorderWrappingMode.cs | 21 ++ .../Convolution/KernelSamplingMap.cs | 126 +++++-- .../Convolution/KernelSamplingMapTest.cs | 324 ++++++++++++++++++ 3 files changed, 433 insertions(+), 38 deletions(-) create mode 100644 src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs create mode 100644 tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs diff --git a/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs new file mode 100644 index 0000000000..0dd5e59439 --- /dev/null +++ b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Processing.Processors.Convolution +{ + /// + /// Wrapping mode for the border pixels in convolution processing. + /// + public enum BorderWrappingMode : byte + { + /// Repeat the border pixel value: aaaaaa|abcdefgh|hhhhhhh + Repeat = 0, + + /// Take values from the opposite edge: cdefgh|abcdefgh|abcdefg + Wrap = 1, + + /// Mirror the last few border values: fedcb|abcdefgh|gfedcb + /// Please note this mode doe not repeat the very border pixel, as this gives better image quality. + Mirror = 2 + } +} diff --git a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs index 904b599f7c..5c0a25befa 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs @@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// The convolution kernel. /// The source bounds. public void BuildSamplingOffsetMap(DenseMatrix kernel, Rectangle bounds) - => this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds); + => this.BuildSamplingOffsetMap(kernel.Rows, kernel.Columns, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); /// /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. @@ -40,6 +40,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// The width (number of columns) of the convolution kernel to use. /// The source bounds. public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds) + => this.BuildSamplingOffsetMap(kernelHeight, kernelWidth, bounds, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat); + + /// + /// Builds a map of the sampling offsets for the kernel clamped by the given bounds. + /// + /// The height (number of rows) of the convolution kernel to use. + /// The width (number of columns) of the convolution kernel to use. + /// The source bounds. + /// The wrapping mode on the horizontal borders. + /// The wrapping mode on the vertical borders. + public void BuildSamplingOffsetMap(int kernelHeight, int kernelWidth, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode) { this.yOffsets = this.allocator.Allocate(bounds.Height * kernelHeight); this.xOffsets = this.allocator.Allocate(bounds.Width * kernelWidth); @@ -49,43 +60,8 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution int minX = bounds.X; int maxX = bounds.Right - 1; - int radiusY = kernelHeight >> 1; - int radiusX = kernelWidth >> 1; - - // Calculate the y and x sampling offsets clamped to the given rectangle. - // While this isn't a hotpath we still dip into unsafe to avoid the span bounds - // checks as the can potentially be looping over large arrays. - Span ySpan = this.yOffsets.GetSpan(); - ref int ySpanBase = ref MemoryMarshal.GetReference(ySpan); - for (int row = 0; row < bounds.Height; row++) - { - int rowBase = row * kernelHeight; - for (int y = 0; y < kernelHeight; y++) - { - Unsafe.Add(ref ySpanBase, rowBase + y) = row + y + minY - radiusY; - } - } - - if (kernelHeight > 1) - { - Numerics.Clamp(ySpan, minY, maxY); - } - - Span xSpan = this.xOffsets.GetSpan(); - ref int xSpanBase = ref MemoryMarshal.GetReference(xSpan); - for (int column = 0; column < bounds.Width; column++) - { - int columnBase = column * kernelWidth; - for (int x = 0; x < kernelWidth; x++) - { - Unsafe.Add(ref xSpanBase, columnBase + x) = column + x + minX - radiusX; - } - } - - if (kernelWidth > 1) - { - Numerics.Clamp(xSpan, minX, maxX); - } + this.BuildOffsets(this.yOffsets, bounds.Height, kernelHeight, minY, maxY, yBorderMode); + this.BuildOffsets(this.xOffsets, bounds.Width, kernelWidth, minX, maxX, xBorderMode); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -105,5 +81,79 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution this.isDisposed = true; } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BuildOffsets(IMemoryOwner offsets, int boundsSize, int kernelSize, int min, int max, BorderWrappingMode borderMode) + { + int radius = kernelSize >> 1; + Span span = offsets.GetSpan(); + ref int spanBase = ref MemoryMarshal.GetReference(span); + for (int chunk = 0; chunk < boundsSize; chunk++) + { + int chunkBase = chunk * kernelSize; + for (int i = 0; i < kernelSize; i++) + { + Unsafe.Add(ref spanBase, chunkBase + i) = chunk + i + min - radius; + } + } + + this.CorrectBorder(span, kernelSize, min, max, borderMode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CorrectBorder(Span span, int kernelSize, int min, int max, BorderWrappingMode borderMode) + { + var affectedSize = (kernelSize >> 1) * kernelSize; + if (affectedSize > 0) + { + switch (borderMode) + { + case BorderWrappingMode.Repeat: + Numerics.Clamp(span.Slice(0, affectedSize), min, max); + Numerics.Clamp(span.Slice(span.Length - affectedSize), min, max); + break; + case BorderWrappingMode.Mirror: + for (int i = 0; i < affectedSize; i++) + { + var value = span[i]; + if (value < min) + { + span[i] = min - value + min; + } + } + + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + var value = span[i]; + if (value > max) + { + span[i] = max - value + max; + } + } + + break; + case BorderWrappingMode.Wrap: + for (int i = 0; i < affectedSize; i++) + { + var value = span[i]; + if (value < min) + { + span[i] = max - min + value + 1; + } + } + + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + var value = span[i]; + if (value > max) + { + span[i] = min + value - max - 1; + } + } + + break; + } + } + } } } diff --git a/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs new file mode 100644 index 0000000000..68af58b596 --- /dev/null +++ b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs @@ -0,0 +1,324 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Processing.Convolution +{ + [Trait("Category", "Processors")] + public class KernelSamplingMapTest + { + [Fact] + public void KernalSamplingMap_Kernel5Image7x7RepeatBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 0, 0, 0, 1, 2, + 0, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 6, + 4, 5, 6, 6, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7MirrorBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 2, 1, 0, 1, 2, + 1, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 5, + 4, 5, 6, 5, 4, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7WrapBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 5, 6, 0, 1, 2, + 6, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 0, + 4, 5, 6, 0, 1, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image9x9MirrorBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(1, 1, 9, 9); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 3, 2, 1, 2, 3, + 2, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 9, + 6, 7, 8, 9, 8, + 7, 8, 9, 8, 7, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image9x9WrapBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(1, 1, 9, 9); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 8, 9, 1, 2, 3, + 9, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 9, + 6, 7, 8, 9, 1, + 7, 8, 9, 1, 2, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7RepeatBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 2, 2, 2, 3, 4, + 2, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 8, + 6, 7, 8, 8, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7MirrorBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 4, 3, 2, 3, 4, + 3, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 7, + 6, 7, 8, 7, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel5Image7x7WrapBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 7, 8, 2, 3, 4, + 8, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 2, + 6, 7, 8, 2, 3, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7RepeatBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 0, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7MirrorBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 1, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 5, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7WrapBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 6, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 0, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7RepeatBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Repeat; + int[] expected = + { + 2, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7MirrorBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 3, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 7, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x7WrapBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Wrap; + int[] expected = + { + 8, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 2, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + + [Fact] + public void KernalSamplingMap_Kernel3Image7x5WrapBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 5); + var mode = BorderWrappingMode.Wrap; + int[] xExpected = + { + 8, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 2, + }; + int[] yExpected = + { + 6, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 2, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, xExpected, yExpected); + } + + private void AssertOffsets(Size kernelSize, Rectangle bounds, BorderWrappingMode xBorderMode, BorderWrappingMode yBorderMode, int[] xExpected, int[] yExpected) + { + // Arrange + var map = new KernelSamplingMap(Configuration.Default.MemoryAllocator); + + // Act + map.BuildSamplingOffsetMap(kernelSize.Height, kernelSize.Width, bounds, xBorderMode, yBorderMode); + + // Assert + var xOffsets = map.GetColumnOffsetSpan().ToArray(); + Assert.Equal(xExpected, xOffsets); + var yOffsets = map.GetRowOffsetSpan().ToArray(); + Assert.Equal(yExpected, yOffsets); + } + } +} From f336b80160f2579df6f263c74f68d7bb34179915 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 13 Mar 2022 11:37:09 +0100 Subject: [PATCH 03/30] Cache some variables --- .../Processors/Convolution/BorderWrappingMode.cs | 2 +- .../Processors/Convolution/KernelSamplingMap.cs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs index 0dd5e59439..1fd1020cc8 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Wrap = 1, /// Mirror the last few border values: fedcb|abcdefgh|gfedcb - /// Please note this mode doe not repeat the very border pixel, as this gives better image quality. + /// Please note this mode does not repeat the very border pixel, as this gives better image quality. Mirror = 2 } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs index 5c0a25befa..fde8bae3cb 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs @@ -104,6 +104,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution private void CorrectBorder(Span span, int kernelSize, int min, int max, BorderWrappingMode borderMode) { var affectedSize = (kernelSize >> 1) * kernelSize; + ref int spanBase = ref MemoryMarshal.GetReference(span); if (affectedSize > 0) { switch (borderMode) @@ -113,32 +114,35 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Numerics.Clamp(span.Slice(span.Length - affectedSize), min, max); break; case BorderWrappingMode.Mirror: + var min2 = min + min; for (int i = 0; i < affectedSize; i++) { var value = span[i]; if (value < min) { - span[i] = min - value + min; + span[i] = min2 - value; } } + var max2 = max + max; for (int i = span.Length - affectedSize; i < span.Length; i++) { var value = span[i]; if (value > max) { - span[i] = max - value + max; + span[i] = max2 - value; } } break; case BorderWrappingMode.Wrap: + var diff = max - min + 1; for (int i = 0; i < affectedSize; i++) { var value = span[i]; if (value < min) { - span[i] = max - min + value + 1; + span[i] = diff + value; } } @@ -147,7 +151,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution var value = span[i]; if (value > max) { - span[i] = min + value - max - 1; + span[i] = value - diff; } } From 8dd9d392be06cdd9f8b94139eb293a3a7ea0feaa Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Wed, 16 Mar 2022 17:15:19 +0100 Subject: [PATCH 04/30] Introduce Bounce mode --- .../Convolution/BorderWrappingMode.cs | 10 +- .../Convolution/KernelSamplingMap.cs | 22 ++++ .../Convolution/KernelSamplingMapTest.cs | 117 ++++++++++++++++-- 3 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs index 1fd1020cc8..e835fc748c 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BorderWrappingMode.cs @@ -14,8 +14,12 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// Take values from the opposite edge: cdefgh|abcdefgh|abcdefg Wrap = 1, - /// Mirror the last few border values: fedcb|abcdefgh|gfedcb - /// Please note this mode does not repeat the very border pixel, as this gives better image quality. - Mirror = 2 + /// Mirror the last few border values: fedcba|abcdefgh|hgfedcb + /// This Mode is similar to , but here the very border pixel is repeated. + Mirror = 2, + + /// Bounce off the border: fedcb|abcdefgh|gfedcb + /// This Mode is similar to , but here the very border pixel is not repeated. + Bounce = 3 } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs index fde8bae3cb..98a4ca3574 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/KernelSamplingMap.cs @@ -114,6 +114,28 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution Numerics.Clamp(span.Slice(span.Length - affectedSize), min, max); break; case BorderWrappingMode.Mirror: + var min2dec = min + min - 1; + for (int i = 0; i < affectedSize; i++) + { + var value = span[i]; + if (value < min) + { + span[i] = min2dec - value; + } + } + + var max2inc = max + max + 1; + for (int i = span.Length - affectedSize; i < span.Length; i++) + { + var value = span[i]; + if (value > max) + { + span[i] = max2inc - value; + } + } + + break; + case BorderWrappingMode.Bounce: var min2 = min + min; for (int i = 0; i < affectedSize; i++) { diff --git a/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs index 68af58b596..64e1ea2cbc 100644 --- a/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs +++ b/tests/ImageSharp.Tests/Processing/Convolution/KernelSamplingMapTest.cs @@ -29,11 +29,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution } [Fact] - public void KernalSamplingMap_Kernel5Image7x7MirrorBorder() + public void KernalSamplingMap_Kernel5Image7x7BounceBorder() { var kernelSize = new Size(5, 5); var bounds = new Rectangle(0, 0, 7, 7); - var mode = BorderWrappingMode.Mirror; + var mode = BorderWrappingMode.Bounce; int[] expected = { 2, 1, 0, 1, 2, @@ -47,6 +47,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); } + [Fact] + public void KernalSamplingMap_Kernel5Image7x7MirrorBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 1, 0, 0, 1, 2, + 0, 0, 1, 2, 3, + 0, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 6, + 4, 5, 6, 6, 5, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + [Fact] public void KernalSamplingMap_Kernel5Image7x7WrapBorder() { @@ -67,11 +86,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution } [Fact] - public void KernalSamplingMap_Kernel5Image9x9MirrorBorder() + public void KernalSamplingMap_Kernel5Image9x9BounceBorder() { var kernelSize = new Size(5, 5); var bounds = new Rectangle(1, 1, 9, 9); - var mode = BorderWrappingMode.Mirror; + var mode = BorderWrappingMode.Bounce; int[] expected = { 3, 2, 1, 2, 3, @@ -87,6 +106,27 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); } + [Fact] + public void KernalSamplingMap_Kernel5Image9x9MirrorBorder() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(1, 1, 9, 9); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 2, 1, 1, 2, 3, + 1, 1, 2, 3, 4, + 1, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 9, + 6, 7, 8, 9, 9, + 7, 8, 9, 9, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + [Fact] public void KernalSamplingMap_Kernel5Image9x9WrapBorder() { @@ -128,11 +168,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution } [Fact] - public void KernalSamplingMap_Kernel5Image7x7MirrorBorderTile() + public void KernalSamplingMap_Kernel5Image7x7BounceBorderTile() { var kernelSize = new Size(5, 5); var bounds = new Rectangle(2, 2, 7, 7); - var mode = BorderWrappingMode.Mirror; + var mode = BorderWrappingMode.Bounce; int[] expected = { 4, 3, 2, 3, 4, @@ -146,6 +186,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); } + [Fact] + public void KernalSamplingMap_Kernel5Image7x7MirrorBorderTile() + { + var kernelSize = new Size(5, 5); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 3, 2, 2, 3, 4, + 2, 2, 3, 4, 5, + 2, 3, 4, 5, 6, + 3, 4, 5, 6, 7, + 4, 5, 6, 7, 8, + 5, 6, 7, 8, 8, + 6, 7, 8, 8, 7, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + [Fact] public void KernalSamplingMap_Kernel5Image7x7WrapBorderTile() { @@ -185,11 +244,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution } [Fact] - public void KernalSamplingMap_Kernel3Image7x7MirrorBorder() + public void KernalSamplingMap_Kernel3Image7x7BounceBorder() { var kernelSize = new Size(3, 3); var bounds = new Rectangle(0, 0, 7, 7); - var mode = BorderWrappingMode.Mirror; + var mode = BorderWrappingMode.Bounce; int[] expected = { 1, 0, 1, @@ -203,6 +262,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); } + [Fact] + public void KernalSamplingMap_Kernel3Image7x7MirrorBorder() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(0, 0, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 0, 0, 1, + 0, 1, 2, + 1, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 6, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + [Fact] public void KernalSamplingMap_Kernel3Image7x7WrapBorder() { @@ -242,11 +320,11 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution } [Fact] - public void KernalSamplingMap_Kernel3Image7MirrorBorderTile() + public void KernalSamplingMap_Kernel3Image7BounceBorderTile() { var kernelSize = new Size(3, 3); var bounds = new Rectangle(2, 2, 7, 7); - var mode = BorderWrappingMode.Mirror; + var mode = BorderWrappingMode.Bounce; int[] expected = { 3, 2, 3, @@ -260,6 +338,25 @@ namespace SixLabors.ImageSharp.Tests.Processing.Convolution this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); } + [Fact] + public void KernalSamplingMap_Kernel3Image7MirrorBorderTile() + { + var kernelSize = new Size(3, 3); + var bounds = new Rectangle(2, 2, 7, 7); + var mode = BorderWrappingMode.Mirror; + int[] expected = + { + 2, 2, 3, + 2, 3, 4, + 3, 4, 5, + 4, 5, 6, + 5, 6, 7, + 6, 7, 8, + 7, 8, 8, + }; + this.AssertOffsets(kernelSize, bounds, mode, mode, expected, expected); + } + [Fact] public void KernalSamplingMap_Kernel3Image7x7WrapBorderTile() { From 223b665fc6520e5a8eff6394fa6fba28ef17fdcb Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Fri, 25 Mar 2022 12:25:50 +0100 Subject: [PATCH 05/30] Apply BorderWrappingMode to Gaussian Blur/Sharpen and BoxBlur --- .../Convolution/BoxBlurExtensions.cs | 21 +++++++++ .../Convolution/GaussianBlurExtensions.cs | 21 +++++++++ .../Convolution/GaussianSharpenExtensions.cs | 21 +++++++++ .../Convolution/BoxBlurProcessor.cs | 29 +++++++++++- .../Convolution/BoxBlurProcessor{TPixel}.cs | 36 ++++++++++++++- .../Convolution2PassProcessor{TPixel}.cs | 20 +++++++- .../Convolution/GaussianBlurProcessor.cs | 46 ++++++++++++++++++- .../GaussianBlurProcessor{TPixel}.cs | 36 ++++++++++++++- .../Convolution/GaussianSharpenProcessor.cs | 46 ++++++++++++++++++- .../GaussianSharpenProcessor{TPixel}.cs | 36 ++++++++++++++- 10 files changed, 302 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs index f891ffa97c..a7f93e23ec 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/BoxBlurExtensions.cs @@ -39,5 +39,26 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle) => source.ApplyProcessor(new BoxBlurProcessor(radius), rectangle); + + /// + /// Applies a box blur to the image. + /// + /// The image this method extends. + /// The 'radius' value representing the size of the area to sample. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext BoxBlur(this IImageProcessingContext source, int radius, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + var processor = new BoxBlurProcessor(radius, borderWrapModeX, borderWrapModeY); + return source.ApplyProcessor(processor, rectangle); + } } } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs index bd4fb716d4..49af591e98 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/GaussianBlurExtensions.cs @@ -39,5 +39,26 @@ namespace SixLabors.ImageSharp.Processing /// The to allow chaining of operations. public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle) => source.ApplyProcessor(new GaussianBlurProcessor(sigma), rectangle); + + /// + /// Applies a Gaussian blur to the image. + /// + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext GaussianBlur(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + var processor = new GaussianBlurProcessor(sigma, borderWrapModeX, borderWrapModeY); + return source.ApplyProcessor(processor, rectangle); + } } } diff --git a/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs b/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs index f5b8798f46..5ac9d6909c 100644 --- a/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/Convolution/GaussianSharpenExtensions.cs @@ -42,5 +42,26 @@ namespace SixLabors.ImageSharp.Processing float sigma, Rectangle rectangle) => source.ApplyProcessor(new GaussianSharpenProcessor(sigma), rectangle); + + /// + /// Applies a Gaussian sharpening filter to the image. + /// + /// The image this method extends. + /// The 'sigma' value representing the weight of the blur. + /// + /// The structure that specifies the portion of the image object to alter. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + /// The to allow chaining of operations. + public static IImageProcessingContext GaussianSharpen(this IImageProcessingContext source, float sigma, Rectangle rectangle, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + { + var processor = new GaussianSharpenProcessor(sigma, borderWrapModeX, borderWrapModeY); + return source.ApplyProcessor(processor, rectangle); + } } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs index da6b967181..a622739fd4 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor.cs @@ -21,9 +21,24 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// /// The 'radius' value representing the size of the area to sample. /// - public BoxBlurProcessor(int radius) + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public BoxBlurProcessor(int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) { this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// + public BoxBlurProcessor(int radius) + : this(radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { } /// @@ -39,9 +54,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public int Radius { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - => new BoxBlurProcessor(configuration, this, source, sourceRectangle); + => new BoxBlurProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs index 5beadb0cee..ceebdf15aa 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BoxBlurProcessor{TPixel}.cs @@ -27,15 +27,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution this.Kernel = CreateBoxKernel(kernelSize); } + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public BoxBlurProcessor( + Configuration configuration, + BoxBlurProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = CreateBoxKernel(kernelSize); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + /// /// Gets the 1D convolution kernel. /// public float[] Kernel { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle); + using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index fa58422dc6..2fc0a5fe87 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -26,16 +26,22 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// Whether the convolution filter is applied to alpha as well as the color channels. /// The source for the current processor instance. /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. public Convolution2PassProcessor( Configuration configuration, float[] kernel, bool preserveAlpha, Image source, - Rectangle sourceRectangle) + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) : base(configuration, source, sourceRectangle) { this.Kernel = kernel; this.PreserveAlpha = preserveAlpha; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -48,6 +54,16 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public bool PreserveAlpha { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { @@ -63,7 +79,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution // the two 1D kernels represent, and reuse it across both convolution steps, like in the bokeh blur. using var mapXY = new KernelSamplingMap(this.Configuration.MemoryAllocator); - mapXY.BuildSamplingOffsetMap(this.Kernel.Length, this.Kernel.Length, interest); + mapXY.BuildSamplingOffsetMap(this.Kernel.Length, this.Kernel.Length, interest, this.BorderWrapModeX, this.BorderWrapModeY); // Horizontal convolution var horizontalOperation = new HorizontalConvolutionRowOperation( diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs index 1fa65b62cd..3af9791dcb 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor.cs @@ -32,6 +32,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { } + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianBlurProcessor(float sigma, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma), borderWrapModeX, borderWrapModeY) + { + } + /// /// Initializes a new instance of the class. /// @@ -54,9 +65,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// This should be at least twice the sigma value. /// public GaussianBlurProcessor(float sigma, int radius) + : this(sigma, radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + public GaussianBlurProcessor(float sigma, int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) { this.Sigma = sigma; this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -69,9 +103,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public int Radius { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - => new GaussianBlurProcessor(configuration, this, source, sourceRectangle); + => new GaussianBlurProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs index 4ade01f914..16b05b8bb0 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianBlurProcessor{TPixel}.cs @@ -30,15 +30,49 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution this.Kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, definition.Sigma); } + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianBlurProcessor( + Configuration configuration, + GaussianBlurProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) + : base(configuration, source, sourceRectangle) + { + int kernelSize = (definition.Radius * 2) + 1; + this.Kernel = ConvolutionProcessorHelpers.CreateGaussianBlurKernel(kernelSize, definition.Sigma); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; + } + /// /// Gets the 1D convolution kernel. /// public float[] Kernel { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle); + using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs index 7e1f029066..98c897c21e 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor.cs @@ -32,6 +32,17 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution { } + /// + /// Initializes a new instance of the class. + /// + /// The 'sigma' value representing the weight of the blur. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianSharpenProcessor(float sigma, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) + : this(sigma, ConvolutionProcessorHelpers.GetDefaultGaussianRadius(sigma), borderWrapModeX, borderWrapModeY) + { + } + /// /// Initializes a new instance of the class. /// @@ -54,9 +65,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// This should be at least twice the sigma value. /// public GaussianSharpenProcessor(float sigma, int radius) + : this(sigma, radius, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// The 'sigma' value representing the weight of the blur. + /// + /// + /// The 'radius' value representing the size of the area to sample. + /// This should be at least twice the sigma value. + /// + /// + /// The to use when mapping the pixels outside of the border, in X direction. + /// + /// + /// The to use when mapping the pixels outside of the border, in Y direction. + /// + public GaussianSharpenProcessor(float sigma, int radius, BorderWrappingMode borderWrapModeX, BorderWrappingMode borderWrapModeY) { this.Sigma = sigma; this.Radius = radius; + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -69,9 +103,19 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public int Radius { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// public IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle) where TPixel : unmanaged, IPixel - => new GaussianSharpenProcessor(configuration, this, source, sourceRectangle); + => new GaussianSharpenProcessor(configuration, this, source, sourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs index 73aaaec188..bddaab233b 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/GaussianSharpenProcessor{TPixel}.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// Initializes a new instance of the class. /// /// The configuration which allows altering default behaviour or extending the library. - /// The defining the processor parameters. + /// The defining the processor parameters. /// The source for the current processor instance. /// The source area to process for the current processor instance. public GaussianSharpenProcessor( @@ -24,10 +24,32 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution GaussianSharpenProcessor definition, Image source, Rectangle sourceRectangle) + : this(configuration, definition, source, sourceRectangle, BorderWrappingMode.Repeat, BorderWrappingMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The defining the processor parameters. + /// The source for the current processor instance. + /// The source area to process for the current processor instance. + /// The to use when mapping the pixels outside of the border, in X direction. + /// The to use when mapping the pixels outside of the border, in Y direction. + public GaussianSharpenProcessor( + Configuration configuration, + GaussianSharpenProcessor definition, + Image source, + Rectangle sourceRectangle, + BorderWrappingMode borderWrapModeX, + BorderWrappingMode borderWrapModeY) : base(configuration, source, sourceRectangle) { int kernelSize = (definition.Radius * 2) + 1; this.Kernel = ConvolutionProcessorHelpers.CreateGaussianSharpenKernel(kernelSize, definition.Sigma); + this.BorderWrapModeX = borderWrapModeX; + this.BorderWrapModeY = borderWrapModeY; } /// @@ -35,10 +57,20 @@ namespace SixLabors.ImageSharp.Processing.Processors.Convolution /// public float[] Kernel { get; } + /// + /// Gets the to use when mapping the pixels outside of the border, in X direction. + /// + public BorderWrappingMode BorderWrapModeX { get; } + + /// + /// Gets the to use when mapping the pixels outside of the border, in Y direction. + /// + public BorderWrappingMode BorderWrapModeY { get; } + /// protected override void OnFrameApply(ImageFrame source) { - using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle); + using var processor = new Convolution2PassProcessor(this.Configuration, this.Kernel, false, this.Source, this.SourceRectangle, this.BorderWrapModeX, this.BorderWrapModeY); processor.Apply(source); } From d3936e2d3ff5342df8723244bc89479048a598e3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 27 Mar 2022 22:47:44 +1100 Subject: [PATCH 06/30] Add check for App1 XMP marker length --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index ef4e3ffac2..51e0675754 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -722,7 +722,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(0, ExifMarkerLength))) { - int remainingXmpMarkerBytes = XmpMarkerLength - ExifMarkerLength; + const int remainingXmpMarkerBytes = XmpMarkerLength - ExifMarkerLength; + if (remaining < remainingXmpMarkerBytes || this.IgnoreMetadata) + { + // Skip the application header length. + stream.Skip(remaining); + return; + } + stream.Read(this.temp, ExifMarkerLength, remainingXmpMarkerBytes); remaining -= remainingXmpMarkerBytes; if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker)) From 96f6fd45a316e33b78026c8c40868928643775ef Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 14:17:32 +0200 Subject: [PATCH 07/30] Add check, if enough data is read in LoadTables --- .../Formats/Jpeg/JpegDecoderCore.cs | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 51e0675754..41effc865d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -232,7 +232,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg using var stream = new BufferedReadStream(this.Configuration, ms); // Check for the Start Of Image marker. - stream.Read(this.markerBuffer, 0, 2); + int bytesRead = stream.Read(this.markerBuffer, 0, 2); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read the SOI marker"); + } + var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { @@ -240,7 +245,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } // Read next marker. - stream.Read(this.markerBuffer, 0, 2); + bytesRead = stream.Read(this.markerBuffer, 0, 2); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); + } + byte marker = this.markerBuffer[1]; fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); @@ -273,7 +283,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } // Read next marker. - stream.Read(this.markerBuffer, 0, 2); + bytesRead = stream.Read(this.markerBuffer, 0, 2); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); + } + fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); } } From 588a6d444a2aec33e4d93b65d2def63cd8a7ba2c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 15:18:59 +0200 Subject: [PATCH 08/30] Fix lossy image with not enough exif bytes --- tests/Images/Input/Webp/exif_lossy.webp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Images/Input/Webp/exif_lossy.webp b/tests/Images/Input/Webp/exif_lossy.webp index 35e454b96f..5d6db3800f 100644 --- a/tests/Images/Input/Webp/exif_lossy.webp +++ b/tests/Images/Input/Webp/exif_lossy.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdf4e9b20af4168f4177d33f7f502906343bbaaae2af9b90e1531bd4452b317b -size 40765 +oid sha256:5c53967bfefcfece8cd4411740c1c394e75864ca61a7a9751df3b28e727c0205 +size 68646 From f820b0b965716de398974a23a02ac95616580522 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 15:21:30 +0200 Subject: [PATCH 09/30] ParseOptional chunks now checks, if enough data is read --- .../Formats/Webp/WebpDecoderCore.cs | 29 ++++++++++++++++--- .../Formats/Webp/WebpThrowHelper.cs | 7 +++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 9d18e5d821..412ec2760a 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -426,6 +426,7 @@ namespace SixLabors.ImageSharp.Formats.Webp /// The webp image features. private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features) { + int bytesRead; switch (chunkType) { case WebpChunkType.Iccp: @@ -437,7 +438,12 @@ namespace SixLabors.ImageSharp.Formats.Webp else { byte[] iccpData = new byte[iccpChunkSize]; - this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); + bytesRead = this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); + if (bytesRead != iccpChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); + } + var profile = new IccProfile(iccpData); if (profile.CheckIsValid()) { @@ -456,7 +462,12 @@ namespace SixLabors.ImageSharp.Formats.Webp else { byte[] exifData = new byte[exifChunkSize]; - this.currentStream.Read(exifData, 0, (int)exifChunkSize); + bytesRead = this.currentStream.Read(exifData, 0, (int)exifChunkSize); + if (bytesRead != exifChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the exif chunk"); + } + var profile = new ExifProfile(exifData); this.Metadata.ExifProfile = profile; } @@ -472,7 +483,12 @@ namespace SixLabors.ImageSharp.Formats.Webp else { byte[] xmpData = new byte[xmpChunkSize]; - this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); + bytesRead = this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); + if (bytesRead != xmpChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the xmp chunk"); + } + var profile = new XmpProfile(xmpData); this.Metadata.XmpProfile = profile; } @@ -488,7 +504,12 @@ namespace SixLabors.ImageSharp.Formats.Webp features.AlphaChunkHeader = (byte)this.currentStream.ReadByte(); int alphaDataSize = (int)(alphaChunkSize - 1); features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); - this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); + bytesRead = this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); + if (bytesRead != alphaDataSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha chunk"); + } + break; default: WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); diff --git a/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs b/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs index fffdd34101..5f063fd11a 100644 --- a/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs +++ b/src/ImageSharp/Formats/Webp/WebpThrowHelper.cs @@ -8,6 +8,13 @@ namespace SixLabors.ImageSharp.Formats.Webp { internal static class WebpThrowHelper { + /// + /// Cold path optimization for throwing 's. + /// + /// The error message for the exception. + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + /// /// Cold path optimization for throwing -s /// From 853b222af50851a9d46f73df0042ecc88b467bba Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 15:30:15 +0200 Subject: [PATCH 10/30] Add test for lossy with exif which does not contain enough data --- .../Formats/WebP/WebpMetaDataTests.cs | 12 ++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Webp/exif_lossy_not_enough_data.webp | 3 +++ 3 files changed, 16 insertions(+) create mode 100644 tests/Images/Input/Webp/exif_lossy_not_enough_data.webp diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index eaa7fb5646..499b0579b6 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -150,5 +150,17 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp Assert.NotNull(actualExif); Assert.Equal(expectedExif.Values.Count, actualExif.Values.Count); } + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)] + public void WebpDecoder_ThrowInvalidImageContentException_OnWithInvalidExifData(TestImageProvider provider) + where TPixel : unmanaged, IPixel => + Assert.Throws( + () => + { + using (provider.GetImage(WebpDecoder)) + { + } + }); } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 07aff6fc12..ac060fd99f 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -635,6 +635,7 @@ namespace SixLabors.ImageSharp.Tests { public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp"; + public const string WithExifNotEnoughData = "Webp/exif_lossy_not_enough_data.webp"; public const string WithIccp = "Webp/lossy_with_iccp.webp"; public const string WithXmp = "Webp/xmp_lossy.webp"; public const string BikeSmall = "Webp/bike_lossless_small.webp"; diff --git a/tests/Images/Input/Webp/exif_lossy_not_enough_data.webp b/tests/Images/Input/Webp/exif_lossy_not_enough_data.webp new file mode 100644 index 0000000000..35e454b96f --- /dev/null +++ b/tests/Images/Input/Webp/exif_lossy_not_enough_data.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdf4e9b20af4168f4177d33f7f502906343bbaaae2af9b90e1531bd4452b317b +size 40765 From 5d04734c324a0930ff43f90b9f236a5e8d39e247 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 18:59:04 +0200 Subject: [PATCH 11/30] Add checks in ReadVp8Header() to make sure enough data is read --- .../Formats/Webp/WebpDecoderCore.cs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 412ec2760a..9d83591c36 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -272,7 +272,12 @@ namespace SixLabors.ImageSharp.Formats.Webp this.webpMetadata.FileFormat = WebpFileFormatType.Lossy; // VP8 data size (not including this 4 bytes). - this.currentStream.Read(this.buffer, 0, 4); + int bytesRead = this.currentStream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 data size"); + } + uint dataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); // remaining counts the available image data payload. @@ -284,7 +289,12 @@ namespace SixLabors.ImageSharp.Formats.Webp // - A 3-bit version number. // - A 1-bit show_frame flag. // - A 19-bit field containing the size of the first data partition in bytes. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 frame tag"); + } + uint frameTag = (uint)(this.buffer[0] | (this.buffer[1] << 8) | (this.buffer[2] << 16)); remaining -= 3; bool isNoKeyFrame = (frameTag & 0x1) == 1; @@ -312,13 +322,23 @@ namespace SixLabors.ImageSharp.Formats.Webp } // Check for VP8 magic bytes. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the VP8 magic bytes"); + } + if (!this.buffer.AsSpan(0, 3).SequenceEqual(WebpConstants.Vp8HeaderMagicBytes)) { WebpThrowHelper.ThrowImageFormatException("VP8 magic bytes not found"); } - this.currentStream.Read(this.buffer, 0, 4); + bytesRead = this.currentStream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the image width and height"); + } + uint tmp = (uint)BinaryPrimitives.ReadInt16LittleEndian(this.buffer); uint width = tmp & 0x3fff; sbyte xScale = (sbyte)(tmp >> 6); From 8cc5a6b6cad3bccd6b7a04855428b8f78ce0a517 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 19:14:42 +0200 Subject: [PATCH 12/30] Remove code duplication when reading the profile data --- .../Formats/Webp/WebpDecoderCore.cs | 145 ++++++++++-------- 1 file changed, 85 insertions(+), 60 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 9d83591c36..2a840ba709 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -450,68 +450,17 @@ namespace SixLabors.ImageSharp.Formats.Webp switch (chunkType) { case WebpChunkType.Iccp: - uint iccpChunkSize = this.ReadChunkSize(); - if (this.IgnoreMetadata) - { - this.currentStream.Skip((int)iccpChunkSize); - } - else - { - byte[] iccpData = new byte[iccpChunkSize]; - bytesRead = this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); - if (bytesRead != iccpChunkSize) - { - WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); - } - - var profile = new IccProfile(iccpData); - if (profile.CheckIsValid()) - { - this.Metadata.IccProfile = profile; - } - } + this.ReadIccProfile(); break; case WebpChunkType.Exif: - uint exifChunkSize = this.ReadChunkSize(); - if (this.IgnoreMetadata) - { - this.currentStream.Skip((int)exifChunkSize); - } - else - { - byte[] exifData = new byte[exifChunkSize]; - bytesRead = this.currentStream.Read(exifData, 0, (int)exifChunkSize); - if (bytesRead != exifChunkSize) - { - WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the exif chunk"); - } - - var profile = new ExifProfile(exifData); - this.Metadata.ExifProfile = profile; - } + this.ReadExifProfile(); break; case WebpChunkType.Xmp: - uint xmpChunkSize = this.ReadChunkSize(); - if (this.IgnoreMetadata) - { - this.currentStream.Skip((int)xmpChunkSize); - } - else - { - byte[] xmpData = new byte[xmpChunkSize]; - bytesRead = this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); - if (bytesRead != xmpChunkSize) - { - WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the xmp chunk"); - } - - var profile = new XmpProfile(xmpData); - this.Metadata.XmpProfile = profile; - } + this.ReadXmpProfile(); break; @@ -555,22 +504,98 @@ namespace SixLabors.ImageSharp.Formats.Webp { // Read chunk header. WebpChunkType chunkType = this.ReadChunkType(); - uint chunkLength = this.ReadChunkSize(); - if (chunkType == WebpChunkType.Exif && this.Metadata.ExifProfile == null) { - byte[] exifData = new byte[chunkLength]; - this.currentStream.Read(exifData, 0, (int)chunkLength); - this.Metadata.ExifProfile = new ExifProfile(exifData); + this.ReadExifProfile(); + } + else if (chunkType == WebpChunkType.Xmp && this.Metadata.XmpProfile == null) + { + this.ReadXmpProfile(); } else { - // Skip XMP chunk data or any duplicate EXIF chunk. + // Skip duplicate XMP or EXIF chunk. + uint chunkLength = this.ReadChunkSize(); this.currentStream.Skip((int)chunkLength); } } } + /// + /// Reads the EXIF profile from the stream. + /// + private void ReadExifProfile() + { + uint exifChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)exifChunkSize); + } + else + { + byte[] exifData = new byte[exifChunkSize]; + int bytesRead = this.currentStream.Read(exifData, 0, (int)exifChunkSize); + if (bytesRead != exifChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the exif chunk"); + } + + var profile = new ExifProfile(exifData); + this.Metadata.ExifProfile = profile; + } + } + + /// + /// Reads the XMP profile the stream. + /// + private void ReadXmpProfile() + { + uint xmpChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)xmpChunkSize); + } + else + { + byte[] xmpData = new byte[xmpChunkSize]; + int bytesRead = this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); + if (bytesRead != xmpChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the xmp chunk"); + } + + var profile = new XmpProfile(xmpData); + this.Metadata.XmpProfile = profile; + } + } + + /// + /// Reads the ICCP chunk from the stream. + /// + private void ReadIccProfile() + { + uint iccpChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)iccpChunkSize); + } + else + { + byte[] iccpData = new byte[iccpChunkSize]; + int bytesRead = this.currentStream.Read(iccpData, 0, (int)iccpChunkSize); + if (bytesRead != iccpChunkSize) + { + WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the iccp chunk"); + } + + var profile = new IccProfile(iccpData); + if (profile.CheckIsValid()) + { + this.Metadata.IccProfile = profile; + } + } + } + /// /// Identifies the chunk type from the chunk. /// From 8e4129a7335ebb2a1a1f977654eeb7db6389cd1c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 19:20:14 +0200 Subject: [PATCH 13/30] Make sure enough data is read in ReadVp8XHeader() --- .../Formats/Webp/WebpDecoderCore.cs | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 2a840ba709..91594a6740 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -190,7 +190,11 @@ namespace SixLabors.ImageSharp.Formats.Webp uint fileSize = this.ReadChunkSize(); // The first byte contains information about the image features used. - byte imageFeatures = (byte)this.currentStream.ReadByte(); + int imageFeatures = this.currentStream.ReadByte(); + if (imageFeatures == -1) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8X header doe not contain enough data"); + } // The first two bit of it are reserved and should be 0. if (imageFeatures >> 6 != 0) @@ -214,19 +218,34 @@ namespace SixLabors.ImageSharp.Formats.Webp features.Animation = (imageFeatures & (1 << 1)) != 0; // 3 reserved bytes should follow which are supposed to be zero. - this.currentStream.Read(this.buffer, 0, 3); + int bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8X header does not contain enough data"); + } + if (this.buffer[0] != 0 || this.buffer[1] != 0 || this.buffer[2] != 0) { WebpThrowHelper.ThrowImageFormatException("reserved bytes should be zero"); } // 3 bytes for the width. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the width"); + } + this.buffer[3] = 0; uint width = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; // 3 bytes for the height. - this.currentStream.Read(this.buffer, 0, 3); + bytesRead = this.currentStream.Read(this.buffer, 0, 3); + if (bytesRead != 3) + { + WebpThrowHelper.ThrowInvalidImageContentException("VP8 header does not contain enough data to read the height"); + } + this.buffer[3] = 0; uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; From aac680fea60968320220c09492d8aa6216910295 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 19:28:03 +0200 Subject: [PATCH 14/30] Add check, if enough data was read for progressive scan decoding data --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 41effc865d..56bdca5f8d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -1334,8 +1334,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg component.ACHuffmanTableId = acTableIndex; } - // 3 bytes: Progressive scan decoding data - stream.Read(this.temp, 0, 3); + // 3 bytes: Progressive scan decoding data. + int bytesRead = stream.Read(this.temp, 0, 3); + if (bytesRead != 3) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read progressive scan decoding data"); + } int spectralStart = this.temp[0]; this.scanDecoder.SpectralStart = spectralStart; From e6ad467503ed31af90d7a55920a021e0c8c723ef Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 27 Mar 2022 19:29:12 +0200 Subject: [PATCH 15/30] Add check in ReadUint16 for enough data --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 56bdca5f8d..e2d6cfcd29 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -1362,7 +1362,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg [MethodImpl(InliningOptions.ShortMethod)] private ushort ReadUint16(BufferedReadStream stream) { - stream.Read(this.markerBuffer, 0, 2); + int bytesRead = stream.Read(this.markerBuffer, 0, 2); + if (bytesRead != 2) + { + JpegThrowHelper.ThrowInvalidImageContentException("jpeg stream does not contain enough data, could not read ushort."); + } + return BinaryPrimitives.ReadUInt16BigEndian(this.markerBuffer); } } From d22e50c0d0b01b75ca02396d745617f7ad81d256 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 28 Mar 2022 12:22:58 +0200 Subject: [PATCH 16/30] Ignore invalid EXIF or XMP chunks --- .../Formats/Webp/WebpDecoderCore.cs | 12 +++++------- .../Formats/WebP/WebpMetaDataTests.cs | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 91594a6740..9e709236c3 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -465,22 +465,18 @@ namespace SixLabors.ImageSharp.Formats.Webp /// The webp image features. private void ParseOptionalExtendedChunks(WebpChunkType chunkType, WebpFeatures features) { - int bytesRead; switch (chunkType) { case WebpChunkType.Iccp: this.ReadIccProfile(); - break; case WebpChunkType.Exif: this.ReadExifProfile(); - break; case WebpChunkType.Xmp: this.ReadXmpProfile(); - break; case WebpChunkType.Animation: @@ -492,7 +488,7 @@ namespace SixLabors.ImageSharp.Formats.Webp features.AlphaChunkHeader = (byte)this.currentStream.ReadByte(); int alphaDataSize = (int)(alphaChunkSize - 1); features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); - bytesRead = this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); + int bytesRead = this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); if (bytesRead != alphaDataSize) { WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the alpha chunk"); @@ -556,7 +552,8 @@ namespace SixLabors.ImageSharp.Formats.Webp int bytesRead = this.currentStream.Read(exifData, 0, (int)exifChunkSize); if (bytesRead != exifChunkSize) { - WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the exif chunk"); + // Ignore invalid chunk. + return; } var profile = new ExifProfile(exifData); @@ -580,7 +577,8 @@ namespace SixLabors.ImageSharp.Formats.Webp int bytesRead = this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); if (bytesRead != xmpChunkSize) { - WebpThrowHelper.ThrowInvalidImageContentException("Not enough data to read the xmp chunk"); + // Ignore invalid chunk. + return; } var profile = new XmpProfile(xmpData); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 499b0579b6..456b9a3f52 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats.Webp; @@ -153,14 +154,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp [Theory] [WithFile(TestImages.Webp.Lossy.WithExifNotEnoughData, PixelTypes.Rgba32)] - public void WebpDecoder_ThrowInvalidImageContentException_OnWithInvalidExifData(TestImageProvider provider) - where TPixel : unmanaged, IPixel => - Assert.Throws( - () => - { - using (provider.GetImage(WebpDecoder)) - { - } - }); + public void WebpDecoder_IgnoresInvalidExifChunk(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception ex = Record.Exception(() => + { + using Image image = provider.GetImage(); + }); + Assert.Null(ex); + } } } From ced98879ddb048860732aafb1f0678a51e9df1d5 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 11 Apr 2022 14:49:41 +0200 Subject: [PATCH 17/30] Add checks, if enough data was read --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 16dca3324f..d17e89cd45 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -221,7 +221,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private void ReadGraphicalControlExtension() { - this.stream.Read(this.buffer, 0, 6); + int bytesRead = this.stream.Read(this.buffer, 0, 6); + if (bytesRead != 6) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); + } this.graphicsControlExtension = GifGraphicControlExtension.Parse(this.buffer); } @@ -231,7 +235,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private void ReadImageDescriptor() { - this.stream.Read(this.buffer, 0, 9); + int bytesRead = this.stream.Read(this.buffer, 0, 9); + if (bytesRead != 9) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); + } this.imageDescriptor = GifImageDescriptor.Parse(this.buffer); if (this.imageDescriptor.Height == 0 || this.imageDescriptor.Width == 0) @@ -245,7 +253,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// private void ReadLogicalScreenDescriptor() { - this.stream.Read(this.buffer, 0, 7); + int bytesRead = this.stream.Read(this.buffer, 0, 7); + if (bytesRead != 7) + { + GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); + } this.logicalScreenDescriptor = GifLogicalScreenDescriptor.Parse(this.buffer); } From 8cdcda343b5004b9f41da9884ffbce162e361400 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 13 Apr 2022 15:56:23 +0300 Subject: [PATCH 18/30] Added sanity check for every jpeg marker --- .../Formats/Jpeg/JpegDecoderCore.cs | 40 +++++++++++-------- .../Formats/Jpeg/JpegThrowHelper.cs | 3 ++ src/ImageSharp/IO/BufferedReadStream.cs | 9 +++++ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index ef4e3ffac2..52cd411916 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -315,14 +315,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (!fileMarker.Invalid) { // Get the marker length. - int remaining = this.ReadUint16(stream) - 2; + int markerContentByteSize = this.ReadUint16(stream) - 2; + + // Check whether stream actually has enought bytes to read + // markerContentByteSize is always positive so we cast + // to uint to avoid sign extension + if (stream.RemainingBytes < (uint)markerContentByteSize) + { + JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); + } switch (fileMarker.Marker) { case JpegConstants.Markers.SOF0: case JpegConstants.Markers.SOF1: case JpegConstants.Markers.SOF2: - this.ProcessStartOfFrameMarker(stream, remaining, fileMarker, metadataOnly); + this.ProcessStartOfFrameMarker(stream, markerContentByteSize, fileMarker, metadataOnly); break; case JpegConstants.Markers.SOF5: @@ -350,7 +358,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.SOS: if (!metadataOnly) { - this.ProcessStartOfScanMarker(stream, remaining); + this.ProcessStartOfScanMarker(stream, markerContentByteSize); break; } else @@ -364,41 +372,41 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (metadataOnly) { - stream.Skip(remaining); + stream.Skip(markerContentByteSize); } else { - this.ProcessDefineHuffmanTablesMarker(stream, remaining); + this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize); } break; case JpegConstants.Markers.DQT: - this.ProcessDefineQuantizationTablesMarker(stream, remaining); + this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.DRI: if (metadataOnly) { - stream.Skip(remaining); + stream.Skip(markerContentByteSize); } else { - this.ProcessDefineRestartIntervalMarker(stream, remaining); + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); } break; case JpegConstants.Markers.APP0: - this.ProcessApplicationHeaderMarker(stream, remaining); + this.ProcessApplicationHeaderMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP1: - this.ProcessApp1Marker(stream, remaining); + this.ProcessApp1Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP2: - this.ProcessApp2Marker(stream, remaining); + this.ProcessApp2Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP3: @@ -411,20 +419,20 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.APP10: case JpegConstants.Markers.APP11: case JpegConstants.Markers.APP12: - stream.Skip(remaining); + stream.Skip(markerContentByteSize); break; case JpegConstants.Markers.APP13: - this.ProcessApp13Marker(stream, remaining); + this.ProcessApp13Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP14: - this.ProcessApp14Marker(stream, remaining); + this.ProcessApp14Marker(stream, markerContentByteSize); break; case JpegConstants.Markers.APP15: case JpegConstants.Markers.COM: - stream.Skip(remaining); + stream.Skip(markerContentByteSize); break; case JpegConstants.Markers.DAC: @@ -1260,7 +1268,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int selectorsBytes = selectorsCount * 2; if (remaining != 4 + selectorsBytes) { - JpegThrowHelper.ThrowBadMarker("SOS", remaining); + JpegThrowHelper.ThrowBadMarker(nameof(JpegConstants.Markers.SOS), remaining); } // selectorsCount*2 bytes: component index + huffman tables indices diff --git a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs index b238e45ef3..1073ffff78 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegThrowHelper.cs @@ -25,6 +25,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg [MethodImpl(InliningOptions.ColdPath)] public static void ThrowBadMarker(string marker, int length) => throw new InvalidImageContentException($"Marker {marker} has bad length {length}."); + [MethodImpl(InliningOptions.ColdPath)] + public static void ThrowNotEnoughBytesForMarker(byte marker) => throw new InvalidImageContentException($"Input stream does not have enough bytes to parse declared contents of the {marker:X2} marker."); + [MethodImpl(InliningOptions.ColdPath)] public static void ThrowBadQuantizationTableIndex(int index) => throw new InvalidImageContentException($"Bad Quantization Table index {index}."); diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 4ab7f312b2..2823b8ed6f 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -114,6 +114,15 @@ namespace SixLabors.ImageSharp.IO /// public override bool CanWrite { get; } = false; + /// + /// Gets remaining byte count available to read. + /// + public long RemainingBytes + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.Length - this.Position; + } + /// /// Gets the underlying stream. /// From 64d9146a8d6def56c5970c774193e36aeb202f90 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Wed, 13 Apr 2022 22:00:40 +0300 Subject: [PATCH 19/30] Added malformed image --- tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index 70cbc3af72..d5e0f081bf 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -105,6 +105,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693A, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException1693B, TestImages.Jpeg.Issues.Fuzz.IndexOutOfRangeException824C, + TestImages.Jpeg.Issues.Fuzz.NullReferenceException2085, }; private static readonly Dictionary CustomToleranceValues = new() diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 07aff6fc12..bbc0b1e3db 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -292,6 +292,7 @@ namespace SixLabors.ImageSharp.Tests public const string AccessViolationException922 = "Jpg/issues/fuzz/Issue922-AccessViolationException.jpg"; public const string IndexOutOfRangeException1693A = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-A.jpg"; public const string IndexOutOfRangeException1693B = "Jpg/issues/fuzz/Issue1693-IndexOutOfRangeException-B.jpg"; + public const string NullReferenceException2085 = "Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg"; } } diff --git a/tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg b/tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg new file mode 100644 index 0000000000..8a680ff6a6 --- /dev/null +++ b/tests/Images/Input/Jpg/issues/fuzz/Issue2085-NullReferenceException.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d478beff34179fda26238a44434607c276f55438ee96824c5af8c0188d358d8d +size 234 From c7f9d54705f617e14a415346a2c8f7ce199091c7 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 14 Apr 2022 14:01:01 +0300 Subject: [PATCH 20/30] Fixed string exif value corner case null ref exception --- .../Profiles/Exif/ExifEncodedStringHelpers.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs index 5fd613b1f0..6e697029eb 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs @@ -79,16 +79,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return CharacterCodeBytesLength + count; } - public static unsafe int Write(Encoding encoding, string value, Span destination) - { - fixed (char* c = value) - { - fixed (byte* b = destination) - { - return encoding.GetBytes(c, value.Length, b, destination.Length); - } - } - } + public static unsafe int Write(Encoding encoding, string value, Span destination) => + encoding.GetBytes(value.AsSpan(), destination); private static bool TryDetect(ReadOnlySpan buffer, out CharacterCode code) { From 210945f93909ab903e08456ef51a964841127b43 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 14 Apr 2022 15:01:54 +0300 Subject: [PATCH 21/30] Fixed compilation error for older frameworks --- .../Profiles/Exif/ExifEncodedStringHelpers.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs index 6e697029eb..4dc7f83985 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs @@ -79,8 +79,20 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return CharacterCodeBytesLength + count; } - public static unsafe int Write(Encoding encoding, string value, Span destination) => - encoding.GetBytes(value.AsSpan(), destination); + public static unsafe int Write(Encoding encoding, string value, Span destination) +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER || NET + => encoding.GetBytes(value.AsSpan(), destination); +#else + { + fixed (char* c = value) + { + fixed (byte* b = destination) + { + return encoding.GetBytes(c, value.Length, b, destination.Length); + } + } + } +#endif private static bool TryDetect(ReadOnlySpan buffer, out CharacterCode code) { From 55d01f231f840f3d57e997be43040e85fb3dc205 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 14 Apr 2022 15:03:59 +0300 Subject: [PATCH 22/30] Added guard clause of empty exif strings --- .../Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs index 4dc7f83985..4ec9b3267e 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifEncodedStringHelpers.cs @@ -84,6 +84,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif => encoding.GetBytes(value.AsSpan(), destination); #else { + if (value.Length == 0) + { + return 0; + } + fixed (char* c = value) { fixed (byte* b = destination) From d1c6e5042900dbbe8668e82e3213764453506b76 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 15 Apr 2022 12:08:58 +0300 Subject: [PATCH 23/30] Added test image --- .../Formats/Jpg/JpegDecoderTests.Images.cs | 1 + .../Formats/Jpg/JpegEncoderTests.Metadata.cs | 33 +++++++++++++++++++ .../Formats/Jpg/JpegEncoderTests.cs | 2 +- tests/ImageSharp.Tests/TestImages.cs | 1 + ...ssue2087-exif-null-reference-on-encode.jpg | 3 ++ 5 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs create mode 100644 tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index 70cbc3af72..60439bc3d7 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -33,6 +33,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.ExifGetString750Load, TestImages.Jpeg.Issues.ExifGetString750Transform, TestImages.Jpeg.Issues.BadSubSampling1076, + TestImages.Jpeg.Issues.ValidExifArgumentNullExceptionOnEncode, // LibJpeg can open this despite the invalid density units. TestImages.Jpeg.Issues.Fuzz.ArgumentOutOfRangeException825B, diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs new file mode 100644 index 0000000000..46ac23be12 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.PixelFormats; + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Jpg +{ + [Trait("Format", "Jpg")] + public partial class JpegEncoderTests + { + [Theory] + [WithFile(TestImages.Jpeg.Issues.ValidExifArgumentNullExceptionOnEncode, PixelTypes.Rgba32)] + public void Encode_WithValidExifProfile_DoesNotThrowException(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Exception exception = Record.Exception(() => + { + var encoder = new JpegEncoder(); + var stream = new MemoryStream(); + + using Image image = provider.GetImage(JpegDecoder); + image.Save(stream, encoder); + }); + + Assert.Null(exception); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index 18eae9fbd3..d860836e08 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -20,7 +20,7 @@ using Xunit; namespace SixLabors.ImageSharp.Tests.Formats.Jpg { [Trait("Format", "Jpg")] - public class JpegEncoderTests + public partial class JpegEncoderTests { private static JpegEncoder JpegEncoder => new(); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 07aff6fc12..4ea740266a 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -264,6 +264,7 @@ namespace SixLabors.ImageSharp.Tests public const string InvalidIptcTag = "Jpg/issues/Issue1942InvalidIptcTag.jpg"; public const string Issue2057App1Parsing = "Jpg/issues/Issue2057-App1Parsing.jpg"; public const string ExifNullArrayTag = "Jpg/issues/issue-2056-exif-null-array.jpg"; + public const string ValidExifArgumentNullExceptionOnEncode = "Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg"; public static class Fuzz { diff --git a/tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg b/tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg new file mode 100644 index 0000000000..e95ef7a73d --- /dev/null +++ b/tests/Images/Input/Jpg/issues/Issue2087-exif-null-reference-on-encode.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d41a41180a3371d0c4a724b40a4c86f6f975dab6be9da96964a484818770394 +size 30715 From cde4a88c22d461375aabc4dd632dd96c154b88e5 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 15 Apr 2022 12:13:57 +0300 Subject: [PATCH 24/30] Removed invalid image from test suite --- tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs | 1 - .../ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index 60439bc3d7..70cbc3af72 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -33,7 +33,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Issues.ExifGetString750Load, TestImages.Jpeg.Issues.ExifGetString750Transform, TestImages.Jpeg.Issues.BadSubSampling1076, - TestImages.Jpeg.Issues.ValidExifArgumentNullExceptionOnEncode, // LibJpeg can open this despite the invalid density units. TestImages.Jpeg.Issues.Fuzz.ArgumentOutOfRangeException825B, diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs index 46ac23be12..2bbce6cb1b 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs @@ -18,7 +18,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void Encode_WithValidExifProfile_DoesNotThrowException(TestImageProvider provider) where TPixel : unmanaged, IPixel { - Exception exception = Record.Exception(() => + Exception ex = Record.Exception(() => { var encoder = new JpegEncoder(); var stream = new MemoryStream(); @@ -27,7 +27,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg image.Save(stream, encoder); }); - Assert.Null(exception); + Assert.Null(ex); } } } From 2ac18d816f0074f51a19983662594090516a0a7a Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 16 Apr 2022 18:39:49 +1000 Subject: [PATCH 25/30] Optimize tiff/jpeg checks --- .../Formats/Jpeg/JpegDecoderCore.cs | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index a7f93b0e85..533ffa719c 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -228,16 +228,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.Metadata = new ImageMetadata(); this.QuantizationTables = new Block8x8F[4]; this.scanDecoder = huffmanScanDecoder; + + if (tableBytes.Length < 4) + { + JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); + } + using var ms = new MemoryStream(tableBytes); using var stream = new BufferedReadStream(this.Configuration, ms); // Check for the Start Of Image marker. int bytesRead = stream.Read(this.markerBuffer, 0, 2); - if (bytesRead != 2) - { - JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read the SOI marker"); - } - var fileMarker = new JpegFileMarker(this.markerBuffer[1], 0); if (fileMarker.Marker != JpegConstants.Markers.SOI) { @@ -246,20 +247,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Read next marker. bytesRead = stream.Read(this.markerBuffer, 0, 2); - if (bytesRead != 2) - { - JpegThrowHelper.ThrowInvalidImageContentException("Not enough data to read marker"); - } - - byte marker = this.markerBuffer[1]; - fileMarker = new JpegFileMarker(marker, (int)stream.Position - 2); + fileMarker = new JpegFileMarker(this.markerBuffer[1], (int)stream.Position - 2); while (fileMarker.Marker != JpegConstants.Markers.EOI || (fileMarker.Marker == JpegConstants.Markers.EOI && fileMarker.Invalid)) { if (!fileMarker.Invalid) { // Get the marker length. - int remaining = this.ReadUint16(stream) - 2; + int markerContentByteSize = this.ReadUint16(stream) - 2; + + // Check whether stream actually has enought bytes to read + // markerContentByteSize is always positive so we cast + // to uint to avoid sign extension + if (stream.RemainingBytes < (uint)markerContentByteSize) + { + JpegThrowHelper.ThrowNotEnoughBytesForMarker(fileMarker.Marker); + } switch (fileMarker.Marker) { @@ -269,13 +272,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg case JpegConstants.Markers.RST7: break; case JpegConstants.Markers.DHT: - this.ProcessDefineHuffmanTablesMarker(stream, remaining); + this.ProcessDefineHuffmanTablesMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.DQT: - this.ProcessDefineQuantizationTablesMarker(stream, remaining); + this.ProcessDefineQuantizationTablesMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.DRI: - this.ProcessDefineRestartIntervalMarker(stream, remaining); + this.ProcessDefineRestartIntervalMarker(stream, markerContentByteSize); break; case JpegConstants.Markers.EOI: return; From d8012cab6fe86f7dc95c44f55e0322a94c120918 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 22 Apr 2022 16:46:01 +1000 Subject: [PATCH 26/30] Use .NET 6 only --- .github/workflows/build-and-test.yml | 49 +++++-------------- .github/workflows/code-coverage.yml | 8 ++- ImageSharp.sln | 18 ------- src/ImageSharp/ImageSharp.csproj | 31 ++---------- .../ImageSharp.Benchmarks.csproj | 11 ++--- .../ImageSharp.Tests.ProfilingSandbox.csproj | 11 ++--- .../LoadResizeSaveParallelMemoryStress.cs | 1 - .../ImageSharp.Tests/ImageSharp.Tests.csproj | 12 ++--- 8 files changed, 34 insertions(+), 107 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 43a4a17084..551d2e0524 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -15,59 +15,38 @@ jobs: matrix: options: - os: ubuntu-latest - framework: net6.0 - sdk: 6.0.x + framework: net7.0 + sdk: 7.0.x sdk-preview: true runtime: -x64 codecov: false - os: macos-latest - framework: net6.0 - sdk: 6.0.x + framework: net7.0 + sdk: 7.0.x sdk-preview: true runtime: -x64 codecov: false - os: windows-latest - framework: net6.0 - sdk: 6.0.x + framework: net7.0 + sdk: 7.0.x sdk-preview: true runtime: -x64 codecov: false - os: ubuntu-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: macos-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: windows-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: ubuntu-latest - framework: netcoreapp3.1 + framework: net6.0 + sdk: 6.0.x runtime: -x64 codecov: false - os: macos-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: false - - os: windows-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: false - - os: windows-latest - framework: netcoreapp2.1 + framework: net6.0 + sdk: 6.0.x runtime: -x64 codecov: false - os: windows-latest - framework: net472 + framework: net6.0 + sdk: 6.0.x runtime: -x64 codecov: false - - os: windows-latest - framework: net472 - runtime: -x86 - codecov: false runs-on: ${{matrix.options.os}} @@ -113,10 +92,8 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: | + 7.0.x 6.0.x - 5.0.x - 3.1.x - 2.1.x - name: DotNet Build if: ${{ matrix.options.sdk-preview != true }} diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 2b14f2a4b7..3f8a820313 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -10,7 +10,7 @@ jobs: matrix: options: - os: ubuntu-latest - framework: netcoreapp3.1 + framework: net6.0 runtime: -x64 codecov: true @@ -54,6 +54,12 @@ jobs: key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} restore-keys: ${{ runner.os }}-nuget- + - name: DotNet Setup + uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 6.0.x + - name: DotNet Build shell: pwsh run: ./ci-build.ps1 "${{matrix.options.framework}}" diff --git a/ImageSharp.sln b/ImageSharp.sln index 5428f3394d..fbf1ca24ba 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -654,43 +654,25 @@ Global EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug-InnerLoop|Any CPU = Debug-InnerLoop|Any CPU Release|Any CPU = Release|Any CPU - Release-InnerLoop|Any CPU = Release-InnerLoop|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|Any CPU.ActiveCfg = Release|Any CPU {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release|Any CPU.Build.0 = Release|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {2AA31A1F-142C-43F4-8687-09ABCA4B3A26}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release|Any CPU.Build.0 = Release|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {EA3000E9-2A91-4EC4-8A68-E566DEBDC4F6}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|Any CPU.ActiveCfg = Release|Any CPU {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release|Any CPU.Build.0 = Release|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {2BF743D8-2A06-412D-96D7-F448F00C5EA5}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|Any CPU.ActiveCfg = Debug-InnerLoop|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Debug-InnerLoop|Any CPU.Build.0 = Debug-InnerLoop|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.ActiveCfg = Release|Any CPU {FC527290-2F22-432C-B77B-6E815726B02C}.Release|Any CPU.Build.0 = Release|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|Any CPU.ActiveCfg = Release-InnerLoop|Any CPU - {FC527290-2F22-432C-B77B-6E815726B02C}.Release-InnerLoop|Any CPU.Build.0 = Release-InnerLoop|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 39c85c4f22..7e58607dd1 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -12,33 +12,23 @@ $(RepositoryUrl) Image Resize Crop Gif Jpg Jpeg Bitmap Pbm Png Tga Tiff WebP NetCore A new, fully featured, fully managed, cross-platform, 2D graphics API for .NET - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release - - 2.0 + + 3.0 - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;net472 - - - - - net5.0;netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - netcoreapp3.1;netcoreapp2.1;netstandard2.1;netstandard2.0;net472 + net6.0 @@ -47,17 +37,6 @@ - - - - - - - - - - - True diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 9a92741997..24f618d11b 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -9,7 +9,7 @@ portable false - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release @@ -17,17 +17,12 @@ - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - net5.0;netcoreapp3.1;netcoreapp2.1;net472 + net6.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index 6ff5a4cc7f..492ce36b81 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -12,24 +12,19 @@ false false - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release false - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - net5.0;netcoreapp3.1;netcoreapp2.1;net472 + net6.0 diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs index 95e64b1539..0c7b157b2b 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs @@ -5,7 +5,6 @@ using System; using System.Diagnostics; using System.Globalization; using System.IO; -using System.Runtime.CompilerServices; using System.Text; using System.Threading; using CommandLine; diff --git a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj index 28c778787a..a4f1de17be 100644 --- a/tests/ImageSharp.Tests/ImageSharp.Tests.csproj +++ b/tests/ImageSharp.Tests/ImageSharp.Tests.csproj @@ -6,23 +6,18 @@ SixLabors.ImageSharp.Tests AnyCPU;x64;x86 SixLabors.ImageSharp.Tests - Debug;Release;Debug-InnerLoop;Release-InnerLoop + Debug;Release - net6.0;net5.0;netcoreapp3.1;netcoreapp2.1;net472 - - - - - netcoreapp3.1 + net7.0;net6.0 - net5.0;netcoreapp3.1;netcoreapp2.1;net472 + net6.0 @@ -47,7 +42,6 @@ - From 78017247a962d8ef18b7e67f7ccea7f12234dd5c Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 22 Apr 2022 16:52:40 +1000 Subject: [PATCH 27/30] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 551d2e0524..16f8ebb065 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -91,6 +91,7 @@ jobs: - name: DotNet Setup uses: actions/setup-dotnet@v1 with: + include-prerelease: true dotnet-version: | 7.0.x 6.0.x From bda5418356eaacedbc625c4dca01d216c4850fd3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 22 Apr 2022 20:51:42 +1000 Subject: [PATCH 28/30] Update FeatureTestRunnerTests.cs --- .../TestUtilities/Tests/FeatureTestRunnerTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs index a2f36c85a8..a46e4341d1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs @@ -55,6 +55,9 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests HwIntrinsics.AllowAll); } +#if !NET7_0_OR_GREATER + // COMPlus_EnableSIMD isn't thing anymore. + // https://github.com/dotnet/runtime/issues/66206 [Fact] public void CanLimitHwIntrinsicSIMDFeatures() { @@ -62,6 +65,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests () => Assert.False(Vector.IsHardwareAccelerated), HwIntrinsics.DisableSIMD); } +#endif #if SUPPORTS_RUNTIME_INTRINSICS [Fact] @@ -101,9 +105,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic)) { +#if !NET7_0_OR_GREATER + // COMPlus_EnableSIMD isn't thing anymore. + // https://github.com/dotnet/runtime/issues/66206 case HwIntrinsics.DisableSIMD: Assert.False(Vector.IsHardwareAccelerated); break; +#endif #if SUPPORTS_RUNTIME_INTRINSICS case HwIntrinsics.DisableHWIntrinsic: Assert.False(Sse.IsSupported); @@ -206,9 +214,13 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic)) { +#if !NET7_0_OR_GREATER + // COMPlus_EnableSIMD isn't thing anymore. + // https://github.com/dotnet/runtime/issues/66206 case HwIntrinsics.DisableSIMD: Assert.False(Vector.IsHardwareAccelerated, nameof(Vector.IsHardwareAccelerated)); break; +#endif #if SUPPORTS_RUNTIME_INTRINSICS case HwIntrinsics.DisableHWIntrinsic: Assert.False(Sse.IsSupported); From 205df12e4ec6c52805f66ea293067d7ee1ee7b1b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sun, 24 Apr 2022 18:20:58 +1000 Subject: [PATCH 29/30] Remove DisableSIMD --- .../ImageSharp.Tests/Formats/Jpg/DCTTests.cs | 8 ++-- .../Formats/Png/PngEncoderFilterTests.cs | 8 ++-- .../FeatureTesting/FeatureTestRunner.cs | 39 ++++++++----------- .../Tests/FeatureTestRunnerTests.cs | 30 +------------- 4 files changed, 27 insertions(+), 58 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs index 9c467a1cc9..a40ae2af50 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs @@ -149,11 +149,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // 1. AllowAll - call avx/fma implementation // 2. DisableFMA - call avx without fma implementation // 3. DisableAvx - call sse implementation - // 4. DisableSIMD - call Vector4 fallback implementation + // 4. DisableHWIntrinsic - call Vector4 fallback implementation FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD); + HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } // Forward transform @@ -200,11 +200,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // 1. AllowAll - call avx/fma implementation // 2. DisableFMA - call avx without fma implementation // 3. DisableAvx - call Vector4 implementation - // 4. DisableSIMD - call scalar fallback implementation + // 4. DisableHWIntrinsic - call scalar fallback implementation FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, seed, - HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD); + HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs index 11e3fbb230..90fa5777b5 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderFilterTests.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] @@ -95,7 +95,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] @@ -137,7 +137,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] @@ -179,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png FeatureTestRunner.RunWithHwIntrinsicsFeature( RunTest, - HwIntrinsics.DisableSIMD); + HwIntrinsics.DisableHWIntrinsic); } [Fact] diff --git a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs index 0d2f3fcefb..fc0374bbd7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs +++ b/tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs @@ -356,10 +356,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities var key = (HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic); switch (intrinsic) { - case nameof(HwIntrinsics.DisableSIMD): - features.Add(key, "FeatureSIMD"); - break; - case nameof(HwIntrinsics.AllowAll): // Not a COMPlus value. We filter in calling method. @@ -390,23 +386,22 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities { // Use flags so we can pass multiple values without using params. // Don't base on 0 or use inverse for All as that doesn't translate to string values. - DisableSIMD = 1 << 0, - DisableHWIntrinsic = 1 << 1, - DisableSSE = 1 << 2, - DisableSSE2 = 1 << 3, - DisableAES = 1 << 4, - DisablePCLMULQDQ = 1 << 5, - DisableSSE3 = 1 << 6, - DisableSSSE3 = 1 << 7, - DisableSSE41 = 1 << 8, - DisableSSE42 = 1 << 9, - DisablePOPCNT = 1 << 10, - DisableAVX = 1 << 11, - DisableFMA = 1 << 12, - DisableAVX2 = 1 << 13, - DisableBMI1 = 1 << 14, - DisableBMI2 = 1 << 15, - DisableLZCNT = 1 << 16, - AllowAll = 1 << 17 + DisableHWIntrinsic = 1 << 0, + DisableSSE = 1 << 1, + DisableSSE2 = 1 << 2, + DisableAES = 1 << 3, + DisablePCLMULQDQ = 1 << 4, + DisableSSE3 = 1 << 5, + DisableSSSE3 = 1 << 6, + DisableSSE41 = 1 << 7, + DisableSSE42 = 1 << 8, + DisablePOPCNT = 1 << 9, + DisableAVX = 1 << 10, + DisableFMA = 1 << 11, + DisableAVX2 = 1 << 12, + DisableBMI1 = 1 << 13, + DisableBMI2 = 1 << 14, + DisableLZCNT = 1 << 15, + AllowAll = 1 << 16 } } diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs index a46e4341d1..6dd59a750d 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs @@ -16,10 +16,10 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests public class FeatureTestRunnerTests { public static TheoryData Intrinsics => - new TheoryData + new() { { HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, new string[] { "EnableAES", "AllowAll" } }, - { HwIntrinsics.DisableSIMD | HwIntrinsics.DisableHWIntrinsic, new string[] { "FeatureSIMD", "EnableHWIntrinsic" } }, + { HwIntrinsics.DisableHWIntrinsic, new string[] { "EnableHWIntrinsic" } }, { HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, new string[] { "EnableSSE42", "EnableAVX" } } }; @@ -55,18 +55,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests HwIntrinsics.AllowAll); } -#if !NET7_0_OR_GREATER - // COMPlus_EnableSIMD isn't thing anymore. - // https://github.com/dotnet/runtime/issues/66206 - [Fact] - public void CanLimitHwIntrinsicSIMDFeatures() - { - FeatureTestRunner.RunWithHwIntrinsicsFeature( - () => Assert.False(Vector.IsHardwareAccelerated), - HwIntrinsics.DisableSIMD); - } -#endif - #if SUPPORTS_RUNTIME_INTRINSICS [Fact] public void CanLimitHwIntrinsicBaseFeatures() @@ -105,13 +93,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic)) { -#if !NET7_0_OR_GREATER - // COMPlus_EnableSIMD isn't thing anymore. - // https://github.com/dotnet/runtime/issues/66206 - case HwIntrinsics.DisableSIMD: - Assert.False(Vector.IsHardwareAccelerated); - break; -#endif #if SUPPORTS_RUNTIME_INTRINSICS case HwIntrinsics.DisableHWIntrinsic: Assert.False(Sse.IsSupported); @@ -214,13 +195,6 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests switch ((HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic)) { -#if !NET7_0_OR_GREATER - // COMPlus_EnableSIMD isn't thing anymore. - // https://github.com/dotnet/runtime/issues/66206 - case HwIntrinsics.DisableSIMD: - Assert.False(Vector.IsHardwareAccelerated, nameof(Vector.IsHardwareAccelerated)); - break; -#endif #if SUPPORTS_RUNTIME_INTRINSICS case HwIntrinsics.DisableHWIntrinsic: Assert.False(Sse.IsSupported); From ed3cda5070bbacc1c1884a3ac872a089010afd9d Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Tue, 26 Apr 2022 20:26:53 +0200 Subject: [PATCH 30/30] Use ReadOnlySpan static data compiler optimization in more places --- src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs | 5 +++-- src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs | 11 +++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs index 2a40cffdcd..b6703d7390 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/QuantEnc.cs @@ -16,8 +16,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// internal static unsafe class QuantEnc { - private static readonly byte[] Zigzag = { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; - private static readonly ushort[] WeightY = { 38, 32, 20, 9, 32, 28, 17, 7, 20, 17, 10, 4, 9, 7, 4, 2 }; private const int MaxLevel = 2047; @@ -47,6 +45,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy private const int DSHIFT = 4; private const int DSCALE = 1; // storage descaling, needed to make the error fit byte + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan Zigzag => new byte[] { 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15 }; + public static void PickBestIntra16(Vp8EncIterator it, ref Vp8ModeScore rd, Vp8SegmentInfo[] segmentInfos, Vp8EncProba proba) { const int numBlocks = 16; diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs index 66c91e44ad..7ee7a70080 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Matrix.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; + namespace SixLabors.ImageSharp.Formats.Webp.Lossy { internal unsafe struct Vp8Matrix @@ -13,10 +15,6 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy new[] { 110, 115 } }; - // Sharpening by (slightly) raising the hi-frequency coeffs. - // Hack-ish but helpful for mid-bitrate range. Use with care. - private static readonly byte[] FreqSharpening = { 0, 30, 60, 90, 30, 60, 90, 90, 60, 90, 90, 90, 90, 90, 90, 90 }; - /// /// Number of descaling bits for sharpening bias. /// @@ -47,6 +45,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy /// public fixed short Sharpen[16]; + // Sharpening by (slightly) raising the hi-frequency coeffs. + // Hack-ish but helpful for mid-bitrate range. Use with care. + // This uses C#'s optimization to refer to the static data segment of the assembly, no allocation occurs. + private static ReadOnlySpan FreqSharpening => new byte[] { 0, 30, 60, 90, 30, 60, 90, 90, 60, 90, 90, 90, 90, 90, 90, 90 }; + /// /// Returns the average quantizer. ///