diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
index 14e676dbe..ce719dc91 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
@@ -7,6 +7,76 @@ namespace SixLabors.ImageSharp.Memory
{
internal static class MemoryGroupExtensions
{
+ ///
+ /// Returns a slice that is expected to be within the bounds of a single buffer.
+ /// Otherwise is thrown.
+ ///
+ public static Memory GetBoundedSlice(this IMemoryGroup group, long start, int length)
+ where T : struct
+ {
+ Guard.NotNull(group, nameof(group));
+ Guard.IsTrue(group.IsValid, nameof(group), "Group must be valid!");
+ Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
+ Guard.MustBeLessThan(start, group.TotalLength, nameof(start));
+
+ int bufferIdx = (int)(start / group.BufferLength);
+ if (bufferIdx >= group.Count)
+ {
+ throw new ArgumentOutOfRangeException(nameof(start));
+ }
+
+ int bufferStart = (int)(start % group.BufferLength);
+ int bufferEnd = bufferStart + length;
+ Memory memory = group[bufferIdx];
+
+ if (bufferEnd > memory.Length)
+ {
+ throw new ArgumentOutOfRangeException(nameof(length));
+ }
+
+ return memory.Slice(bufferStart, length);
+ }
+
+ public static void CopyTo(this IMemoryGroup source, Span target)
+ where T : struct
+ {
+ Guard.NotNull(source, nameof(source));
+ Guard.MustBeGreaterThanOrEqualTo(target.Length, source.TotalLength, nameof(target));
+
+ var cur = new MemoryGroupCursor(source);
+ long position = 0;
+ while (position < source.TotalLength)
+ {
+ int fwd = Math.Min(cur.LookAhead(), target.Length);
+ cur.GetSpan(fwd).CopyTo(target);
+
+ cur.Forward(fwd);
+ target = target.Slice(fwd);
+ position += fwd;
+ }
+ }
+
+ public static void CopyTo(this Span source, IMemoryGroup target)
+ where T : struct
+ => CopyTo((ReadOnlySpan)source, target);
+
+ public static void CopyTo(this ReadOnlySpan source, IMemoryGroup target)
+ where T : struct
+ {
+ Guard.NotNull(target, nameof(target));
+ Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, source.Length, nameof(target));
+
+ var cur = new MemoryGroupCursor(target);
+
+ while (!source.IsEmpty)
+ {
+ int fwd = Math.Min(cur.LookAhead(), source.Length);
+ source.Slice(0, fwd).CopyTo(cur.GetSpan(fwd));
+ cur.Forward(fwd);
+ source = source.Slice(fwd);
+ }
+ }
+
public static void CopyTo(this IMemoryGroup source, IMemoryGroup target)
where T : struct
{
diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs
new file mode 100644
index 000000000..ab69a3077
--- /dev/null
+++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs
@@ -0,0 +1,111 @@
+// Copyright (c) Six Labors and contributors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using SixLabors.ImageSharp.Memory;
+using Xunit;
+
+namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
+{
+ public partial class MemoryGroupTests
+ {
+ public class CopyTo : MemoryGroupTestsBase
+ {
+ public static readonly TheoryData WhenSourceBufferIsShorterOrEqual_Data =
+ CopyAndTransformData;
+
+ [Theory]
+ [MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))]
+ public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen)
+ {
+ using MemoryGroup src = this.CreateTestGroup(srcTotal, srcBufLen, true);
+ using MemoryGroup trg = this.CreateTestGroup(trgTotal, trgBufLen, false);
+
+ src.CopyTo(trg);
+
+ int pos = 0;
+ MemoryGroupIndex i = src.MinIndex();
+ MemoryGroupIndex j = trg.MinIndex();
+ for (; i < src.MaxIndex(); i += 1, j += 1, pos++)
+ {
+ int a = src.GetElementAt(i);
+ int b = trg.GetElementAt(j);
+
+ Assert.True(a == b, $"Mismatch @ {pos} Expected: {a} Actual: {b}");
+ }
+ }
+
+ [Fact]
+ public void WhenTargetBufferTooShort_Throws()
+ {
+ using MemoryGroup src = this.CreateTestGroup(10, 20, true);
+ using MemoryGroup trg = this.CreateTestGroup(5, 20, false);
+
+ Assert.Throws(() => src.CopyTo(trg));
+ }
+
+ [Theory]
+ [InlineData(30, 10, 40)]
+ [InlineData(42, 23, 42)]
+ [InlineData(1, 3, 10)]
+ [InlineData(0, 4, 0)]
+ public void GroupToSpan_Success(long totalLength, int bufferLength, int spanLength)
+ {
+ using MemoryGroup src = this.CreateTestGroup(totalLength, bufferLength, true);
+ var trg = new int[spanLength];
+ src.CopyTo(trg);
+
+ int expected = 1;
+ foreach (int val in trg.AsSpan().Slice(0, (int)totalLength))
+ {
+ Assert.Equal(expected, val);
+ expected++;
+ }
+ }
+
+ [Theory]
+ [InlineData(20, 7, 19)]
+ [InlineData(2, 1, 1)]
+ public void GroupToSpan_OutOfRange(long totalLength, int bufferLength, int spanLength)
+ {
+ using MemoryGroup src = this.CreateTestGroup(totalLength, bufferLength, true);
+ var trg = new int[spanLength];
+ Assert.ThrowsAny(() => src.CopyTo(trg));
+ }
+
+ [Theory]
+ [InlineData(30, 35, 10)]
+ [InlineData(42, 23, 42)]
+ [InlineData(10, 3, 1)]
+ [InlineData(0, 3, 0)]
+ public void SpanToGroup_Success(long totalLength, int bufferLength, int spanLength)
+ {
+ var src = new int[spanLength];
+ for (int i = 0; i < src.Length; i++)
+ {
+ src[i] = i + 1;
+ }
+
+ using MemoryGroup trg = this.CreateTestGroup(totalLength, bufferLength);
+ src.AsSpan().CopyTo(trg);
+
+ int position = 0;
+ for (MemoryGroupIndex i = trg.MinIndex(); position < spanLength; i += 1, position++)
+ {
+ int expected = position + 1;
+ Assert.Equal(expected, trg.GetElementAt(i));
+ }
+ }
+
+ [Theory]
+ [InlineData(10, 3, 11)]
+ [InlineData(0, 3, 1)]
+ public void SpanToGroup_OutOfRange(long totalLength, int bufferLength, int spanLength)
+ {
+ var src = new int[spanLength];
+ using MemoryGroup trg = this.CreateTestGroup(totalLength, bufferLength, true);
+ Assert.ThrowsAny(() => src.AsSpan().CopyTo(trg));
+ }
+ }
+ }
+}
diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs
index 6b0737742..bf081cb55 100644
--- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs
+++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs
@@ -44,42 +44,6 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
{ 30, 5, 40, 12 },
};
- public class CopyTo : MemoryGroupTestsBase
- {
- public static readonly TheoryData WhenSourceBufferIsShorterOrEqual_Data =
- CopyAndTransformData;
-
- [Theory]
- [MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))]
- public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen)
- {
- using MemoryGroup src = this.CreateTestGroup(srcTotal, srcBufLen, true);
- using MemoryGroup trg = this.CreateTestGroup(trgTotal, trgBufLen, false);
-
- src.CopyTo(trg);
-
- int pos = 0;
- MemoryGroupIndex i = src.MinIndex();
- MemoryGroupIndex j = trg.MinIndex();
- for (; i < src.MaxIndex(); i += 1, j += 1, pos++)
- {
- int a = src.GetElementAt(i);
- int b = trg.GetElementAt(j);
-
- Assert.True(a == b, $"Mismatch @ {pos} Expected: {a} Actual: {b}");
- }
- }
-
- [Fact]
- public void WhenTargetBufferTooShort_Throws()
- {
- using MemoryGroup src = this.CreateTestGroup(10, 20, true);
- using MemoryGroup trg = this.CreateTestGroup(5, 20, false);
-
- Assert.Throws(() => src.CopyTo(trg));
- }
- }
-
public class TransformTo : MemoryGroupTestsBase
{
public static readonly TheoryData WhenSourceBufferIsShorterOrEqual_Data =
@@ -154,6 +118,51 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
Assert.True(group[2].Span.SequenceEqual(data2));
}
+ public static TheoryData GetBoundedSlice_SuccessData = new TheoryData()
+ {
+ { 300, 100, 110, 80 },
+ { 300, 100, 100, 100 },
+ { 280, 100, 201, 79 },
+ { 42, 7, 0, 0 },
+ { 42, 7, 0, 1 },
+ { 42, 7, 0, 7 },
+ { 42, 9, 9, 9 },
+ };
+
+ [Theory]
+ [MemberData(nameof(GetBoundedSlice_SuccessData))]
+ public void GetBoundedSlice_WhenArgsAreCorrect(long totalLength, int bufferLength, long start, int length)
+ {
+ using MemoryGroup group = this.CreateTestGroup(totalLength, bufferLength, true);
+
+ Memory slice = group.GetBoundedSlice(start, length);
+
+ Assert.Equal(length, slice.Length);
+
+ int expected = (int)start + 1;
+ foreach (int val in slice.Span)
+ {
+ Assert.Equal(expected, val);
+ expected++;
+ }
+ }
+
+ public static TheoryData GetBoundedSlice_ErrorData = new TheoryData()
+ {
+ { 300, 100, 110, 91 },
+ { 42, 7, 0, 8 },
+ { 42, 7, 1, 7 },
+ { 42, 7, 1, 30 },
+ };
+
+ [Theory]
+ [MemberData(nameof(GetBoundedSlice_ErrorData))]
+ public void GetBoundedSlice_WhenOverlapsBuffers_Throws(long totalLength, int bufferLength, long start, int length)
+ {
+ using MemoryGroup group = this.CreateTestGroup(totalLength, bufferLength, true);
+ Assert.ThrowsAny(() => group.GetBoundedSlice(start, length));
+ }
+
private static void MultiplyAllBy2(ReadOnlySpan source, Span target)
{
Assert.Equal(source.Length, target.Length);