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 }