Skip to content
378 changes: 361 additions & 17 deletions src/ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ public class DrawImageProcessor : IImageProcessor
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend.</param>
/// <param name="foregroundRepeatCount">The number of times the foreground frames are allowed to loop. 0 means infinitely.</param>
public DrawImageProcessor(
Image foreground,
Point backgroundLocation,
PixelColorBlendingMode colorBlendingMode,
PixelAlphaCompositionMode alphaCompositionMode,
float opacity)
: this(foreground, backgroundLocation, foreground.Bounds, colorBlendingMode, alphaCompositionMode, opacity)
float opacity,
int foregroundRepeatCount)
: this(foreground, backgroundLocation, foreground.Bounds, colorBlendingMode, alphaCompositionMode, opacity, foregroundRepeatCount)
{
}

Expand All @@ -38,20 +40,23 @@ public DrawImageProcessor(
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend.</param>
/// <param name="foregroundRepeatCount">The number of times the foreground frames are allowed to loop. 0 means infinitely.</param>
public DrawImageProcessor(
Image foreground,
Point backgroundLocation,
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlendingMode,
PixelAlphaCompositionMode alphaCompositionMode,
float opacity)
float opacity,
int foregroundRepeatCount)
{
this.ForeGround = foreground;
this.BackgroundLocation = backgroundLocation;
this.ForegroundRectangle = foregroundRectangle;
this.ColorBlendingMode = colorBlendingMode;
this.AlphaCompositionMode = alphaCompositionMode;
this.Opacity = opacity;
this.ForegroundRepeatCount = foregroundRepeatCount;
}

/// <summary>
Expand Down Expand Up @@ -84,6 +89,11 @@ public DrawImageProcessor(
/// </summary>
public float Opacity { get; }

/// <summary>
/// Gets the number of times the foreground frames are allowed to loop. 0 means infinitely.
/// </summary>
public int ForegroundRepeatCount { get; }

/// <inheritdoc />
public IImageProcessor<TPixelBg> CreatePixelSpecificProcessor<TPixelBg>(Configuration configuration, Image<TPixelBg> source, Rectangle sourceRectangle)
where TPixelBg : unmanaged, IPixel<TPixelBg>
Expand Down Expand Up @@ -122,6 +132,7 @@ public void Visit<TPixelFg>(Image<TPixelFg> image)
this.definition.ForegroundRectangle,
this.definition.ColorBlendingMode,
this.definition.AlphaCompositionMode,
this.definition.Opacity);
this.definition.Opacity,
this.definition.ForegroundRepeatCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
where TPixelBg : unmanaged, IPixel<TPixelBg>
where TPixelFg : unmanaged, IPixel<TPixelFg>
{
/// <summary>
/// Counts how many times <see cref="OnFrameApply"/> has been called for this processor instance.
/// Used to select the current foreground frame.
/// </summary>
private int foregroundFrameCounter;

/// <summary>
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelBg, TPixelFg}"/> class.
/// </summary>
Expand All @@ -28,6 +34,10 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
/// <param name="alphaCompositionMode">The alpha blending mode to use when drawing the image.</param>
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
/// <param name="foregroundRepeatCount">
/// The number of times the foreground frames are allowed to loop while applying this processor across successive frames.
/// A value of 0 means loop indefinitely.
/// </param>
public DrawImageProcessor(
Configuration configuration,
Image<TPixelFg> foregroundImage,
Expand All @@ -36,16 +46,19 @@ public DrawImageProcessor(
Rectangle foregroundRectangle,
PixelColorBlendingMode colorBlendingMode,
PixelAlphaCompositionMode alphaCompositionMode,
float opacity)
float opacity,
int foregroundRepeatCount)
: base(configuration, backgroundImage, backgroundImage.Bounds)
{
Guard.MustBeGreaterThanOrEqualTo(foregroundRepeatCount, 0, nameof(foregroundRepeatCount));
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));

this.ForegroundImage = foregroundImage;
this.ForegroundRectangle = foregroundRectangle;
this.Opacity = opacity;
this.Blender = PixelOperations<TPixelBg>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
this.BackgroundLocation = backgroundLocation;
this.ForegroundRepeatCount = foregroundRepeatCount;
}

/// <summary>
Expand Down Expand Up @@ -73,6 +86,12 @@ public DrawImageProcessor(
/// </summary>
public Point BackgroundLocation { get; }

/// <summary>
/// Gets the number of times the foreground frames are allowed to loop while applying this processor across
/// successive frames. A value of 0 means loop indefinitely.
/// </summary>
public int ForegroundRepeatCount { get; }

/// <inheritdoc/>
protected override void OnFrameApply(ImageFrame<TPixelBg> source)
{
Expand Down Expand Up @@ -114,12 +133,13 @@ protected override void OnFrameApply(ImageFrame<TPixelBg> source)
// Sanitize the dimensions so that we don't try and sample outside the image.
Rectangle backgroundRectangle = Rectangle.Intersect(new Rectangle(left, top, width, height), this.SourceRectangle);
Configuration configuration = this.Configuration;
int currentFrameIndex = this.foregroundFrameCounter % this.ForegroundImage.Frames.Count;

DrawImageProcessor<TPixelBg, TPixelFg>.RowOperation operation =
RowOperation operation =
new(
configuration,
source.PixelBuffer,
this.ForegroundImage.Frames.RootFrame.PixelBuffer,
this.ForegroundImage.Frames[currentFrameIndex].PixelBuffer,
backgroundRectangle,
foregroundRectangle,
this.Blender,
Expand All @@ -129,6 +149,13 @@ protected override void OnFrameApply(ImageFrame<TPixelBg> source)
configuration,
new Rectangle(0, 0, foregroundRectangle.Width, foregroundRectangle.Height),
in operation);

// The repeat count only affects how the foreground frame advances across successive background frames.
// When exhausted, the selected foreground frame stops advancing.
if (this.ForegroundRepeatCount is 0 || this.foregroundFrameCounter / this.ForegroundImage.Frames.Count < this.ForegroundRepeatCount)
{
this.foregroundFrameCounter++;
}
}

/// <summary>
Expand Down
17 changes: 17 additions & 0 deletions tests/ImageSharp.Tests/Drawing/DrawImageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,4 +293,21 @@ public void Issue2603<TPixel>(TestImageProvider<TPixel> provider)
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
}

[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void DrawImageAnimatedForegroundRepeatCount<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> background = provider.GetImage();
using Image<TPixel> foreground = Image.Load<TPixel>(TestFile.Create(TestImages.Gif.Giphy).Bytes);

Size size = new(foreground.Width / 4, foreground.Height / 4);
foreground.Mutate(x => x.Resize(size.Width, size.Height, KnownResamplers.Bicubic));

background.Mutate(x => x.DrawImage(foreground, Point.Empty, 1F, 0));

background.DebugSaveMultiFrame(provider);
background.CompareToReferenceOutputMultiFrame(provider, ImageComparer.TolerantPercentage(0.01f));
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading