From 48c6f3f6678b6f456001dbdcfc51c48a878dfcb7 Mon Sep 17 00:00:00 2001 From: Anton Firszov Date: Fri, 7 Feb 2020 23:38:26 +0100 Subject: [PATCH] implement correct AdvancedImageExtensions behavior --- .../Advanced/AdvancedImageExtensions.cs | 20 +- src/ImageSharp/Memory/Buffer2D{T}.cs | 23 ++- .../Advanced/AdvancedImageExtensionsTests.cs | 173 +++++++++++------- .../DiscontiguousBuffers/MemoryGroupIndex.cs | 8 +- .../BasicTestPatternProvider.cs | 56 ++++-- 5 files changed, 169 insertions(+), 111 deletions(-) diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index 665d0e28b6..fc6ba10b0d 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Advanced /// The /// Thrown when the backing buffer is discontiguous. [Obsolete( - @"GetPixelSpan might fail, because the backing buffer allowed to be discontiguous for large images. Use GetPixelMemoryGroup or GetPixelRowSpan instead!")] + @"GetPixelSpan might fail, because the backing buffer could be discontiguous for large images. Use GetPixelMemoryGroup or GetPixelRowSpan instead!")] public static Span GetPixelSpan(this ImageFrame source) where TPixel : struct, IPixel { @@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Advanced /// The /// Thrown when the backing buffer is discontiguous. [Obsolete( - @"GetPixelSpan might fail, because the backing buffer allowed to be discontiguous for large images. Use GetPixelMemoryGroup or GetPixelRowSpan instead!")] + @"GetPixelSpan might fail, because the backing buffer could be discontiguous for large images. Use GetPixelMemoryGroup or GetPixelRowSpan instead!")] public static Span GetPixelSpan(this Image source) where TPixel : struct, IPixel => source.Frames.RootFrame.GetPixelSpan(); @@ -146,9 +146,9 @@ namespace SixLabors.ImageSharp.Advanced /// The source. /// The row. /// The - internal static Memory GetPixelRowMemory(this ImageFrame source, int rowIndex) + public static Memory GetPixelRowMemory(this ImageFrame source, int rowIndex) where TPixel : struct, IPixel - => source.PixelBuffer.GetRowMemory(rowIndex); + => source.PixelBuffer.GetRowMemorySafe(rowIndex); /// /// Gets the representation of the pixels as of of contiguous memory @@ -158,7 +158,7 @@ namespace SixLabors.ImageSharp.Advanced /// The source. /// The row. /// The - internal static Memory GetPixelRowMemory(this Image source, int rowIndex) + public static Memory GetPixelRowMemory(this Image source, int rowIndex) where TPixel : struct, IPixel => source.Frames.RootFrame.GetPixelRowMemory(rowIndex); @@ -169,15 +169,5 @@ namespace SixLabors.ImageSharp.Advanced /// Returns the configuration. internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source) => GetConfiguration(source).MemoryAllocator; - - /// - /// Returns a reference to the 0th element of the Pixel buffer. - /// Such a reference can be used for pinning but must never be dereferenced. - /// - /// The source image frame - /// A reference to the element. - private static ref TPixel DangerousGetPinnableReferenceToPixelBuffer(IPixelSource source) - where TPixel : struct, IPixel - => ref MemoryMarshal.GetReference(source.PixelBuffer.GetSingleSpan()); } } diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index ea2568efde..bb00b48cdb 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -83,13 +83,24 @@ namespace SixLabors.ImageSharp.Memory : this.GetRowMemorySlow(y).Span; } + /// + /// Disposes the instance + /// + public void Dispose() + { + this.MemoryGroup.Dispose(); + this.cachedMemory = default; + } + /// /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// This method is intended for internal use only, since it does not use the indirection provided by + /// . /// /// The y (row) coordinate. /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Memory GetRowMemory(int y) + internal Memory GetRowMemoryFast(int y) { return this.cachedMemory.Length > 0 ? this.cachedMemory.Slice(y * this.Width, this.Width) @@ -97,13 +108,11 @@ namespace SixLabors.ImageSharp.Memory } /// - /// Disposes the instance + /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. /// - public void Dispose() - { - this.MemoryGroup.Dispose(); - this.cachedMemory = default; - } + /// The y (row) coordinate. + /// The . + internal Memory GetRowMemorySafe(int y) => this.MemoryGroup.View.GetBoundedSlice(y * this.Width, this.Width); /// /// Swaps the contents of 'destination' with 'source' if the buffers are owned (1), diff --git a/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs b/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs index 548caa488d..de69d7207b 100644 --- a/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs +++ b/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs @@ -2,10 +2,13 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Linq; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers; using Xunit; // ReSharper disable InconsistentNaming @@ -13,113 +16,145 @@ namespace SixLabors.ImageSharp.Tests.Advanced { public class AdvancedImageExtensionsTests { - public class GetPixelMemory + public class GetPixelMemoryGroup { [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void WhenMemoryIsOwned(TestImageProvider provider) + [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)] + public void OwnedMemory_PixelDataIsCorrect(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image0 = provider.GetImage()) - { - var targetBuffer = new TPixel[image0.Width * image0.Height]; + provider.LimitAllocatorBufferCapacity(); - // Act: - Memory memory = image0.GetRootFramePixelBuffer().GetSingleMemory(); + using Image image = provider.GetImage(); - // Assert: - Assert.Equal(image0.Width * image0.Height, memory.Length); - memory.Span.CopyTo(targetBuffer); + // Act: + IMemoryGroup memoryGroup = image.GetPixelMemoryGroup(); - using (Image image1 = provider.GetImage()) - { - // We are using a copy of the original image for assertion - image1.ComparePixelBufferTo(targetBuffer); - } - } + // Assert: + VerifyMemoryGroupDataMatchesTestPattern(provider, memoryGroup, image.Size()); } [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32 | PixelTypes.Bgr24)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void WhenMemoryIsConsumed(TestImageProvider provider) + [WithBlankImages(16, 16, PixelTypes.Rgba32)] + public void OwnedMemory_DestructiveMutate_ShouldInvalidateMemoryGroup(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image0 = provider.GetImage()) - { - var targetBuffer = new TPixel[image0.Width * image0.Height]; - image0.GetPixelSpan().CopyTo(targetBuffer); + using Image image = provider.GetImage(); + + IMemoryGroup memoryGroup = image.GetPixelMemoryGroup(); + Memory memory = memoryGroup.Single(); - var managerOfExternalMemory = new TestMemoryManager(targetBuffer); + image.Mutate(c => c.Resize(8, 8)); - Memory externalMemory = managerOfExternalMemory.Memory; + Assert.False(memoryGroup.IsValid); + Assert.ThrowsAny(() => _ = memoryGroup.First()); + Assert.ThrowsAny(() => _ = memory.Span); + } + + [Theory] + [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(131, 127, PixelTypes.Bgr24)] + public void ConsumedMemory_PixelDataIsCorrect(TestImageProvider provider) + where TPixel : struct, IPixel + { + using Image image0 = provider.GetImage(); + var targetBuffer = new TPixel[image0.Width * image0.Height]; + image0.GetPixelSpan().CopyTo(targetBuffer); - using (var image1 = Image.WrapMemory(externalMemory, image0.Width, image0.Height)) - { - Memory internalMemory = image1.GetRootFramePixelBuffer().GetSingleMemory(); - Assert.Equal(targetBuffer.Length, internalMemory.Length); - Assert.True(Unsafe.AreSame(ref targetBuffer[0], ref internalMemory.Span[0])); + var managerOfExternalMemory = new TestMemoryManager(targetBuffer); - image0.ComparePixelBufferTo(internalMemory.Span); - } + Memory externalMemory = managerOfExternalMemory.Memory; - // Make sure externalMemory works after destruction: - image0.ComparePixelBufferTo(externalMemory.Span); + using (var image1 = Image.WrapMemory(externalMemory, image0.Width, image0.Height)) + { + VerifyMemoryGroupDataMatchesTestPattern(provider, image1.GetPixelMemoryGroup(), image1.Size()); } + + // Make sure externalMemory works after destruction: + VerifyMemoryGroupDataMatchesTestPattern(provider, image0.GetPixelMemoryGroup(), image0.Size()); } - } - [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void GetPixelRowMemory(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage()) + private static void VerifyMemoryGroupDataMatchesTestPattern( + TestImageProvider provider, + IMemoryGroup memoryGroup, + Size size) + where TPixel : struct, IPixel { - var targetBuffer = new TPixel[image.Width * image.Height]; + Assert.True(memoryGroup.IsValid); + Assert.Equal(size.Width * size.Height, memoryGroup.TotalLength); + Assert.True(memoryGroup.BufferLength % size.Width == 0); - // Act: - for (int y = 0; y < image.Height; y++) + int cnt = 0; + for (MemoryGroupIndex i = memoryGroup.MaxIndex(); i < memoryGroup.MaxIndex(); i += 1, cnt++) { - Memory rowMemory = image.GetPixelRowMemory(y); - rowMemory.Span.CopyTo(targetBuffer.AsSpan(image.Width * y)); - } + int y = cnt / size.Width; + int x = cnt % size.Width; - // Assert: - using (Image image1 = provider.GetImage()) - { - // We are using a copy of the original image for assertion - image1.ComparePixelBufferTo(targetBuffer); + TPixel expected = provider.GetExpectedBasicTestPatternPixelAt(x, y); + TPixel actual = memoryGroup.GetElementAt(i); + Assert.Equal(expected, actual); } } } [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void GetPixelRowSpan(TestImageProvider provider) + [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)] + public void GetPixelRowMemory_PixelDataIsCorrect(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - var targetBuffer = new TPixel[image.Width * image.Height]; + provider.LimitAllocatorBufferCapacity(); + + using Image image = provider.GetImage(); + for (int y = 0; y < image.Height; y++) + { // Act: - for (int y = 0; y < image.Height; y++) - { - Span rowMemory = image.GetPixelRowSpan(y); - rowMemory.CopyTo(targetBuffer.AsSpan(image.Width * y)); - } + Memory rowMemory = image.GetPixelRowMemory(y); + Span span = rowMemory.Span; // Assert: - using (Image image1 = provider.GetImage()) + for (int x = 0; x < image.Width; x++) { - // We are using a copy of the original image for assertion - image1.ComparePixelBufferTo(targetBuffer); + Assert.Equal(provider.GetExpectedBasicTestPatternPixelAt(x, y), span[x]); } } } + + [Theory] + [WithBasicTestPatternImages(16, 16, PixelTypes.Rgba32)] + public void GetPixelRowMemory_DestructiveMutate_ShouldInvalidateMemory(TestImageProvider provider) + where TPixel : struct, IPixel + { + using Image image = provider.GetImage(); + + Memory memory3 = image.GetPixelRowMemory(3); + Memory memory10 = image.GetPixelRowMemory(10); + + image.Mutate(c => c.Resize(8, 8)); + + Assert.ThrowsAny(() => _ = memory3.Span); + Assert.ThrowsAny(() => _ = memory10.Span); + } + + [Theory] + [WithBlankImages(1, 1, PixelTypes.Rgba32)] + [WithBlankImages(100, 111, PixelTypes.Rgba32)] + [WithBlankImages(400, 600, PixelTypes.Rgba32)] + public void GetPixelRowSpan_ShouldReferenceSpanOfMemory(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity(); + + using Image image = provider.GetImage(); + + Memory memory = image.GetPixelRowMemory(image.Height - 1); + Span span = image.GetPixelRowSpan(image.Height - 1); + + Assert.True(span == memory.Span); + } } } diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs index 88824baf28..158428f4b0 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs @@ -91,25 +91,25 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers internal static class MemoryGroupIndexExtensions { - public static T GetElementAt(this MemoryGroup group, MemoryGroupIndex idx) + public static T GetElementAt(this IMemoryGroup group, MemoryGroupIndex idx) where T : struct { return group[idx.BufferIndex].Span[idx.ElementIndex]; } - public static void SetElementAt(this MemoryGroup group, MemoryGroupIndex idx, T value) + public static void SetElementAt(this IMemoryGroup group, MemoryGroupIndex idx, T value) where T : struct { group[idx.BufferIndex].Span[idx.ElementIndex] = value; } - public static MemoryGroupIndex MinIndex(this MemoryGroup group) + public static MemoryGroupIndex MinIndex(this IMemoryGroup group) where T : struct { return new MemoryGroupIndex(group.BufferLength, 0, 0); } - public static MemoryGroupIndex MaxIndex(this MemoryGroup group) + public static MemoryGroupIndex MaxIndex(this IMemoryGroup group) where T : struct { return group.Count == 0 diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs index 9100e26e88..1025ed9a15 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs @@ -11,16 +11,26 @@ namespace SixLabors.ImageSharp.Tests { public abstract partial class TestImageProvider : IXunitSerializable { + public virtual TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) + { + throw new NotSupportedException("GetExpectedBasicTestPatternPixelAt(x,y) only works with BasicTestPattern"); + } + private class BasicTestPatternProvider : BlankProvider { + private static readonly TPixel TopLeftColor = Color.Red.ToPixel(); + private static readonly TPixel TopRightColor = Color.Green.ToPixel(); + private static readonly TPixel BottomLeftColor = Color.Blue.ToPixel(); + + // Transparent purple: + private static readonly TPixel BottomRightColor = GetBottomRightColor(); + public BasicTestPatternProvider(int width, int height) : base(width, height) { } - /// - /// This parameterless constructor is needed for xUnit deserialization - /// + // This parameterless constructor is needed for xUnit deserialization public BasicTestPatternProvider() { } @@ -31,14 +41,6 @@ namespace SixLabors.ImageSharp.Tests { var result = new Image(this.Configuration, this.Width, this.Height); - TPixel topLeftColor = Color.Red.ToPixel(); - TPixel topRightColor = Color.Green.ToPixel(); - TPixel bottomLeftColor = Color.Blue.ToPixel(); - - // Transparent purple: - TPixel bottomRightColor = default; - bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f)); - int midY = this.Height / 2; int midX = this.Width / 2; @@ -46,20 +48,42 @@ namespace SixLabors.ImageSharp.Tests { Span row = result.GetPixelRowSpan(y); - row.Slice(0, midX).Fill(topLeftColor); - row.Slice(midX, this.Width - midX).Fill(topRightColor); + row.Slice(0, midX).Fill(TopLeftColor); + row.Slice(midX, this.Width - midX).Fill(TopRightColor); } for (int y = midY; y < this.Height; y++) { Span row = result.GetPixelRowSpan(y); - row.Slice(0, midX).Fill(bottomLeftColor); - row.Slice(midX, this.Width - midX).Fill(bottomRightColor); + row.Slice(0, midX).Fill(BottomLeftColor); + row.Slice(midX, this.Width - midX).Fill(BottomRightColor); } return result; } + + public override TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) + { + int midY = this.Height / 2; + int midX = this.Width / 2; + + if (y < midY) + { + return x < midX ? TopLeftColor : TopRightColor; + } + else + { + return x < midX ? BottomLeftColor : BottomRightColor; + } + } + + private static TPixel GetBottomRightColor() + { + TPixel bottomRightColor = default; + bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f)); + return bottomRightColor; + } } } -} \ No newline at end of file +}