Skip to content

Commit 6e136c3

Browse files
Convert only the required source region in DrawImage
1 parent f23980d commit 6e136c3

2 files changed

Lines changed: 174 additions & 17 deletions

File tree

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

Lines changed: 61 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,24 @@ public override void DrawImage(
662662
return;
663663
}
664664

665-
Image<TPixel> convertedImage = image.CloneAs<TPixel>();
666-
this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, ownsSourceImage: true);
665+
// Only the pixels inside the clipped source region are ever sampled by the draw operation.
666+
// When that region covers just part of the image, crop it in the source pixel format first so
667+
// the per-pixel format conversion runs over the required region instead of the whole image.
668+
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
669+
{
670+
return;
671+
}
672+
673+
if (clippedSourceRect == image.Bounds)
674+
{
675+
Image<TPixel> convertedImage = image.CloneAs<TPixel>();
676+
this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, ownsSourceImage: true);
677+
return;
678+
}
679+
680+
using Image croppedSource = image.Clone(ctx => ctx.Crop(clippedSourceRect));
681+
Image<TPixel> convertedRegion = croppedSource.CloneAs<TPixel>();
682+
this.DrawImageCore(convertedRegion, convertedRegion.Bounds, clippedDestinationRect, sampler, ownsSourceImage: true);
667683
}
668684

669685
/// <inheritdoc cref="DrawingCanvas.DrawImage(Image, Rectangle, RectangleF, IResampler?)" />
@@ -722,23 +738,13 @@ private void DrawImageCore(
722738
DrawingOptions commandOptions = effectiveOptions;
723739
IReadOnlyList<IPath> commandClipPaths = state.ClipPaths;
724740

725-
if (sourceRect.Width <= 0 ||
726-
sourceRect.Height <= 0 ||
727-
destinationRect.Width <= 0 ||
728-
destinationRect.Height <= 0)
729-
{
730-
return;
731-
}
732-
733-
Rectangle clippedSourceRect = Rectangle.Intersect(sourceRect, image.Bounds);
734-
if (clippedSourceRect.Width <= 0 || clippedSourceRect.Height <= 0)
741+
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
735742
{
736-
return;
737-
}
743+
if (disposeSourceImage)
744+
{
745+
image.Dispose();
746+
}
738747

739-
RectangleF clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect);
740-
if (clippedDestinationRect.Width <= 0 || clippedDestinationRect.Height <= 0)
741-
{
742748
return;
743749
}
744750

@@ -1532,6 +1538,44 @@ private static Image<TPixel> CreateTransformedDrawImage(
15321538
sampler ?? KnownResamplers.Bicubic));
15331539
}
15341540

1541+
/// <summary>
1542+
/// Computes the source and destination rectangles that a draw-image operation will actually
1543+
/// touch, clipping the requested source rectangle to the image bounds.
1544+
/// </summary>
1545+
/// <param name="sourceRect">Requested source rectangle.</param>
1546+
/// <param name="destinationRect">Requested destination rectangle.</param>
1547+
/// <param name="imageBounds">Bounds of the source image.</param>
1548+
/// <param name="clippedSourceRect">Receives the source rectangle clipped to <paramref name="imageBounds"/>.</param>
1549+
/// <param name="clippedDestinationRect">Receives the destination rectangle matching <paramref name="clippedSourceRect"/>.</param>
1550+
/// <returns><see langword="true"/> when the operation covers a non-empty region; otherwise <see langword="false"/>.</returns>
1551+
private static bool TryGetDrawImageClip(
1552+
Rectangle sourceRect,
1553+
RectangleF destinationRect,
1554+
Rectangle imageBounds,
1555+
out Rectangle clippedSourceRect,
1556+
out RectangleF clippedDestinationRect)
1557+
{
1558+
clippedSourceRect = default;
1559+
clippedDestinationRect = default;
1560+
1561+
if (sourceRect.Width <= 0 ||
1562+
sourceRect.Height <= 0 ||
1563+
destinationRect.Width <= 0 ||
1564+
destinationRect.Height <= 0)
1565+
{
1566+
return false;
1567+
}
1568+
1569+
clippedSourceRect = Rectangle.Intersect(sourceRect, imageBounds);
1570+
if (clippedSourceRect.Width <= 0 || clippedSourceRect.Height <= 0)
1571+
{
1572+
return false;
1573+
}
1574+
1575+
clippedDestinationRect = MapSourceClipToDestination(sourceRect, destinationRect, clippedSourceRect);
1576+
return clippedDestinationRect.Width > 0 && clippedDestinationRect.Height > 0;
1577+
}
1578+
15351579
/// <summary>
15361580
/// Maps a clipped source rectangle back to the corresponding destination rectangle.
15371581
/// </summary>

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

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,4 +107,117 @@ public void DrawImage_WithClipPathAndTransform_MatchesReference<TPixel>(TestImag
107107
target.DebugSave(provider, appendSourceFileOrDescription: false);
108108
target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false);
109109
}
110+
111+
[Theory]
112+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
113+
public void DrawImage_WithForeignPixelFormat_MatchesFullConversion<TPixel>(TestImageProvider<TPixel> provider)
114+
where TPixel : unmanaged, IPixel<TPixel>
115+
=> AssertForeignPixelFormatMatchesFullConversion(
116+
provider,
117+
new Rectangle(64, 48, 180, 150),
118+
new RectangleF(40, 30, 200, 170),
119+
new Matrix4x4(Matrix3x2.CreateRotation(0.28F, new Vector2(160, 120))));
120+
121+
[Theory]
122+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
123+
public void DrawImage_WithForeignPixelFormat_PartialRegionNoTransform_MatchesFullConversion<TPixel>(TestImageProvider<TPixel> provider)
124+
where TPixel : unmanaged, IPixel<TPixel>
125+
=> AssertForeignPixelFormatMatchesFullConversion(
126+
provider,
127+
new Rectangle(64, 48, 180, 150),
128+
new RectangleF(40, 30, 200, 170),
129+
Matrix4x4.Identity);
130+
131+
[Theory]
132+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
133+
public void DrawImage_WithForeignPixelFormat_SourceOutsideTopLeft_MatchesFullConversion<TPixel>(TestImageProvider<TPixel> provider)
134+
where TPixel : unmanaged, IPixel<TPixel>
135+
=> AssertForeignPixelFormatMatchesFullConversion(
136+
provider,
137+
new Rectangle(-48, -32, 220, 190),
138+
new RectangleF(30, 24, 210, 180),
139+
new Matrix4x4(Matrix3x2.CreateRotation(0.21F, new Vector2(160, 120))));
140+
141+
[Theory]
142+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
143+
public void DrawImage_WithForeignPixelFormat_SourceOutsideBottomRight_MatchesFullConversion<TPixel>(TestImageProvider<TPixel> provider)
144+
where TPixel : unmanaged, IPixel<TPixel>
145+
=> AssertForeignPixelFormatMatchesFullConversion(
146+
provider,
147+
new Rectangle(200, 150, 260, 220),
148+
new RectangleF(48, 40, 200, 168),
149+
Matrix4x4.Identity);
150+
151+
[Theory]
152+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
153+
public void DrawImage_WithForeignPixelFormat_ProjectiveTransform_MatchesFullConversion<TPixel>(TestImageProvider<TPixel> provider)
154+
where TPixel : unmanaged, IPixel<TPixel>
155+
{
156+
// A quad/projective transform (non-affine Matrix4x4 with perspective terms) combined
157+
// with a rotation, exercising the transform path over the clipped region.
158+
Matrix4x4 projective = new Matrix4x4(Matrix3x2.CreateRotation(0.18F, new Vector2(160, 120)))
159+
{
160+
M14 = 0.0006F,
161+
M24 = 0.0004F
162+
};
163+
164+
AssertForeignPixelFormatMatchesFullConversion(
165+
provider,
166+
new Rectangle(56, 40, 190, 160),
167+
new RectangleF(44, 34, 200, 168),
168+
projective);
169+
}
170+
171+
[Theory]
172+
[WithBasicTestPatternImages(320, 240, PixelTypes.Rgba32)]
173+
public void DrawImage_WithForeignPixelFormat_WholeImage_MatchesFullConversion<TPixel>(TestImageProvider<TPixel> provider)
174+
where TPixel : unmanaged, IPixel<TPixel>
175+
=> AssertForeignPixelFormatMatchesFullConversion(
176+
provider,
177+
new Rectangle(0, 0, 320, 240),
178+
new RectangleF(24, 20, 260, 200),
179+
new Matrix4x4(Matrix3x2.CreateRotation(0.15F, new Vector2(160, 120))));
180+
181+
/// <summary>
182+
/// Drawing a foreign-pixel-format image (which converts only the clipped source region) must
183+
/// produce pixels identical to first converting the whole image to the canvas format and drawing that.
184+
/// </summary>
185+
private static void AssertForeignPixelFormatMatchesFullConversion<TPixel>(
186+
TestImageProvider<TPixel> provider,
187+
Rectangle sourceRect,
188+
RectangleF destinationRect,
189+
Matrix4x4 transform)
190+
where TPixel : unmanaged, IPixel<TPixel>
191+
{
192+
using Image<TPixel> source = provider.GetImage();
193+
194+
// A source image whose pixel format differs from the canvas, forcing a per-pixel conversion.
195+
using Image<Rgb24> foreignSource = source.CloneAs<Rgb24>();
196+
197+
// Reference source: the whole foreign image converted up-front to the canvas format.
198+
using Image<TPixel> convertedSource = foreignSource.CloneAs<TPixel>();
199+
200+
DrawingOptions options = new()
201+
{
202+
Transform = transform
203+
};
204+
205+
using Image<TPixel> actual = new(source.Width, source.Height);
206+
using Image<TPixel> expected = new(source.Width, source.Height);
207+
208+
using (DrawingCanvas<TPixel> canvas = CreateCanvas(provider, actual, options))
209+
{
210+
canvas.Clear(Brushes.Solid(Color.White));
211+
canvas.DrawImage((Image)foreignSource, sourceRect, destinationRect, KnownResamplers.Bicubic);
212+
}
213+
214+
using (DrawingCanvas<TPixel> canvas = CreateCanvas(provider, expected, options))
215+
{
216+
canvas.Clear(Brushes.Solid(Color.White));
217+
canvas.DrawImage(convertedSource, sourceRect, destinationRect, KnownResamplers.Bicubic);
218+
}
219+
220+
// Converting only the clipped region must produce pixels identical to converting the whole image.
221+
ImageComparer.Exact.VerifySimilarity(expected, actual);
222+
}
110223
}

0 commit comments

Comments
 (0)