Browse Source

Merge branch 'main' into js/accumulative-memory-limit

pull/3056/head
James Jackson-South 1 month ago
committed by GitHub
parent
commit
19130efb42
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs
  2. 8
      src/ImageSharp/Advanced/ParallelRowIterator.cs
  3. 9
      src/ImageSharp/Formats/Png/Chunks/FrameControl.cs
  4. 13
      src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs
  5. 198
      src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs
  6. 16
      src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
  7. 16
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs

4
src/ImageSharp/Advanced/IRowOperation{TBuffer}.cs

@ -15,12 +15,12 @@ public interface IRowOperation<TBuffer>
/// </summary>
/// <param name="bounds">The bounds of the operation.</param>
/// <returns>The required buffer length.</returns>
int GetRequiredBufferLength(Rectangle bounds);
public int GetRequiredBufferLength(Rectangle bounds);
/// <summary>
/// Invokes the method passing the row and a buffer.
/// </summary>
/// <param name="y">The row y coordinate.</param>
/// <param name="span">The contiguous region of memory.</param>
void Invoke(int y, Span<TBuffer> span);
public void Invoke(int y, Span<TBuffer> span);
}

8
src/ImageSharp/Advanced/ParallelRowIterator.cs

@ -68,7 +68,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -138,7 +138,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -195,7 +195,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T> wrappingOperation = new(top, bottom, verticalStep, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,
@ -262,7 +262,7 @@ public static partial class ParallelRowIterator
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = numOfSteps };
RowIntervalOperationWrapper<T, TBuffer> wrappingOperation = new(top, bottom, verticalStep, bufferLength, allocator, in operation);
Parallel.For(
_ = Parallel.For(
0,
numOfSteps,
parallelOptions,

9
src/ImageSharp/Formats/Png/Chunks/FrameControl.cs

@ -147,7 +147,13 @@ internal readonly struct FrameControl
/// <param name="data">The data to parse.</param>
/// <returns>The parsed fcTL.</returns>
public static FrameControl Parse(ReadOnlySpan<byte> data)
=> new(
{
if (data.Length < Size)
{
PngThrowHelper.ThrowInvalidImageContentException("The frame control chunk does not contain enough data!");
}
return new(
sequenceNumber: BinaryPrimitives.ReadUInt32BigEndian(data[..4]),
width: BinaryPrimitives.ReadUInt32BigEndian(data[4..8]),
height: BinaryPrimitives.ReadUInt32BigEndian(data[8..12]),
@ -157,4 +163,5 @@ internal readonly struct FrameControl
delayDenominator: BinaryPrimitives.ReadUInt16BigEndian(data[22..24]),
disposalMode: (FrameDisposalMode)(data[24] + 1),
blendMode: (FrameBlendMode)data[25]);
}
}

13
src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Formats.Webp.Chunks;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
@ -456,15 +457,21 @@ internal class WebpAnimationDecoder : IDisposable
// The destination frame has already been prepopulated with the pixel data from the previous frame
// so blending will leave the desired result which takes into consideration restoration to the
// background color within the restore area.
PixelBlender<TPixel> blender =
PixelOperations<TPixel>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
PixelBlender<TPixel> blender = PixelOperations<TPixel>.Instance.GetPixelBlender(
PixelColorBlendingMode.Normal,
PixelAlphaCompositionMode.SrcOver);
// By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend
// We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending.
using IMemoryOwner<Vector4> workingBufferOwner = imageFrame.Configuration.MemoryAllocator.Allocate<Vector4>(restoreArea.Width * 3);
Span<Vector4> workingBuffer = workingBufferOwner.GetSpan();
for (int y = 0; y < restoreArea.Height; y++)
{
Span<TPixel> framePixelRow = imageFramePixels.DangerousGetRowSpan(y);
Span<TPixel> decodedPixelRow = decodedImageFrame.DangerousGetRowSpan(y)[..restoreArea.Width];
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f);
blender.Blend<TPixel>(imageFrame.Configuration, framePixelRow, framePixelRow, decodedPixelRow, 1f, workingBuffer);
}
return;

198
src/ImageSharp/PixelFormats/PixelBlender{TPixel}.cs

@ -3,7 +3,6 @@
using System.Buffers;
using System.Numerics;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.PixelFormats;
@ -52,16 +51,53 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 3);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = buffer.Slice(maxLength * 2, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 3)]);
}
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <typeparam name="TPixelSrc">the pixel format of the source span</typeparam>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend<TPixelSrc>(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixelSrc> source,
float amount,
Span<Vector4> workingBuffer)
where TPixelSrc : unmanaged, IPixel<TPixelSrc>
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
PixelOperations<TPixelSrc>.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
@ -87,14 +123,48 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 2)]);
}
/// <summary>
/// Blends a row against a constant source color using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A value between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 2 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
float amount,
Span<Vector4> workingBuffer)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeBetweenOrEqualTo(amount, 0, 1, nameof(amount));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
@ -116,6 +186,27 @@ public abstract class PixelBlender<TPixel>
ReadOnlySpan<float> amount)
=> this.Blend<TPixel>(configuration, destination, background, source, amount);
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixel> source,
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
=> this.Blend<TPixel>(configuration, destination, background, source, amount, workingBuffer);
/// <summary>
/// Blends 2 rows together
/// </summary>
@ -142,20 +233,89 @@ public abstract class PixelBlender<TPixel>
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 3);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = buffer.Slice(maxLength * 2, maxLength);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 3)]);
}
/// <summary>
/// Blends a row against a constant source color.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source color</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
ReadOnlySpan<float> amount)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
this.Blend(
configuration,
destination,
background,
source,
amount,
buffer.Memory.Span[..(maxLength * 2)]);
}
/// <summary>
/// Blends 2 rows together using caller-provided temporary vector scratch.
/// </summary>
/// <typeparam name="TPixelSrc">the pixel format of the source span</typeparam>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
/// <param name="background">the background span</param>
/// <param name="source">the source span</param>
/// <param name="amount">
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 3 rows.</param>
public void Blend<TPixelSrc>(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
ReadOnlySpan<TPixelSrc> source,
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
where TPixelSrc : unmanaged, IPixel<TPixelSrc>
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(source.Length, maxLength, nameof(source.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 3, nameof(workingBuffer.Length));
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
Span<Vector4> sourceVectors = workingBuffer.Slice(maxLength * 2, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
PixelOperations<TPixelSrc>.Instance.ToVector4(configuration, source[..maxLength], sourceVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, sourceVectors, amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>
/// Blends a row against a constant source color.
/// Blends a row against a constant source color using caller-provided temporary vector scratch.
/// </summary>
/// <param name="configuration"><see cref="Configuration"/> to use internally</param>
/// <param name="destination">the destination span</param>
@ -165,26 +325,28 @@ public abstract class PixelBlender<TPixel>
/// A span with values between 0 and 1 indicating the weight of the second source vector.
/// At amount = 0, "background" is returned, at amount = 1, "source" is returned.
/// </param>
/// <param name="workingBuffer">Reusable temporary vector scratch with capacity for at least 2 rows.</param>
public void Blend(
Configuration configuration,
Span<TPixel> destination,
ReadOnlySpan<TPixel> background,
TPixel source,
ReadOnlySpan<float> amount)
ReadOnlySpan<float> amount,
Span<Vector4> workingBuffer)
{
int maxLength = destination.Length;
Guard.MustBeGreaterThanOrEqualTo(background.Length, maxLength, nameof(background.Length));
Guard.MustBeGreaterThanOrEqualTo(amount.Length, maxLength, nameof(amount.Length));
Guard.MustBeGreaterThanOrEqualTo(workingBuffer.Length, maxLength * 2, nameof(workingBuffer.Length));
using IMemoryOwner<Vector4> buffer = configuration.MemoryAllocator.Allocate<Vector4>(maxLength * 2);
Span<Vector4> destinationVectors = buffer.Slice(0, maxLength);
Span<Vector4> backgroundVectors = buffer.Slice(maxLength, maxLength);
Span<Vector4> destinationVectors = workingBuffer[..maxLength];
Span<Vector4> backgroundVectors = workingBuffer.Slice(maxLength, maxLength);
PixelOperations<TPixel>.Instance.ToVector4(configuration, background[..maxLength], backgroundVectors, PixelConversionModifiers.Scale);
this.BlendFunction(destinationVectors, backgroundVectors, source.ToScaledVector4(), amount);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors[..maxLength], destination, PixelConversionModifiers.Scale);
PixelOperations<TPixel>.Instance.FromVector4Destructive(configuration, destinationVectors, destination, PixelConversionModifiers.Scale);
}
/// <summary>

16
src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Numerics;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@ -145,7 +146,7 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
this.Blender,
this.Opacity);
ParallelRowIterator.IterateRows(
ParallelRowIterator.IterateRows<RowOperation, Vector4>(
configuration,
new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height),
in operation);
@ -161,7 +162,7 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
/// <summary>
/// A <see langword="struct"/> implementing the draw logic for <see cref="DrawImageProcessor{TPixelBg,TPixelFg}"/>.
/// </summary>
private readonly struct RowOperation : IRowOperation
private readonly struct RowOperation : IRowOperation<Vector4>
{
private readonly Buffer2D<TPixelBg> background;
private readonly Buffer2D<TPixelFg> foreground;
@ -190,13 +191,20 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
this.opacity = opacity;
}
/// <inheritdoc/>
public int GetRequiredBufferLength(Rectangle bounds)
// By using a dedicated vector span we can avoid per-row pool allocations in PixelBlender.Blend
// We need 3 Vector4 values per pixel to store the background, foreground, and result pixels for blending.
=> 3 * bounds.Width;
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public void Invoke(int y)
public void Invoke(int y, Span<Vector4> span)
{
Span<TPixelBg> background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width);
Span<TPixelFg> foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity, span);
}
}
}

16
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.Chunks.cs

@ -92,6 +92,22 @@ public partial class PngDecoderTests
Assert.Equal("pHYs chunk is too short", exception.Message);
}
// https://github.com/SixLabors/ImageSharp/issues/3093
[Fact]
public void Decode_TruncatedFrameControlChunk_ExceptionIsThrown()
{
// PNG signature + truncated frame control chunk
byte[] payload = Convert.FromHexString(
"89504e470d0a1a0a424d3a00000000007f000000000028030405060000000100" +
"000101002000000000000000000000000000ff00006663544cff190000000000" +
"010000424d000100000101002000000000");
using MemoryStream stream = new(payload);
InvalidImageContentException exception = Assert.Throws<InvalidImageContentException>(() => Image.Load<Rgba32>(stream));
Assert.Equal("The frame control chunk does not contain enough data!", exception.Message);
}
// https://github.com/SixLabors/ImageSharp/issues/3079
[Fact]
public void Decode_CompressedTxtChunk_WithTruncatedData_DoesNotThrow()

Loading…
Cancel
Save