Skip to content

Commit 844c332

Browse files
Increase DrawImage test coverage with noops and dispose case
1 parent 11b6ea0 commit 844c332

2 files changed

Lines changed: 86 additions & 22 deletions

File tree

src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -738,31 +738,26 @@ private void DrawImageCore(
738738
DrawingOptions commandOptions = effectiveOptions;
739739
IReadOnlyList<IPath> commandClipPaths = state.ClipPaths;
740740

741-
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
741+
Image<TPixel>? ownedImage = null;
742+
try
742743
{
743-
if (disposeSourceImage)
744+
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
744745
{
745-
image.Dispose();
746+
return;
746747
}
747748

748-
return;
749-
}
750-
751-
Size scaledSize = new(
752-
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)),
753-
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height)));
749+
Size scaledSize = new(
750+
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)),
751+
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height)));
754752

755-
bool requiresScaling =
756-
clippedSourceRect.Width != scaledSize.Width ||
757-
clippedSourceRect.Height != scaledSize.Height;
753+
bool requiresScaling =
754+
clippedSourceRect.Width != scaledSize.Width ||
755+
clippedSourceRect.Height != scaledSize.Height;
758756

759-
Image<TPixel> brushImage = image;
760-
RectangleF brushImageRegion = clippedSourceRect;
761-
RectangleF renderDestinationRect = clippedDestinationRect;
762-
Image<TPixel>? ownedImage = null;
757+
Image<TPixel> brushImage = image;
758+
RectangleF brushImageRegion = clippedSourceRect;
759+
RectangleF renderDestinationRect = clippedDestinationRect;
763760

764-
try
765-
{
766761
// Phase 1: Prepare source pixels (crop/scale) in image-local space.
767762
if (requiresScaling)
768763
{
@@ -1558,10 +1553,8 @@ private static bool TryGetDrawImageClip(
15581553
clippedSourceRect = default;
15591554
clippedDestinationRect = default;
15601555

1561-
if (sourceRect.Width <= 0 ||
1562-
sourceRect.Height <= 0 ||
1563-
destinationRect.Width <= 0 ||
1564-
destinationRect.Height <= 0)
1556+
// A zero-area source cannot be sampled and would divide by zero when mapping to the destination.
1557+
if (sourceRect.Width <= 0 || sourceRect.Height <= 0)
15651558
{
15661559
return false;
15671560
}
@@ -1573,6 +1566,8 @@ private static bool TryGetDrawImageClip(
15731566
}
15741567

15751568
clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect);
1569+
1570+
// A degenerate (empty or inverted) destination maps to nothing to draw.
15761571
return clippedDestinationRect.Width > 0 && clippedDestinationRect.Height > 0;
15771572
}
15781573

tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.DrawImage.cs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,75 @@ public void DrawImage_WithForeignPixelFormat_WholeImage_MatchesFullConversion<TP
178178
new RectangleF(24, 20, 260, 200),
179179
new Matrix4x4(Matrix3x2.CreateRotation(0.15F, new Vector2(160, 120))));
180180

181+
[Theory]
182+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
183+
public void DrawImage_WithEmptySourceRect_IsNoOp<TPixel>(TestImageProvider<TPixel> provider)
184+
where TPixel : unmanaged, IPixel<TPixel>
185+
=> AssertDrawImageIsNoOp(
186+
provider,
187+
new Rectangle(40, 30, 0, 120),
188+
new RectangleF(20, 20, 200, 160));
189+
190+
[Theory]
191+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
192+
public void DrawImage_WithEmptyDestinationRect_IsNoOp<TPixel>(TestImageProvider<TPixel> provider)
193+
where TPixel : unmanaged, IPixel<TPixel>
194+
=> AssertDrawImageIsNoOp(
195+
provider,
196+
new Rectangle(40, 30, 180, 150),
197+
new RectangleF(20, 20, 200, 0));
198+
199+
[Theory]
200+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
201+
public void DrawImage_WithSourceRectFullyOutsideImage_IsNoOp<TPixel>(TestImageProvider<TPixel> provider)
202+
where TPixel : unmanaged, IPixel<TPixel>
203+
=> AssertDrawImageIsNoOp(
204+
provider,
205+
new Rectangle(400, 300, 120, 100),
206+
new RectangleF(20, 20, 200, 160));
207+
208+
/// <summary>
209+
/// A draw whose clipped source/destination region is empty must be a no-op for both the
210+
/// typed <see cref="Image{TPixel}"/> overload and the foreign-pixel-format <see cref="Image"/>
211+
/// overload, leaving the cleared background untouched.
212+
/// </summary>
213+
private static void AssertDrawImageIsNoOp<TPixel>(
214+
TestImageProvider<TPixel> provider,
215+
Rectangle sourceRect,
216+
RectangleF destinationRect)
217+
where TPixel : unmanaged, IPixel<TPixel>
218+
{
219+
using Image<TPixel> source = provider.GetImage();
220+
221+
// A source image whose pixel format differs from the canvas, forcing the foreign-format path.
222+
using Image<Rgb24> foreignSource = source.CloneAs<Rgb24>();
223+
224+
// The reference is the cleared background: a degenerate draw must not change any pixel.
225+
using Image<TPixel> expected = new(source.Width, source.Height);
226+
using (DrawingCanvas<TPixel> reference = CreateCanvas(provider, expected, new DrawingOptions()))
227+
{
228+
reference.Clear(Brushes.Solid(Color.White));
229+
}
230+
231+
void AssertNoOp(Action<DrawingCanvas<TPixel>> draw)
232+
{
233+
using Image<TPixel> actual = new(source.Width, source.Height);
234+
using (DrawingCanvas<TPixel> canvas = CreateCanvas(provider, actual, new DrawingOptions()))
235+
{
236+
canvas.Clear(Brushes.Solid(Color.White));
237+
draw(canvas);
238+
}
239+
240+
ImageComparer.Exact.VerifySimilarity(expected, actual);
241+
}
242+
243+
// Typed overload -> DrawImageCore empty-region early-return.
244+
AssertNoOp(canvas => canvas.DrawImage(source, sourceRect, destinationRect, KnownResamplers.Bicubic));
245+
246+
// Foreign-format overload -> DrawImage empty-region early-return before any conversion.
247+
AssertNoOp(canvas => canvas.DrawImage((Image)foreignSource, sourceRect, destinationRect, KnownResamplers.Bicubic));
248+
}
249+
181250
/// <summary>
182251
/// Drawing a foreign-pixel-format image (which converts only the clipped source region) must
183252
/// produce pixels identical to first converting the whole image to the canvas format and drawing that.

0 commit comments

Comments
 (0)