@@ -107,4 +107,186 @@ 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+ [ 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+
250+ /// <summary>
251+ /// Drawing a foreign-pixel-format image (which converts only the clipped source region) must
252+ /// produce pixels identical to first converting the whole image to the canvas format and drawing that.
253+ /// </summary>
254+ private static void AssertForeignPixelFormatMatchesFullConversion < TPixel > (
255+ TestImageProvider < TPixel > provider ,
256+ Rectangle sourceRect ,
257+ RectangleF destinationRect ,
258+ Matrix4x4 transform )
259+ where TPixel : unmanaged, IPixel < TPixel >
260+ {
261+ using Image < TPixel > source = provider . GetImage ( ) ;
262+
263+ // A source image whose pixel format differs from the canvas, forcing a per-pixel conversion.
264+ using Image < Rgb24 > foreignSource = source . CloneAs < Rgb24 > ( ) ;
265+
266+ // Reference source: the whole foreign image converted up-front to the canvas format.
267+ using Image < TPixel > convertedSource = foreignSource . CloneAs < TPixel > ( ) ;
268+
269+ DrawingOptions options = new ( )
270+ {
271+ Transform = transform
272+ } ;
273+
274+ using Image < TPixel > actual = new ( source . Width , source . Height ) ;
275+ using Image < TPixel > expected = new ( source . Width , source . Height ) ;
276+
277+ using ( DrawingCanvas < TPixel > canvas = CreateCanvas ( provider , actual , options ) )
278+ {
279+ canvas . Clear ( Brushes . Solid ( Color . White ) ) ;
280+ canvas . DrawImage ( ( Image ) foreignSource , sourceRect , destinationRect , KnownResamplers . Bicubic ) ;
281+ }
282+
283+ using ( DrawingCanvas < TPixel > canvas = CreateCanvas ( provider , expected , options ) )
284+ {
285+ canvas . Clear ( Brushes . Solid ( Color . White ) ) ;
286+ canvas . DrawImage ( convertedSource , sourceRect , destinationRect , KnownResamplers . Bicubic ) ;
287+ }
288+
289+ // Converting only the clipped region must produce pixels identical to converting the whole image.
290+ ImageComparer . Exact . VerifySimilarity ( expected , actual ) ;
291+ }
110292}
0 commit comments