Browse Source

implement correct AdvancedImageExtensions behavior

af/octree-no-pixelmap
Anton Firszov 6 years ago
parent
commit
793a4fb6c1
  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>
/// <exception cref="InvalidOperationException">Thrown when the backing buffer is discontiguous.</exception>
[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)
where TPixel : struct, IPixel<TPixel>
{
@ -109,7 +109,7 @@ namespace SixLabors.ImageSharp.Advanced
/// <returns>The <see cref="Span{TPixel}"/></returns>
/// <exception cref="InvalidOperationException">Thrown when the backing buffer is discontiguous.</exception>
[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)
where TPixel : struct, IPixel<TPixel>
=> source.Frames.RootFrame.GetPixelSpan();
@ -146,9 +146,9 @@ namespace SixLabors.ImageSharp.Advanced
/// <param name="source">The source.</param>
/// <param name="rowIndex">The row.</param>
/// <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>
=> source.PixelBuffer.GetRowMemory(rowIndex);
=> source.PixelBuffer.GetRowMemorySafe(rowIndex);
/// <summary>
/// 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="rowIndex">The row.</param>
/// <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>
=> source.Frames.RootFrame.GetPixelRowMemory(rowIndex);
@ -169,15 +169,5 @@ namespace SixLabors.ImageSharp.Advanced
/// <returns>Returns the configuration.</returns>
internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source)
=> 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;
}
/// <summary>
/// Disposes the <see cref="Buffer2D{T}"/> instance
/// </summary>
public void Dispose()
{
this.MemoryGroup.Dispose();
this.cachedMemory = default;
}
/// <summary>
/// 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>
/// <param name="y">The y (row) coordinate.</param>
/// <returns>The <see cref="Span{T}"/>.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Memory<T> GetRowMemory(int y)
internal Memory<T> GetRowMemoryFast(int y)
{
return this.cachedMemory.Length > 0
? this.cachedMemory.Slice(y * this.Width, this.Width)
@ -97,13 +108,11 @@ namespace SixLabors.ImageSharp.Memory
}
/// <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>
public void Dispose()
{
this.MemoryGroup.Dispose();
this.cachedMemory = default;
}
/// <param name="y">The y (row) coordinate.</param>
/// <returns>The <see cref="Span{T}"/>.</returns>
internal Memory<T> GetRowMemorySafe(int y) => this.MemoryGroup.View.GetBoundedSlice(y * this.Width, this.Width);
/// <summary>
/// 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.
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<TPixel>(TestImageProvider<TPixel> provider)
[WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)]
public void OwnedMemory_PixelDataIsCorrect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image0 = provider.GetImage())
{
var targetBuffer = new TPixel[image0.Width * image0.Height];
provider.LimitAllocatorBufferCapacity();
// Act:
Memory<TPixel> memory = image0.GetRootFramePixelBuffer().GetSingleMemory();
using Image<TPixel> image = provider.GetImage();
// Assert:
Assert.Equal(image0.Width * image0.Height, memory.Length);
memory.Span.CopyTo(targetBuffer);
// Act:
IMemoryGroup<TPixel> memoryGroup = image.GetPixelMemoryGroup();
using (Image<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
[WithBlankImages(16, 16, PixelTypes.Rgba32)]
public void OwnedMemory_DestructiveMutate_ShouldInvalidateMemoryGroup<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 Image<TPixel> image = provider.GetImage();
IMemoryGroup<TPixel> memoryGroup = image.GetPixelMemoryGroup();
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))
{
Memory<TPixel> 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<TPixel>(targetBuffer);
image0.ComparePixelBufferTo(internalMemory.Span);
}
Memory<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
private static void VerifyMemoryGroupDataMatchesTestPattern<TPixel>(
TestImageProvider<TPixel> provider,
IMemoryGroup<TPixel> memoryGroup,
Size size)
where TPixel : struct, IPixel<TPixel>
{
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<TPixel> 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<TPixel> 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<TPixel>(TestImageProvider<TPixel> provider)
[WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)]
[WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)]
public void GetPixelRowMemory_PixelDataIsCorrect<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage())
{
var targetBuffer = new TPixel[image.Width * image.Height];
provider.LimitAllocatorBufferCapacity();
using Image<TPixel> image = provider.GetImage();
for (int y = 0; y < image.Height; y++)
{
// Act:
for (int y = 0; y < image.Height; y++)
{
Span<TPixel> rowMemory = image.GetPixelRowSpan(y);
rowMemory.CopyTo(targetBuffer.AsSpan(image.Width * y));
}
Memory<TPixel> rowMemory = image.GetPixelRowMemory(y);
Span<TPixel> span = rowMemory.Span;
// 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
image1.ComparePixelBufferTo(targetBuffer);
Assert.Equal(provider.GetExpectedBasicTestPatternPixelAt(x, y), span[x]);
}
}
}
[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
{
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
{
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
{
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
{
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
{
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 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<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)
: base(width, height)
{
}
/// <summary>
/// This parameterless constructor is needed for xUnit deserialization
/// </summary>
// This parameterless constructor is needed for xUnit deserialization
public BasicTestPatternProvider()
{
}
@ -31,14 +41,6 @@ namespace SixLabors.ImageSharp.Tests
{
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 midX = this.Width / 2;
@ -46,20 +48,42 @@ namespace SixLabors.ImageSharp.Tests
{
Span<TPixel> 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<TPixel> 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;
}
}
}
}
}

Loading…
Cancel
Save