diff --git a/src/System.Drawing.Common/src/Resources/Strings.resx b/src/System.Drawing.Common/src/Resources/Strings.resx
index d738f1016c1..73fd25fdf9a 100644
--- a/src/System.Drawing.Common/src/Resources/Strings.resx
+++ b/src/System.Drawing.Common/src/Resources/Strings.resx
@@ -425,4 +425,7 @@
The file path could not be opened or did not contain valid data.
+
+ Drawing operation exceeds the bounds of the image. This is a known GDI+ limitation with AntiAlias smoothing mode and Format24bppRgb pixel format.
+
diff --git a/src/System.Drawing.Common/src/System/Drawing/Graphics.cs b/src/System.Drawing.Common/src/System/Drawing/Graphics.cs
index f58c6bdfc32..b3d4c9143a3 100644
--- a/src/System.Drawing.Common/src/System/Drawing/Graphics.cs
+++ b/src/System.Drawing.Common/src/System/Drawing/Graphics.cs
@@ -1222,6 +1222,61 @@ public void FillRectangle(Brush brush, float x, float y, float width, float heig
{
ArgumentNullException.ThrowIfNull(brush);
+ // GDI+ has a bug where it writes out of bounds when using AntiAlias smoothing mode
+ // on a 24bppRgb bitmap, causing an AccessViolationException (or ExecutionEngineException in .NET 9+).
+ // We validate the parameters here to prevent the crash.
+ if (SmoothingMode == Drawing2D.SmoothingMode.AntiAlias &&
+ _backingImage is { PixelFormat: PixelFormat.Format24bppRgb })
+ {
+ float left = x;
+ float right = x + width;
+ float top = y;
+ float bottom = y + height;
+
+ if (width < 0)
+ {
+ left = right;
+ right = x;
+ }
+
+ if (height < 0)
+ {
+ top = bottom;
+ bottom = y;
+ }
+
+ if (left < 0)
+ {
+ left = 0;
+ }
+
+ if (right > _backingImage.Width)
+ {
+ right = _backingImage.Width;
+ }
+
+ if (top < 0)
+ {
+ top = 0;
+ }
+
+ if (bottom > _backingImage.Height)
+ {
+ bottom = _backingImage.Height;
+ }
+
+ if (left >= right || top >= bottom)
+ {
+ // The rectangle is completely outside the image bounds.
+ return;
+ }
+
+ x = left;
+ y = top;
+ width = right - left;
+ height = bottom - top;
+ }
+
CheckErrorStatus(PInvokeGdiPlus.GdipFillRectangle(
NativeGraphics,
brush.NativeBrush,
@@ -1261,6 +1316,92 @@ void FillRectangles(Brush brush, params ReadOnlySpan rects)
{
ArgumentNullException.ThrowIfNull(brush);
+ if (SmoothingMode == Drawing2D.SmoothingMode.AntiAlias &&
+ _backingImage is { PixelFormat: PixelFormat.Format24bppRgb })
+ {
+ RectangleF[]? clippedRects = null;
+ int clippedCount = 0;
+
+ for (int i = 0; i < rects.Length; i++)
+ {
+ RectangleF rect = rects[i];
+ float left = rect.X;
+ float right = rect.X + rect.Width;
+ float top = rect.Y;
+ float bottom = rect.Y + rect.Height;
+
+ if (rect.Width < 0)
+ {
+ left = right;
+ right = rect.X;
+ }
+
+ if (rect.Height < 0)
+ {
+ top = bottom;
+ bottom = rect.Y;
+ }
+
+ if (left < 0 || top < 0 || right > _backingImage.Width || bottom > _backingImage.Height)
+ {
+ if (clippedRects is null)
+ {
+ clippedRects = new RectangleF[rects.Length];
+ // Copy previous valid rects
+ for (int j = 0; j < i; j++)
+ {
+ clippedRects[clippedCount++] = rects[j];
+ }
+ }
+
+ // Clip
+ if (left < 0)
+ {
+ left = 0;
+ }
+
+ if (right > _backingImage.Width)
+ {
+ right = _backingImage.Width;
+ }
+
+ if (top < 0)
+ {
+ top = 0;
+ }
+
+ if (bottom > _backingImage.Height)
+ {
+ bottom = _backingImage.Height;
+ }
+
+ if (left < right && top < bottom)
+ {
+ clippedRects[clippedCount++] = new RectangleF(left, top, right - left, bottom - top);
+ }
+ }
+ else if (clippedRects is not null)
+ {
+ clippedRects[clippedCount++] = rect;
+ }
+ else
+ {
+ clippedCount++;
+ }
+ }
+
+ if (clippedRects is not null)
+ {
+ fixed (RectangleF* r = clippedRects)
+ {
+ CheckErrorStatus(PInvokeGdiPlus.GdipFillRectangles(NativeGraphics, brush.NativeBrush, (RectF*)r, clippedCount));
+ }
+
+ GC.KeepAlive(brush);
+ return;
+ }
+ }
+
fixed (RectangleF* r = rects)
{
CheckErrorStatus(PInvokeGdiPlus.GdipFillRectangles(NativeGraphics, brush.NativeBrush, (RectF*)r, rects.Length));
@@ -2008,6 +2149,76 @@ public void DrawImage(Image image, float x, float y)
public void DrawImage(Image image, float x, float y, float width, float height)
{
ArgumentNullException.ThrowIfNull(image);
+
+ if (SmoothingMode == Drawing2D.SmoothingMode.AntiAlias &&
+ _backingImage is { PixelFormat: PixelFormat.Format24bppRgb })
+ {
+ float left = x;
+ float right = x + width;
+ float top = y;
+ float bottom = y + height;
+
+ if (width < 0)
+ {
+ left = right;
+ right = x;
+ }
+
+ if (height < 0)
+ {
+ top = bottom;
+ bottom = y;
+ }
+
+ if (left < 0 || top < 0 || right > _backingImage.Width || bottom > _backingImage.Height)
+ {
+ float clippedLeft = left;
+ float clippedRight = right;
+ float clippedTop = top;
+ float clippedBottom = bottom;
+
+ if (clippedLeft < 0)
+ {
+ clippedLeft = 0;
+ }
+
+ if (clippedRight > _backingImage.Width)
+ {
+ clippedRight = _backingImage.Width;
+ }
+
+ if (clippedTop < 0)
+ {
+ clippedTop = 0;
+ }
+
+ if (clippedBottom > _backingImage.Height)
+ {
+ clippedBottom = _backingImage.Height;
+ }
+
+ if (clippedLeft >= clippedRight || clippedTop >= clippedBottom)
+ {
+ return;
+ }
+
+ float destW = right - left;
+ float destH = bottom - top;
+
+ if (Math.Abs(destW) > float.Epsilon && Math.Abs(destH) > float.Epsilon)
+ {
+ float srcX = (clippedLeft - left) / destW * image.Width;
+ float srcY = (clippedTop - top) / destH * image.Height;
+ float srcW = (clippedRight - clippedLeft) / destW * image.Width;
+ float srcH = (clippedBottom - clippedTop) / destH * image.Height;
+
+ DrawImage(image, new RectangleF(clippedLeft, clippedTop, clippedRight - clippedLeft, clippedBottom - clippedTop),
+ new RectangleF(srcX, srcY, srcW, srcH), GraphicsUnit.Pixel);
+ return;
+ }
+ }
+ }
+
Status status = PInvokeGdiPlus.GdipDrawImageRect(NativeGraphics, image.Pointer(), x, y, width, height);
IgnoreMetafileErrors(image, ref status);
CheckErrorStatus(status);
@@ -2108,6 +2319,74 @@ public void DrawImage(Image image, RectangleF destRect, RectangleF srcRect, Grap
{
ArgumentNullException.ThrowIfNull(image);
+ if (SmoothingMode == Drawing2D.SmoothingMode.AntiAlias &&
+ _backingImage is { PixelFormat: PixelFormat.Format24bppRgb })
+ {
+ float left = destRect.X;
+ float right = destRect.X + destRect.Width;
+ float top = destRect.Y;
+ float bottom = destRect.Y + destRect.Height;
+
+ if (destRect.Width < 0)
+ {
+ left = right;
+ right = destRect.X;
+ }
+
+ if (destRect.Height < 0)
+ {
+ top = bottom;
+ bottom = destRect.Y;
+ }
+
+ if (left < 0 || top < 0 || right > _backingImage.Width || bottom > _backingImage.Height)
+ {
+ float clippedLeft = left;
+ float clippedRight = right;
+ float clippedTop = top;
+ float clippedBottom = bottom;
+
+ if (clippedLeft < 0)
+ {
+ clippedLeft = 0;
+ }
+
+ if (clippedRight > _backingImage.Width)
+ {
+ clippedRight = _backingImage.Width;
+ }
+
+ if (clippedTop < 0)
+ {
+ clippedTop = 0;
+ }
+
+ if (clippedBottom > _backingImage.Height)
+ {
+ clippedBottom = _backingImage.Height;
+ }
+
+ if (clippedLeft >= clippedRight || clippedTop >= clippedBottom)
+ {
+ return;
+ }
+
+ float destW = right - left;
+ float destH = bottom - top;
+
+ if (Math.Abs(destW) > float.Epsilon && Math.Abs(destH) > float.Epsilon)
+ {
+ float srcX = srcRect.X + (clippedLeft - left) / destW * srcRect.Width;
+ float srcY = srcRect.Y + (clippedTop - top) / destH * srcRect.Height;
+ float srcW = (clippedRight - clippedLeft) / destW * srcRect.Width;
+ float srcH = (clippedBottom - clippedTop) / destH * srcRect.Height;
+
+ destRect = new RectangleF(clippedLeft, clippedTop, clippedRight - clippedLeft, clippedBottom - clippedTop);
+ srcRect = new RectangleF(srcX, srcY, srcW, srcH);
+ }
+ }
+ }
+
Status status = PInvokeGdiPlus.GdipDrawImageRectRect(
NativeGraphics,
image.Pointer(),
diff --git a/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs b/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs
index e921aedb521..500ff990be5 100644
--- a/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs
+++ b/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs
@@ -3002,5 +3002,124 @@ public void Graphics_FillRoundedRectangle_Float()
graphics.FillRoundedRectangle(Brushes.Green, new RectangleF(0, 0, 10, 10), new(2, 2));
VerifyBitmapNotEmpty(bitmap);
}
+
+ [Theory]
+ [InlineData(190.5f, 180.5f, 100, 100)] // Out of bounds (bottom-right)
+ [InlineData(-10, 10, 100, 100)] // Out of bounds (left)
+ [InlineData(10, -10, 100, 100)] // Out of bounds (top)
+ [InlineData(200, 10, 100, 100)] // Out of bounds (right)
+ [InlineData(10, 200, 100, 100)] // Out of bounds (bottom)
+ [InlineData(10, 10, -100, 100)] // Out of bounds (negative width extending left)
+ [InlineData(10, 10, 100, -100)] // Out of bounds (negative height extending top)
+ public void FillRectangle_AntiAlias_24bppRgb_OutOfBounds_DoesNotThrow(float x, float y, float width, float height)
+ {
+ using Bitmap bmp = new(256, 256, PixelFormat.Format24bppRgb);
+ using Graphics graphics = Graphics.FromImage(bmp);
+ graphics.SmoothingMode = SmoothingMode.AntiAlias;
+
+ // This combination causes a crash in GDI+ on .NET 9+ (ExecutionEngineException)
+ // and AccessViolationException on .NET 8.
+ // We expect our fix to clip the rectangle and not throw.
+ graphics.FillRectangle(Brushes.Green, new RectangleF(x, y, width, height));
+ }
+
+ [Theory]
+ [InlineData(0, 0, 100, 100)] // Within bounds
+ [InlineData(156, 156, 100, 100)] // Exactly on bounds (256 - 100 = 156)
+ [InlineData(100, 100, -50, -50)] // Within bounds (negative width/height)
+ public void FillRectangle_AntiAlias_24bppRgb_WithinBounds_Success(float x, float y, float width, float height)
+ {
+ using Bitmap bmp = new(256, 256, PixelFormat.Format24bppRgb);
+ using Graphics graphics = Graphics.FromImage(bmp);
+ graphics.SmoothingMode = SmoothingMode.AntiAlias;
+
+ graphics.FillRectangle(Brushes.Green, new RectangleF(x, y, width, height));
+ }
+
+ [Fact]
+ public void FillRectangle_DefaultSmoothing_24bppRgb_OutOfBounds_Success()
+ {
+ using Bitmap bmp = new(256, 256, PixelFormat.Format24bppRgb);
+ using Graphics graphics = Graphics.FromImage(bmp);
+ // Default SmoothingMode is None (or Invalid/Default which maps to None behavior for this check)
+
+ // Should not throw
+ graphics.FillRectangle(Brushes.Green, new RectangleF(190.5f, 180.5f, 100, 100));
+ }
+
+ [Fact]
+ public void FillRectangle_AntiAlias_32bppArgb_OutOfBounds_Success()
+ {
+ using Bitmap bmp = new(256, 256, PixelFormat.Format32bppArgb);
+ using Graphics graphics = Graphics.FromImage(bmp);
+ graphics.SmoothingMode = SmoothingMode.AntiAlias;
+
+ // Should not throw
+ graphics.FillRectangle(Brushes.Green, new RectangleF(190.5f, 180.5f, 100, 100));
+ }
+
+ [Fact]
+ public void FillRectangles_AntiAlias_24bppRgb_OutOfBounds_DoesNotThrow()
+ {
+ using Bitmap bmp = new(256, 256, PixelFormat.Format24bppRgb);
+ using Graphics g = Graphics.FromImage(bmp);
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+
+ RectangleF[] rects = new[]
+ {
+ new RectangleF(190.5f, 180.5f, 100, 100), // Issue repro
+ new RectangleF(-100, 50, 50, 50), // Fully out left
+ new RectangleF(300, 50, 50, 50), // Fully out right
+ new RectangleF(-10, -10, 50, 50), // Partial top-left
+ new RectangleF(200, 200, 100, 100), // Partial bottom-right
+ new RectangleF(50, 50, 50, 50) // Fully inside
+ };
+
+ g.FillRectangles(Brushes.Green, rects);
+ }
+
+ [Fact]
+ public void DrawImage_Float_AntiAlias_24bppRgb_OutOfBounds_DoesNotThrow()
+ {
+ using Bitmap bmp = new(256, 256, PixelFormat.Format24bppRgb);
+ using Graphics g = Graphics.FromImage(bmp);
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+
+ using Bitmap srcImg = new(50, 50);
+ using Graphics srcG = Graphics.FromImage(srcImg);
+ srcG.Clear(Color.Red);
+
+ // Issue repro equivalent
+ g.DrawImage(srcImg, 190.5f, 180.5f, 100, 100);
+
+ // Fully out of bounds
+ g.DrawImage(srcImg, -100, 50, 50, 50);
+
+ // Partially out of bounds
+ g.DrawImage(srcImg, -10, -10, 50, 50);
+ }
+
+ [Fact]
+ public void DrawImage_RectF_AntiAlias_24bppRgb_OutOfBounds_DoesNotThrow()
+ {
+ using Bitmap bmp = new(256, 256, PixelFormat.Format24bppRgb);
+ using Graphics g = Graphics.FromImage(bmp);
+ g.SmoothingMode = SmoothingMode.AntiAlias;
+
+ using Bitmap srcImg = new(50, 50);
+ using Graphics srcG = Graphics.FromImage(srcImg);
+ srcG.Clear(Color.Red);
+
+ RectangleF srcRect = new(0, 0, 50, 50);
+
+ // Issue repro equivalent
+ g.DrawImage(srcImg, new RectangleF(190.5f, 180.5f, 100, 100), srcRect, GraphicsUnit.Pixel);
+
+ // Fully out of bounds
+ g.DrawImage(srcImg, new RectangleF(-100, 50, 50, 50), srcRect, GraphicsUnit.Pixel);
+
+ // Partially out of bounds
+ g.DrawImage(srcImg, new RectangleF(-10, -10, 50, 50), srcRect, GraphicsUnit.Pixel);
+ }
#endif
}