Browse Source

implement correct AdvancedImageExtensions behavior

pull/1109/head
Anton Firszov 6 years ago
parent
commit
48c6f3f667
  1. 20
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  2. 23
      src/ImageSharp/Memory/Buffer2D{T}.cs
  3. 173
      tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs
  4. 8
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs
  5. 56
      tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs

20
src/ImageSharp/Advanced/AdvancedImageExtensions.cs

@ -87,7 +87,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <returns>The <see cref="Span{TPixel}"/></returns> /// <returns>The <see cref="Span{TPixel}"/></returns>
/// <exception cref="InvalidOperationException">Thrown when the backing buffer is discontiguous.</exception> /// <exception cref="InvalidOperationException">Thrown when the backing buffer is discontiguous.</exception>
[Obsolete( [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<TPixel> GetPixelSpan<TPixel>(this ImageFrame<TPixel> source) public static Span<TPixel> GetPixelSpan<TPixel>(this ImageFrame<TPixel> source)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <returns>The <see cref="Span{TPixel}"/></returns> /// <returns>The <see cref="Span{TPixel}"/></returns>
/// <exception cref="InvalidOperationException">Thrown when the backing buffer is discontiguous.</exception> /// <exception cref="InvalidOperationException">Thrown when the backing buffer is discontiguous.</exception>
[Obsolete( [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<TPixel> GetPixelSpan<TPixel>(this Image<TPixel> source) public static Span<TPixel> GetPixelSpan<TPixel>(this Image<TPixel> source)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.Frames.RootFrame.GetPixelSpan(); => source.Frames.RootFrame.GetPixelSpan();
@ -146,9 +146,9 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="source">The source.</param> /// <param name="source">The source.</param>
/// <param name="rowIndex">The row.</param> /// <param name="rowIndex">The row.</param>
/// <returns>The <see cref="Span{TPixel}"/></returns> /// <returns>The <see cref="Span{TPixel}"/></returns>
internal static Memory<TPixel> GetPixelRowMemory<TPixel>(this ImageFrame<TPixel> source, int rowIndex) public static Memory<TPixel> GetPixelRowMemory<TPixel>(this ImageFrame<TPixel> source, int rowIndex)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.PixelBuffer.GetRowMemory(rowIndex); => source.PixelBuffer.GetRowMemorySafe(rowIndex);
/// <summary> /// <summary>
/// Gets the representation of the pixels as <see cref="Span{T}"/> of of contiguous memory /// Gets the representation of the pixels as <see cref="Span{T}"/> of of contiguous memory
@ -158,7 +158,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="source">The source.</param> /// <param name="source">The source.</param>
/// <param name="rowIndex">The row.</param> /// <param name="rowIndex">The row.</param>
/// <returns>The <see cref="Span{TPixel}"/></returns> /// <returns>The <see cref="Span{TPixel}"/></returns>
internal static Memory<TPixel> GetPixelRowMemory<TPixel>(this Image<TPixel> source, int rowIndex) public static Memory<TPixel> GetPixelRowMemory<TPixel>(this Image<TPixel> source, int rowIndex)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
=> source.Frames.RootFrame.GetPixelRowMemory(rowIndex); => source.Frames.RootFrame.GetPixelRowMemory(rowIndex);
@ -169,15 +169,5 @@ namespace SixLabors.ImageSharp.Advanced
/// <returns>Returns the configuration.</returns> /// <returns>Returns the configuration.</returns>
internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source) internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source)
=> GetConfiguration(source).MemoryAllocator; => GetConfiguration(source).MemoryAllocator;
/// <summary>
/// Returns a reference to the 0th element of the Pixel buffer.
/// Such a reference can be used for pinning but must never be dereferenced.
/// </summary>
/// <param name="source">The source image frame</param>
/// <returns>A reference to the element.</returns>
private static ref TPixel DangerousGetPinnableReferenceToPixelBuffer<TPixel>(IPixelSource<TPixel> source)
where TPixel : struct, IPixel<TPixel>
=> ref MemoryMarshal.GetReference(source.PixelBuffer.GetSingleSpan());
} }
} }

23
src/ImageSharp/Memory/Buffer2D{T}.cs

@ -83,13 +83,24 @@ namespace SixLabors.ImageSharp.Memory
: this.GetRowMemorySlow(y).Span; : this.GetRowMemorySlow(y).Span;
} }
/// <summary>
/// Disposes the <see cref="Buffer2D{T}"/> instance
/// </summary>
public void Dispose()
{
this.MemoryGroup.Dispose();
this.cachedMemory = default;
}
/// <summary> /// <summary>
/// Gets a <see cref="Memory{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row. /// Gets a <see cref="Memory{T}"/> 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
/// <see cref="MemoryGroupView{T}"/>.
/// </summary> /// </summary>
/// <param name="y">The y (row) coordinate.</param> /// <param name="y">The y (row) coordinate.</param>
/// <returns>The <see cref="Span{T}"/>.</returns> /// <returns>The <see cref="Span{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Memory<T> GetRowMemory(int y) internal Memory<T> GetRowMemoryFast(int y)
{ {
return this.cachedMemory.Length > 0 return this.cachedMemory.Length > 0
? this.cachedMemory.Slice(y * this.Width, this.Width) ? this.cachedMemory.Slice(y * this.Width, this.Width)
@ -97,13 +108,11 @@ namespace SixLabors.ImageSharp.Memory
} }
/// <summary> /// <summary>
/// Disposes the <see cref="Buffer2D{T}"/> instance /// Gets a <see cref="Memory{T}"/> to the row 'y' beginning from the pixel at the first pixel on that row.
/// </summary> /// </summary>
public void Dispose() /// <param name="y">The y (row) coordinate.</param>
{ /// <returns>The <see cref="Span{T}"/>.</returns>
this.MemoryGroup.Dispose(); internal Memory<T> GetRowMemorySafe(int y) => this.MemoryGroup.View.GetBoundedSlice(y * this.Width, this.Width);
this.cachedMemory = default;
}
/// <summary> /// <summary>
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1), /// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),

173
tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs

@ -2,10 +2,13 @@
// Licensed under the Apache License, Version 2.0. // Licensed under the Apache License, Version 2.0.
using System; using System;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers;
using Xunit; using Xunit;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -13,113 +16,145 @@ namespace SixLabors.ImageSharp.Tests.Advanced
{ {
public class AdvancedImageExtensionsTests public class AdvancedImageExtensionsTests
{ {
public class GetPixelMemory public class GetPixelMemoryGroup
{ {
[Theory] [Theory]
[WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)]
[WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] [WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)]
public void WhenMemoryIsOwned<TPixel>(TestImageProvider<TPixel> provider) [WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)]
public void OwnedMemory_PixelDataIsCorrect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image0 = provider.GetImage()) provider.LimitAllocatorBufferCapacity();
{
var targetBuffer = new TPixel[image0.Width * image0.Height];
// Act: using Image<TPixel> image = provider.GetImage();
Memory<TPixel> memory = image0.GetRootFramePixelBuffer().GetSingleMemory();
// Assert: // Act:
Assert.Equal(image0.Width * image0.Height, memory.Length); IMemoryGroup<TPixel> memoryGroup = image.GetPixelMemoryGroup();
memory.Span.CopyTo(targetBuffer);
using (Image<TPixel> image1 = provider.GetImage()) // Assert:
{ VerifyMemoryGroupDataMatchesTestPattern(provider, memoryGroup, image.Size());
// We are using a copy of the original image for assertion
image1.ComparePixelBufferTo(targetBuffer);
}
}
} }
[Theory] [Theory]
[WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32 | PixelTypes.Bgr24)] [WithBlankImages(16, 16, PixelTypes.Rgba32)]
[WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] public void OwnedMemory_DestructiveMutate_ShouldInvalidateMemoryGroup<TPixel>(TestImageProvider<TPixel> provider)
public void WhenMemoryIsConsumed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image0 = provider.GetImage()) using Image<TPixel> image = provider.GetImage();
{
var targetBuffer = new TPixel[image0.Width * image0.Height]; IMemoryGroup<TPixel> memoryGroup = image.GetPixelMemoryGroup();
image0.GetPixelSpan().CopyTo(targetBuffer); Memory<TPixel> memory = memoryGroup.Single();
var managerOfExternalMemory = new TestMemoryManager<TPixel>(targetBuffer); image.Mutate(c => c.Resize(8, 8));
Memory<TPixel> externalMemory = managerOfExternalMemory.Memory; Assert.False(memoryGroup.IsValid);
Assert.ThrowsAny<InvalidMemoryOperationException>(() => _ = memoryGroup.First());
Assert.ThrowsAny<InvalidMemoryOperationException>(() => _ = memory.Span);
}
[Theory]
[WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(131, 127, PixelTypes.Bgr24)]
public void ConsumedMemory_PixelDataIsCorrect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using Image<TPixel> 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)) var managerOfExternalMemory = new TestMemoryManager<TPixel>(targetBuffer);
{
Memory<TPixel> internalMemory = image1.GetRootFramePixelBuffer().GetSingleMemory();
Assert.Equal(targetBuffer.Length, internalMemory.Length);
Assert.True(Unsafe.AreSame(ref targetBuffer[0], ref internalMemory.Span[0]));
image0.ComparePixelBufferTo(internalMemory.Span); Memory<TPixel> externalMemory = managerOfExternalMemory.Memory;
}
// Make sure externalMemory works after destruction: using (var image1 = Image.WrapMemory(externalMemory, image0.Width, image0.Height))
image0.ComparePixelBufferTo(externalMemory.Span); {
VerifyMemoryGroupDataMatchesTestPattern(provider, image1.GetPixelMemoryGroup(), image1.Size());
} }
// Make sure externalMemory works after destruction:
VerifyMemoryGroupDataMatchesTestPattern(provider, image0.GetPixelMemoryGroup(), image0.Size());
} }
}
[Theory] private static void VerifyMemoryGroupDataMatchesTestPattern<TPixel>(
[WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] TestImageProvider<TPixel> provider,
[WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] IMemoryGroup<TPixel> memoryGroup,
public void GetPixelRowMemory<TPixel>(TestImageProvider<TPixel> provider) Size size)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{ {
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: int cnt = 0;
for (int y = 0; y < image.Height; y++) for (MemoryGroupIndex i = memoryGroup.MaxIndex(); i < memoryGroup.MaxIndex(); i += 1, cnt++)
{ {
Memory<TPixel> rowMemory = image.GetPixelRowMemory(y); int y = cnt / size.Width;
rowMemory.Span.CopyTo(targetBuffer.AsSpan(image.Width * y)); int x = cnt % size.Width;
}
// Assert: TPixel expected = provider.GetExpectedBasicTestPatternPixelAt(x, y);
using (Image<TPixel> image1 = provider.GetImage()) TPixel actual = memoryGroup.GetElementAt(i);
{ Assert.Equal(expected, actual);
// We are using a copy of the original image for assertion
image1.ComparePixelBufferTo(targetBuffer);
} }
} }
} }
[Theory] [Theory]
[WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)]
[WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] [WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)]
public void GetPixelRowSpan<TPixel>(TestImageProvider<TPixel> provider) [WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)]
public void GetPixelRowMemory_PixelDataIsCorrect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel> where TPixel : struct, IPixel<TPixel>
{ {
using (Image<TPixel> image = provider.GetImage()) provider.LimitAllocatorBufferCapacity();
{
var targetBuffer = new TPixel[image.Width * image.Height]; using Image<TPixel> image = provider.GetImage();
for (int y = 0; y < image.Height; y++)
{
// Act: // Act:
for (int y = 0; y < image.Height; y++) Memory<TPixel> rowMemory = image.GetPixelRowMemory(y);
{ Span<TPixel> span = rowMemory.Span;
Span<TPixel> rowMemory = image.GetPixelRowSpan(y);
rowMemory.CopyTo(targetBuffer.AsSpan(image.Width * y));
}
// Assert: // Assert:
using (Image<TPixel> image1 = provider.GetImage()) for (int x = 0; x < image.Width; x++)
{ {
// We are using a copy of the original image for assertion Assert.Equal(provider.GetExpectedBasicTestPatternPixelAt(x, y), span[x]);
image1.ComparePixelBufferTo(targetBuffer);
} }
} }
} }
[Theory]
[WithBasicTestPatternImages(16, 16, PixelTypes.Rgba32)]
public void GetPixelRowMemory_DestructiveMutate_ShouldInvalidateMemory<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
Memory<TPixel> memory3 = image.GetPixelRowMemory(3);
Memory<TPixel> memory10 = image.GetPixelRowMemory(10);
image.Mutate(c => c.Resize(8, 8));
Assert.ThrowsAny<InvalidMemoryOperationException>(() => _ = memory3.Span);
Assert.ThrowsAny<InvalidMemoryOperationException>(() => _ = memory10.Span);
}
[Theory]
[WithBlankImages(1, 1, PixelTypes.Rgba32)]
[WithBlankImages(100, 111, PixelTypes.Rgba32)]
[WithBlankImages(400, 600, PixelTypes.Rgba32)]
public void GetPixelRowSpan_ShouldReferenceSpanOfMemory<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
provider.LimitAllocatorBufferCapacity();
using Image<TPixel> image = provider.GetImage();
Memory<TPixel> memory = image.GetPixelRowMemory(image.Height - 1);
Span<TPixel> span = image.GetPixelRowSpan(image.Height - 1);
Assert.True(span == memory.Span);
}
} }
} }

8
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs

@ -91,25 +91,25 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
internal static class MemoryGroupIndexExtensions internal static class MemoryGroupIndexExtensions
{ {
public static T GetElementAt<T>(this MemoryGroup<T> group, MemoryGroupIndex idx) public static T GetElementAt<T>(this IMemoryGroup<T> group, MemoryGroupIndex idx)
where T : struct where T : struct
{ {
return group[idx.BufferIndex].Span[idx.ElementIndex]; return group[idx.BufferIndex].Span[idx.ElementIndex];
} }
public static void SetElementAt<T>(this MemoryGroup<T> group, MemoryGroupIndex idx, T value) public static void SetElementAt<T>(this IMemoryGroup<T> group, MemoryGroupIndex idx, T value)
where T : struct where T : struct
{ {
group[idx.BufferIndex].Span[idx.ElementIndex] = value; group[idx.BufferIndex].Span[idx.ElementIndex] = value;
} }
public static MemoryGroupIndex MinIndex<T>(this MemoryGroup<T> group) public static MemoryGroupIndex MinIndex<T>(this IMemoryGroup<T> group)
where T : struct where T : struct
{ {
return new MemoryGroupIndex(group.BufferLength, 0, 0); return new MemoryGroupIndex(group.BufferLength, 0, 0);
} }
public static MemoryGroupIndex MaxIndex<T>(this MemoryGroup<T> group) public static MemoryGroupIndex MaxIndex<T>(this IMemoryGroup<T> group)
where T : struct where T : struct
{ {
return group.Count == 0 return group.Count == 0

56
tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs

@ -11,16 +11,26 @@ namespace SixLabors.ImageSharp.Tests
{ {
public abstract partial class TestImageProvider<TPixel> : IXunitSerializable public abstract partial class TestImageProvider<TPixel> : IXunitSerializable
{ {
public virtual TPixel GetExpectedBasicTestPatternPixelAt(int x, int y)
{
throw new NotSupportedException("GetExpectedBasicTestPatternPixelAt(x,y) only works with BasicTestPattern");
}
private class BasicTestPatternProvider : BlankProvider private class BasicTestPatternProvider : BlankProvider
{ {
private static readonly TPixel TopLeftColor = Color.Red.ToPixel<TPixel>();
private static readonly TPixel TopRightColor = Color.Green.ToPixel<TPixel>();
private static readonly TPixel BottomLeftColor = Color.Blue.ToPixel<TPixel>();
// Transparent purple:
private static readonly TPixel BottomRightColor = GetBottomRightColor();
public BasicTestPatternProvider(int width, int height) public BasicTestPatternProvider(int width, int height)
: base(width, height) : base(width, height)
{ {
} }
/// <summary> // This parameterless constructor is needed for xUnit deserialization
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
public BasicTestPatternProvider() public BasicTestPatternProvider()
{ {
} }
@ -31,14 +41,6 @@ namespace SixLabors.ImageSharp.Tests
{ {
var result = new Image<TPixel>(this.Configuration, this.Width, this.Height); var result = new Image<TPixel>(this.Configuration, this.Width, this.Height);
TPixel topLeftColor = Color.Red.ToPixel<TPixel>();
TPixel topRightColor = Color.Green.ToPixel<TPixel>();
TPixel bottomLeftColor = Color.Blue.ToPixel<TPixel>();
// Transparent purple:
TPixel bottomRightColor = default;
bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f));
int midY = this.Height / 2; int midY = this.Height / 2;
int midX = this.Width / 2; int midX = this.Width / 2;
@ -46,20 +48,42 @@ namespace SixLabors.ImageSharp.Tests
{ {
Span<TPixel> row = result.GetPixelRowSpan(y); Span<TPixel> row = result.GetPixelRowSpan(y);
row.Slice(0, midX).Fill(topLeftColor); row.Slice(0, midX).Fill(TopLeftColor);
row.Slice(midX, this.Width - midX).Fill(topRightColor); row.Slice(midX, this.Width - midX).Fill(TopRightColor);
} }
for (int y = midY; y < this.Height; y++) for (int y = midY; y < this.Height; y++)
{ {
Span<TPixel> row = result.GetPixelRowSpan(y); Span<TPixel> row = result.GetPixelRowSpan(y);
row.Slice(0, midX).Fill(bottomLeftColor); row.Slice(0, midX).Fill(BottomLeftColor);
row.Slice(midX, this.Width - midX).Fill(bottomRightColor); row.Slice(midX, this.Width - midX).Fill(BottomRightColor);
} }
return result; 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;
}
} }
} }
} }

Loading…
Cancel
Save