Skip to content

Commit 119885f

Browse files
Merge pull request #2474 from SixLabors/js/enhanced-drawing
Fix DrawImage offsetting issues and improve API parameter names.
2 parents f5e4605 + ff9ed12 commit 119885f

File tree

10 files changed

+281
-174
lines changed

10 files changed

+281
-174
lines changed

src/ImageSharp/Processing/Extensions/Drawing/DrawImageExtensions.cs

Lines changed: 81 additions & 81 deletions
Large diffs are not rendered by default.

src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor.cs

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,41 @@ public class DrawImageProcessor : IImageProcessor
1414
/// <summary>
1515
/// Initializes a new instance of the <see cref="DrawImageProcessor"/> class.
1616
/// </summary>
17-
/// <param name="image">The image to blend.</param>
18-
/// <param name="location">The location to draw the blended image.</param>
17+
/// <param name="foreground">The image to blend.</param>
18+
/// <param name="backgroundLocation">The location to draw the foreground image on the background.</param>
1919
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
2020
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
2121
/// <param name="opacity">The opacity of the image to blend.</param>
2222
public DrawImageProcessor(
23-
Image image,
24-
Point location,
23+
Image foreground,
24+
Point backgroundLocation,
2525
PixelColorBlendingMode colorBlendingMode,
2626
PixelAlphaCompositionMode alphaCompositionMode,
2727
float opacity)
28+
: this(foreground, backgroundLocation, foreground.Bounds, colorBlendingMode, alphaCompositionMode, opacity)
2829
{
29-
this.Image = image;
30-
this.Location = location;
30+
}
31+
32+
/// <summary>
33+
/// Initializes a new instance of the <see cref="DrawImageProcessor"/> class.
34+
/// </summary>
35+
/// <param name="foreground">The image to blend.</param>
36+
/// <param name="backgroundLocation">The location to draw the foreground image on the background.</param>
37+
/// <param name="foregroundRectangle">The rectangular portion of the foreground image to draw.</param>
38+
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
39+
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
40+
/// <param name="opacity">The opacity of the image to blend.</param>
41+
public DrawImageProcessor(
42+
Image foreground,
43+
Point backgroundLocation,
44+
Rectangle foregroundRectangle,
45+
PixelColorBlendingMode colorBlendingMode,
46+
PixelAlphaCompositionMode alphaCompositionMode,
47+
float opacity)
48+
{
49+
this.ForeGround = foreground;
50+
this.BackgroundLocation = backgroundLocation;
51+
this.ForegroundRectangle = foregroundRectangle;
3152
this.ColorBlendingMode = colorBlendingMode;
3253
this.AlphaCompositionMode = alphaCompositionMode;
3354
this.Opacity = opacity;
@@ -36,12 +57,17 @@ public DrawImageProcessor(
3657
/// <summary>
3758
/// Gets the image to blend.
3859
/// </summary>
39-
public Image Image { get; }
60+
public Image ForeGround { get; }
61+
62+
/// <summary>
63+
/// Gets the location to draw the foreground image on the background.
64+
/// </summary>
65+
public Point BackgroundLocation { get; }
4066

4167
/// <summary>
42-
/// Gets the location to draw the blended image.
68+
/// Gets the rectangular portion of the foreground image to draw.
4369
/// </summary>
44-
public Point Location { get; }
70+
public Rectangle ForegroundRectangle { get; }
4571

4672
/// <summary>
4773
/// Gets the blending mode to use when drawing the image.
@@ -62,8 +88,8 @@ public DrawImageProcessor(
6288
public IImageProcessor<TPixelBg> CreatePixelSpecificProcessor<TPixelBg>(Configuration configuration, Image<TPixelBg> source, Rectangle sourceRectangle)
6389
where TPixelBg : unmanaged, IPixel<TPixelBg>
6490
{
65-
ProcessorFactoryVisitor<TPixelBg> visitor = new(configuration, this, source, sourceRectangle);
66-
this.Image.AcceptVisitor(visitor);
91+
ProcessorFactoryVisitor<TPixelBg> visitor = new(configuration, this, source);
92+
this.ForeGround.AcceptVisitor(visitor);
6793
return visitor.Result!;
6894
}
6995

@@ -73,14 +99,15 @@ private class ProcessorFactoryVisitor<TPixelBg> : IImageVisitor
7399
private readonly Configuration configuration;
74100
private readonly DrawImageProcessor definition;
75101
private readonly Image<TPixelBg> source;
76-
private readonly Rectangle sourceRectangle;
77102

78-
public ProcessorFactoryVisitor(Configuration configuration, DrawImageProcessor definition, Image<TPixelBg> source, Rectangle sourceRectangle)
103+
public ProcessorFactoryVisitor(
104+
Configuration configuration,
105+
DrawImageProcessor definition,
106+
Image<TPixelBg> source)
79107
{
80108
this.configuration = configuration;
81109
this.definition = definition;
82110
this.source = source;
83-
this.sourceRectangle = sourceRectangle;
84111
}
85112

86113
public IImageProcessor<TPixelBg>? Result { get; private set; }
@@ -91,8 +118,8 @@ public void Visit<TPixelFg>(Image<TPixelFg> image)
91118
this.configuration,
92119
image,
93120
this.source,
94-
this.sourceRectangle,
95-
this.definition.Location,
121+
this.definition.BackgroundLocation,
122+
this.definition.ForegroundRectangle,
96123
this.definition.ColorBlendingMode,
97124
this.definition.AlphaCompositionMode,
98125
this.definition.Opacity);

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

Lines changed: 73 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -21,36 +21,42 @@ internal class DrawImageProcessor<TPixelBg, TPixelFg> : ImageProcessor<TPixelBg>
2121
/// Initializes a new instance of the <see cref="DrawImageProcessor{TPixelBg, TPixelFg}"/> class.
2222
/// </summary>
2323
/// <param name="configuration">The configuration which allows altering default behaviour or extending the library.</param>
24-
/// <param name="image">The foreground <see cref="Image{TPixelFg}"/> to blend with the currently processing image.</param>
25-
/// <param name="source">The source <see cref="Image{TPixelBg}"/> for the current processor instance.</param>
26-
/// <param name="sourceRectangle">The source area to process for the current processor instance.</param>
27-
/// <param name="location">The location to draw the blended image.</param>
24+
/// <param name="foregroundImage">The foreground <see cref="Image{TPixelFg}"/> to blend with the currently processing image.</param>
25+
/// <param name="backgroundImage">The source <see cref="Image{TPixelBg}"/> for the current processor instance.</param>
26+
/// <param name="backgroundLocation">The location to draw the blended image.</param>
27+
/// <param name="foregroundRectangle">The source area to process for the current processor instance.</param>
2828
/// <param name="colorBlendingMode">The blending mode to use when drawing the image.</param>
29-
/// <param name="alphaCompositionMode">The Alpha blending mode to use when drawing the image.</param>
29+
/// <param name="alphaCompositionMode">The alpha blending mode to use when drawing the image.</param>
3030
/// <param name="opacity">The opacity of the image to blend. Must be between 0 and 1.</param>
3131
public DrawImageProcessor(
3232
Configuration configuration,
33-
Image<TPixelFg> image,
34-
Image<TPixelBg> source,
35-
Rectangle sourceRectangle,
36-
Point location,
33+
Image<TPixelFg> foregroundImage,
34+
Image<TPixelBg> backgroundImage,
35+
Point backgroundLocation,
36+
Rectangle foregroundRectangle,
3737
PixelColorBlendingMode colorBlendingMode,
3838
PixelAlphaCompositionMode alphaCompositionMode,
3939
float opacity)
40-
: base(configuration, source, sourceRectangle)
40+
: base(configuration, backgroundImage, backgroundImage.Bounds)
4141
{
4242
Guard.MustBeBetweenOrEqualTo(opacity, 0, 1, nameof(opacity));
4343

44-
this.Image = image;
44+
this.ForegroundImage = foregroundImage;
45+
this.ForegroundRectangle = foregroundRectangle;
4546
this.Opacity = opacity;
4647
this.Blender = PixelOperations<TPixelBg>.Instance.GetPixelBlender(colorBlendingMode, alphaCompositionMode);
47-
this.Location = location;
48+
this.BackgroundLocation = backgroundLocation;
4849
}
4950

5051
/// <summary>
5152
/// Gets the image to blend
5253
/// </summary>
53-
public Image<TPixelFg> Image { get; }
54+
public Image<TPixelFg> ForegroundImage { get; }
55+
56+
/// <summary>
57+
/// Gets the rectangular portion of the foreground image to draw.
58+
/// </summary>
59+
public Rectangle ForegroundRectangle { get; }
5460

5561
/// <summary>
5662
/// Gets the opacity of the image to blend
@@ -65,43 +71,57 @@ public DrawImageProcessor(
6571
/// <summary>
6672
/// Gets the location to draw the blended image
6773
/// </summary>
68-
public Point Location { get; }
74+
public Point BackgroundLocation { get; }
6975

7076
/// <inheritdoc/>
7177
protected override void OnFrameApply(ImageFrame<TPixelBg> source)
7278
{
73-
Rectangle sourceRectangle = this.SourceRectangle;
74-
Configuration configuration = this.Configuration;
75-
76-
Image<TPixelFg> targetImage = this.Image;
77-
PixelBlender<TPixelBg> blender = this.Blender;
78-
int locationY = this.Location.Y;
79+
// Align the bounds so that both the source and targets are the same width and height for blending.
80+
// We ensure that negative locations are subtracted from both bounds so that foreground images can partially overlap.
81+
Rectangle foregroundRectangle = this.ForegroundRectangle;
7982

80-
// Align start/end positions.
81-
Rectangle bounds = targetImage.Bounds;
83+
// Sanitize the location so that we don't try and sample outside the image.
84+
int left = this.BackgroundLocation.X;
85+
int top = this.BackgroundLocation.Y;
8286

83-
int minX = Math.Max(this.Location.X, sourceRectangle.X);
84-
int maxX = Math.Min(this.Location.X + bounds.Width, sourceRectangle.Right);
85-
int targetX = minX - this.Location.X;
86-
87-
int minY = Math.Max(this.Location.Y, sourceRectangle.Y);
88-
int maxY = Math.Min(this.Location.Y + bounds.Height, sourceRectangle.Bottom);
89-
90-
int width = maxX - minX;
87+
if (this.BackgroundLocation.X < 0)
88+
{
89+
foregroundRectangle.Width += this.BackgroundLocation.X;
90+
left = 0;
91+
}
9192

92-
Rectangle workingRect = Rectangle.FromLTRB(minX, minY, maxX, maxY);
93+
if (this.BackgroundLocation.Y < 0)
94+
{
95+
foregroundRectangle.Height += this.BackgroundLocation.Y;
96+
top = 0;
97+
}
9398

94-
// Not a valid operation because rectangle does not overlap with this image.
95-
if (workingRect.Width <= 0 || workingRect.Height <= 0)
99+
int width = foregroundRectangle.Width;
100+
int height = foregroundRectangle.Height;
101+
if (width <= 0 || height <= 0)
96102
{
97-
throw new ImageProcessingException(
98-
"Cannot draw image because the source image does not overlap the target image.");
103+
// Nothing to do, return.
104+
return;
99105
}
100106

101-
DrawImageProcessor<TPixelBg, TPixelFg>.RowOperation operation = new(source.PixelBuffer, targetImage.Frames.RootFrame.PixelBuffer, blender, configuration, minX, width, locationY, targetX, this.Opacity);
107+
// Sanitize the dimensions so that we don't try and sample outside the image.
108+
foregroundRectangle = Rectangle.Intersect(foregroundRectangle, this.ForegroundImage.Bounds);
109+
Rectangle backgroundRectangle = Rectangle.Intersect(new(left, top, width, height), this.SourceRectangle);
110+
Configuration configuration = this.Configuration;
111+
112+
DrawImageProcessor<TPixelBg, TPixelFg>.RowOperation operation =
113+
new(
114+
configuration,
115+
source.PixelBuffer,
116+
this.ForegroundImage.Frames.RootFrame.PixelBuffer,
117+
backgroundRectangle,
118+
foregroundRectangle,
119+
this.Blender,
120+
this.Opacity);
121+
102122
ParallelRowIterator.IterateRows(
103123
configuration,
104-
workingRect,
124+
new(0, 0, foregroundRectangle.Width, foregroundRectangle.Height),
105125
in operation);
106126
}
107127

@@ -110,45 +130,39 @@ protected override void OnFrameApply(ImageFrame<TPixelBg> source)
110130
/// </summary>
111131
private readonly struct RowOperation : IRowOperation
112132
{
113-
private readonly Buffer2D<TPixelBg> source;
114-
private readonly Buffer2D<TPixelFg> target;
133+
private readonly Buffer2D<TPixelBg> background;
134+
private readonly Buffer2D<TPixelFg> foreground;
115135
private readonly PixelBlender<TPixelBg> blender;
116136
private readonly Configuration configuration;
117-
private readonly int minX;
118-
private readonly int width;
119-
private readonly int locationY;
120-
private readonly int targetX;
137+
private readonly Rectangle foregroundRectangle;
138+
private readonly Rectangle backgroundRectangle;
121139
private readonly float opacity;
122140

123141
[MethodImpl(InliningOptions.ShortMethod)]
124142
public RowOperation(
125-
Buffer2D<TPixelBg> source,
126-
Buffer2D<TPixelFg> target,
127-
PixelBlender<TPixelBg> blender,
128143
Configuration configuration,
129-
int minX,
130-
int width,
131-
int locationY,
132-
int targetX,
144+
Buffer2D<TPixelBg> background,
145+
Buffer2D<TPixelFg> foreground,
146+
Rectangle backgroundRectangle,
147+
Rectangle foregroundRectangle,
148+
PixelBlender<TPixelBg> blender,
133149
float opacity)
134150
{
135-
this.source = source;
136-
this.target = target;
137-
this.blender = blender;
138151
this.configuration = configuration;
139-
this.minX = minX;
140-
this.width = width;
141-
this.locationY = locationY;
142-
this.targetX = targetX;
152+
this.background = background;
153+
this.foreground = foreground;
154+
this.backgroundRectangle = backgroundRectangle;
155+
this.foregroundRectangle = foregroundRectangle;
156+
this.blender = blender;
143157
this.opacity = opacity;
144158
}
145159

146160
/// <inheritdoc/>
147161
[MethodImpl(InliningOptions.ShortMethod)]
148162
public void Invoke(int y)
149163
{
150-
Span<TPixelBg> background = this.source.DangerousGetRowSpan(y).Slice(this.minX, this.width);
151-
Span<TPixelFg> foreground = this.target.DangerousGetRowSpan(y - this.locationY).Slice(this.targetX, this.width);
164+
Span<TPixelBg> background = this.background.DangerousGetRowSpan(y + this.backgroundRectangle.Top).Slice(this.backgroundRectangle.Left, this.backgroundRectangle.Width);
165+
Span<TPixelFg> foreground = this.foreground.DangerousGetRowSpan(y + this.foregroundRectangle.Top).Slice(this.foregroundRectangle.Left, this.foregroundRectangle.Width);
152166
this.blender.Blend<TPixelFg>(this.configuration, background, background, foreground, this.opacity);
153167
}
154168
}

tests/ImageSharp.Tests/Drawing/DrawImageExtensionsTests.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ public class DrawImageExtensionsTests : BaseImageOperationsExtensionTest
1313
[Fact]
1414
public void DrawImage_OpacityOnly_VerifyGraphicOptionsTakenFromContext()
1515
{
16-
// non-default values as we cant easly defect usage otherwise
16+
// non-default values as we cant easily defect usage otherwise
1717
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
1818
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
1919

20-
this.operations.DrawImage(null, 0.5f);
20+
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
21+
this.operations.DrawImage(image, 0.5f);
2122
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
2223

2324
Assert.Equal(0.5, dip.Opacity);
@@ -28,11 +29,12 @@ public void DrawImage_OpacityOnly_VerifyGraphicOptionsTakenFromContext()
2829
[Fact]
2930
public void DrawImage_OpacityAndBlending_VerifyGraphicOptionsTakenFromContext()
3031
{
31-
// non-default values as we cant easly defect usage otherwise
32+
// non-default values as we cant easily defect usage otherwise
3233
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
3334
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
3435

35-
this.operations.DrawImage(null, PixelColorBlendingMode.Multiply, 0.5f);
36+
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
37+
this.operations.DrawImage(image, PixelColorBlendingMode.Multiply, 0.5f);
3638
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
3739

3840
Assert.Equal(0.5, dip.Opacity);
@@ -43,11 +45,12 @@ public void DrawImage_OpacityAndBlending_VerifyGraphicOptionsTakenFromContext()
4345
[Fact]
4446
public void DrawImage_LocationAndOpacity_VerifyGraphicOptionsTakenFromContext()
4547
{
46-
// non-default values as we cant easly defect usage otherwise
48+
// non-default values as we cant easily defect usage otherwise
4749
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
4850
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
4951

50-
this.operations.DrawImage(null, Point.Empty, 0.5f);
52+
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
53+
this.operations.DrawImage(image, Point.Empty, 0.5f);
5154
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
5255

5356
Assert.Equal(0.5, dip.Opacity);
@@ -58,11 +61,12 @@ public void DrawImage_LocationAndOpacity_VerifyGraphicOptionsTakenFromContext()
5861
[Fact]
5962
public void DrawImage_LocationAndOpacityAndBlending_VerifyGraphicOptionsTakenFromContext()
6063
{
61-
// non-default values as we cant easly defect usage otherwise
64+
// non-default values as we cant easily defect usage otherwise
6265
this.options.AlphaCompositionMode = PixelAlphaCompositionMode.Xor;
6366
this.options.ColorBlendingMode = PixelColorBlendingMode.Screen;
6467

65-
this.operations.DrawImage(null, Point.Empty, PixelColorBlendingMode.Multiply, 0.5f);
68+
using Image<Rgba32> image = new(Configuration.Default, 1, 1);
69+
this.operations.DrawImage(image, Point.Empty, PixelColorBlendingMode.Multiply, 0.5f);
6670
DrawImageProcessor dip = this.Verify<DrawImageProcessor>();
6771

6872
Assert.Equal(0.5, dip.Opacity);

0 commit comments

Comments
 (0)