Browse Source

Merge pull request #1314 from SixLabors/sp/byte-to-tpixel-wrapping

Image.WrapMemory<TPixel> APIs wrapping Memory<byte>
js/color-alpha-handling
James Jackson-South 6 years ago
committed by GitHub
parent
commit
2bdf2a34aa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 82
      src/ImageSharp/Image.WrapMemory.cs
  2. 57
      src/ImageSharp/Memory/ByteMemoryManager{T}.cs
  3. 30
      src/ImageSharp/Memory/MemoryOwnerExtensions.cs
  4. 2
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  5. 164
      tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs

82
src/ImageSharp/Image.WrapMemory.cs

@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp
{
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
@ -38,6 +38,7 @@ namespace SixLabors.ImageSharp
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
Guard.IsTrue(pixelMemory.Length == width * height, nameof(pixelMemory), "The length of the input memory doesn't match the specified image size");
var memorySource = MemoryGroup<TPixel>.Wrap(pixelMemory);
return new Image<TPixel>(configuration, memorySource, width, height, metadata);
@ -45,7 +46,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
@ -64,7 +65,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The memory is being observed, the caller remains responsible for managing it's lifecycle.
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
@ -81,7 +82,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The ownership of the <paramref name="pixelMemoryOwner"/> is being transferred to the new <see cref="Image{TPixel}"/> instance,
/// meaning that the caller is not allowed to dispose <paramref name="pixelMemoryOwner"/>.
/// It will be disposed together with the result image.
@ -105,6 +106,7 @@ namespace SixLabors.ImageSharp
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
Guard.IsTrue(pixelMemoryOwner.Memory.Length == width * height, nameof(pixelMemoryOwner), "The length of the input memory doesn't match the specified image size");
var memorySource = MemoryGroup<TPixel>.Wrap(pixelMemoryOwner);
return new Image<TPixel>(configuration, memorySource, width, height, metadata);
@ -112,7 +114,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The ownership of the <paramref name="pixelMemoryOwner"/> is being transferred to the new <see cref="Image{TPixel}"/> instance,
/// meaning that the caller is not allowed to dispose <paramref name="pixelMemoryOwner"/>.
/// It will be disposed together with the result image.
@ -134,7 +136,7 @@ namespace SixLabors.ImageSharp
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The ownership of the <paramref name="pixelMemoryOwner"/> is being transferred to the new <see cref="Image{TPixel}"/> instance,
/// meaning that the caller is not allowed to dispose <paramref name="pixelMemoryOwner"/>.
/// It will be disposed together with the result image.
@ -150,5 +152,73 @@ namespace SixLabors.ImageSharp
int height)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(Configuration.Default, pixelMemoryOwner, width, height);
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <param name="metadata">The <see cref="ImageMetadata"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentNullException">The metadata is null.</exception>
/// <returns>An <see cref="Image{TPixel}"/> instance</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<byte> byteMemory,
int width,
int height,
ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));
var memoryManager = new ByteMemoryManager<TPixel>(byteMemory);
Guard.IsTrue(memoryManager.Memory.Length == width * height, nameof(byteMemory), "The length of the input memory doesn't match the specified image size");
var memorySource = MemoryGroup<TPixel>.Wrap(memoryManager.Memory);
return new Image<TPixel>(configuration, memorySource, width, height, metadata);
}
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<byte> byteMemory,
int width,
int height)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, byteMemory, width, height, new ImageMetadata());
/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an <see cref="Image{TPixel}"/> instance.
/// The memory is being observed, the caller remains responsible for managing it's lifecycle.
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Memory<byte> byteMemory,
int width,
int height)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, byteMemory, width, height);
}
}

57
src/ImageSharp/Memory/ByteMemoryManager{T}.cs

@ -0,0 +1,57 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// A custom <see cref="MemoryManager{T}"/> that can wrap <see cref="Memory{T}"/> of <see cref="byte"/> instances
/// and cast them to be <see cref="Memory{T}"/> for any arbitrary unmanaged <typeparamref name="T"/> value type.
/// </summary>
/// <typeparam name="T">The value type to use when casting the wrapped <see cref="Memory{T}"/> instance.</typeparam>
internal sealed class ByteMemoryManager<T> : MemoryManager<T>
where T : unmanaged
{
/// <summary>
/// The wrapped <see cref="Memory{T}"/> of <see cref="byte"/> instance.
/// </summary>
private readonly Memory<byte> memory;
/// <summary>
/// Initializes a new instance of the <see cref="ByteMemoryManager{T}"/> class.
/// </summary>
/// <param name="memory">The <see cref="Memory{T}"/> of <see cref="byte"/> instance to wrap.</param>
public ByteMemoryManager(Memory<byte> memory)
{
this.memory = memory;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
/// <inheritdoc/>
public override Span<T> GetSpan()
{
return MemoryMarshal.Cast<byte, T>(this.memory.Span);
}
/// <inheritdoc/>
public override MemoryHandle Pin(int elementIndex = 0)
{
// We need to adjust the offset into the wrapped byte segment,
// as the input index refers to the target-cast memory of T.
// We just have to shift this index by the byte size of T.
return this.memory.Slice(elementIndex * Unsafe.SizeOf<T>()).Pin();
}
/// <inheritdoc/>
public override void Unpin()
{
}
}
}

30
src/ImageSharp/Memory/MemoryOwnerExtensions.cs

@ -13,13 +13,27 @@ namespace SixLabors.ImageSharp.Memory
/// </summary>
internal static class MemoryOwnerExtensions
{
/// <summary>
/// Gets a <see cref="Span{T}"/> from an <see cref="IMemoryOwner{T}"/> instance.
/// </summary>
/// <param name="buffer">The buffer</param>
/// <returns>The <see cref="Span{T}"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Span<T> GetSpan<T>(this IMemoryOwner<T> buffer)
=> buffer.Memory.Span;
{
return buffer.Memory.Span;
}
/// <summary>
/// Gets the length of an <see cref="IMemoryOwner{T}"/> internal buffer.
/// </summary>
/// <param name="buffer">The buffer</param>
/// <returns>The length of the buffer</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Length<T>(this IMemoryOwner<T> buffer)
=> buffer.GetSpan().Length;
{
return buffer.Memory.Length;
}
/// <summary>
/// Gets a <see cref="Span{T}"/> to an offsetted position inside the buffer.
@ -56,8 +70,16 @@ namespace SixLabors.ImageSharp.Memory
buffer.GetSpan().Clear();
}
/// <summary>
/// Gets a reference to the first item in the internal buffer for an <see cref="IMemoryOwner{T}"/> instance.
/// </summary>
/// <param name="buffer">The buffer</param>
/// <returns>A reference to the first item within the memory wrapped by <paramref name="buffer"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ref T GetReference<T>(this IMemoryOwner<T> buffer)
where T : struct =>
ref MemoryMarshal.GetReference(buffer.GetSpan());
where T : struct
{
return ref MemoryMarshal.GetReference(buffer.GetSpan());
}
}
}

2
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -262,7 +262,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Dithering
this.source = source;
this.bounds = bounds;
this.scale = processor.DitherScale;
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Span.Length);
this.bitDepth = ImageMaths.GetBitsNeededForColorDepth(processor.Palette.Length);
}
[MethodImpl(InliningOptions.ShortMethod)]

164
tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs

@ -6,10 +6,10 @@ using System.Buffers;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@ -80,10 +80,52 @@ namespace SixLabors.ImageSharp.Tests
}
}
public sealed class CastMemoryManager<TFrom, TTo> : MemoryManager<TTo>
where TFrom : unmanaged
where TTo : unmanaged
{
private readonly Memory<TFrom> memory;
public CastMemoryManager(Memory<TFrom> memory)
{
this.memory = memory;
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}
/// <inheritdoc/>
public override Span<TTo> GetSpan()
{
return MemoryMarshal.Cast<TFrom, TTo>(this.memory.Span);
}
/// <inheritdoc/>
public override MemoryHandle Pin(int elementIndex = 0)
{
int byteOffset = elementIndex * Unsafe.SizeOf<TTo>();
int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf<TFrom>(), out int remainder);
if (remainder != 0)
{
ThrowHelper.ThrowArgumentException("The input index doesn't result in an aligned item access", nameof(elementIndex));
}
return this.memory.Slice(shiftedOffset).Pin();
}
/// <inheritdoc/>
public override void Unpin()
{
}
}
[Fact]
public void WrapMemory_CreatedImageIsCorrect()
{
Configuration cfg = Configuration.Default.Clone();
var cfg = Configuration.CreateDefaultInstance();
var metaData = new ImageMetadata();
var array = new Rgba32[25];
@ -173,6 +215,124 @@ namespace SixLabors.ImageSharp.Tests
}
}
[Fact]
public void WrapMemory_FromBytes_CreatedImageIsCorrect()
{
var cfg = Configuration.CreateDefaultInstance();
var metaData = new ImageMetadata();
var array = new byte[25 * Unsafe.SizeOf<Rgba32>()];
var memory = new Memory<byte>(array);
using (var image = Image.WrapMemory<Rgba32>(cfg, memory, 5, 5, metaData))
{
Assert.True(image.TryGetSinglePixelSpan(out Span<Rgba32> imageSpan));
ref Rgba32 pixel0 = ref imageSpan[0];
Assert.True(Unsafe.AreSame(ref Unsafe.As<byte, Rgba32>(ref array[0]), ref pixel0));
Assert.Equal(cfg, image.GetConfiguration());
Assert.Equal(metaData, image.Metadata);
}
}
[Fact]
public void WrapSystemDrawingBitmap_FromBytes_WhenObserved()
{
if (ShouldSkipBitmapTest)
{
return;
}
using (var bmp = new Bitmap(51, 23))
{
using (var memoryManager = new BitmapMemoryManager(bmp))
{
Memory<Bgra32> pixelMemory = memoryManager.Memory;
Memory<byte> byteMemory = new CastMemoryManager<Bgra32, byte>(pixelMemory).Memory;
Bgra32 bg = Color.Red;
Bgra32 fg = Color.Green;
using (var image = Image.WrapMemory<Bgra32>(byteMemory, bmp.Width, bmp.Height))
{
Span<Bgra32> pixelSpan = pixelMemory.Span;
Span<Bgra32> imageSpan = image.GetRootFramePixelBuffer().GetSingleMemory().Span;
// We can't compare the two Memory<T> instances directly as they wrap different memory managers.
// To check that the underlying data matches, we can just manually check their lenth, and the
// fact that a reference to the first pixel in both spans is actually the same memory location.
Assert.Equal(pixelSpan.Length, imageSpan.Length);
Assert.True(Unsafe.AreSame(ref pixelSpan.GetPinnableReference(), ref imageSpan.GetPinnableReference()));
Assert.True(image.TryGetSinglePixelSpan(out imageSpan));
imageSpan.Fill(bg);
for (var i = 10; i < 20; i++)
{
image.GetPixelRowSpan(i).Slice(10, 10).Fill(fg);
}
}
Assert.False(memoryManager.IsDisposed);
}
string fn = System.IO.Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
$"{nameof(this.WrapSystemDrawingBitmap_WhenObserved)}.bmp");
bmp.Save(fn, ImageFormat.Bmp);
}
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1023, 32, 32)]
public void WrapMemory_MemoryOfT_InvalidSize(int size, int height, int width)
{
var array = new Rgba32[size];
var memory = new Memory<Rgba32>(array);
Assert.Throws<ArgumentException>(() => Image.WrapMemory(memory, height, width));
}
private class TestMemoryOwner<T> : IMemoryOwner<T>
{
public Memory<T> Memory { get; set; }
public void Dispose()
{
}
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1023, 32, 32)]
public void WrapMemory_IMemoryOwnerOfT_InvalidSize(int size, int height, int width)
{
var array = new Rgba32[size];
var memory = new TestMemoryOwner<Rgba32> { Memory = array };
Assert.Throws<ArgumentException>(() => Image.WrapMemory(memory, height, width));
}
[Theory]
[InlineData(0, 5, 5)]
[InlineData(20, 5, 5)]
[InlineData(26, 5, 5)]
[InlineData(2, 1, 1)]
[InlineData(1023, 32, 32)]
public void WrapMemory_MemoryOfByte_InvalidSize(int size, int height, int width)
{
var array = new byte[size * Unsafe.SizeOf<Rgba32>()];
var memory = new Memory<byte>(array);
Assert.Throws<ArgumentException>(() => Image.WrapMemory<Rgba32>(memory, height, width));
}
private static bool ShouldSkipBitmapTest =>
!TestEnvironment.Is64BitProcess || (TestHelpers.ImageSharpBuiltAgainst != "netcoreapp3.1" && TestHelpers.ImageSharpBuiltAgainst != "netcoreapp2.1");
}

Loading…
Cancel
Save