diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/PictureBox/PictureBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/PictureBox/PictureBox.cs index 976865fd5a8..fae4c3dc49e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/PictureBox/PictureBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/PictureBox/PictureBox.cs @@ -4,6 +4,8 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Drawing; +using System.Drawing.Drawing2D; +using System.Drawing.Imaging; using System.Net; using System.Runtime.InteropServices; using System.Windows.Forms.Layout; @@ -58,7 +60,11 @@ public partial class PictureBox : Control, ISupportInitialize private int _totalBytesRead; private MemoryStream? _tempDownloadStream; private const int ReadBlockSize = 4096; + private const int MaxCachedStretchImageBytes = 16 * 1024 * 1024; private byte[]? _readBuffer; + private Bitmap? _stretchedImageCache; + private Size _stretchedImageCacheSize; + private InterpolationMode _stretchedImageCacheInterpolationMode; private ImageInstallationType _imageInstallationType; private SendOrPostCallback? _loadCompletedDelegate; private SendOrPostCallback? _loadProgressDelegate; @@ -417,6 +423,7 @@ public Image? InitialImage private void InstallNewImage(Image? value, ImageInstallationType installationType) { StopAnimate(); + DisposeStretchedImageCache(); _image = value; LayoutTransaction.DoLayoutIf(AutoSize, this, this, PropertyNames.Image); @@ -841,6 +848,7 @@ public PictureBoxSizeMode SizeMode } _sizeMode = value; + DisposeStretchedImageCache(); AdjustSize(); Invalidate(); OnSizeModeChanged(EventArgs.Empty); @@ -1001,6 +1009,7 @@ protected override void Dispose(bool disposing) if (disposing) { StopAnimate(); + DisposeStretchedImageCache(); } DisposeImageStream(); @@ -1128,13 +1137,76 @@ protected override void OnPaint(PaintEventArgs pe) ? ImageRectangleFromSizeMode(PictureBoxSizeMode.CenterImage) : ImageRectangle; - pe.Graphics.DrawImage(_image, drawingRect); + if (TryGetStretchImageCache(drawingRect, pe.Graphics.InterpolationMode, out Bitmap? stretchedImage)) + { + pe.Graphics.DrawImage( + stretchedImage, + drawingRect, + new Rectangle(Point.Empty, drawingRect.Size), + GraphicsUnit.Pixel); + } + else + { + pe.Graphics.DrawImage(_image, drawingRect); + } } // Windows draws the border for us (see CreateParams) base.OnPaint(pe!); } + private bool TryGetStretchImageCache( + Rectangle drawingRect, + InterpolationMode interpolationMode, + [NotNullWhen(true)] out Bitmap? stretchedImage) + { + stretchedImage = null; + + // This cache tracks control state (size/mode/image assignment), but not in-place pixel edits on + // the same Image instance. Callers that mutate pixels should reassign Image or invalidate explicitly. + + if (_sizeMode != PictureBoxSizeMode.StretchImage + || _image is null + || _imageInstallationType == ImageInstallationType.ErrorOrInitial + || drawingRect.Width <= 0 + || drawingRect.Height <= 0 + || ImageAnimator.CanAnimate(_image) + || !CanCacheStretchImage(drawingRect.Size)) + { + return false; + } + + Size drawingSize = drawingRect.Size; + if (_stretchedImageCache is null + || _stretchedImageCacheSize != drawingSize + || _stretchedImageCacheInterpolationMode != interpolationMode) + { + DisposeStretchedImageCache(); + + _stretchedImageCache = new Bitmap(drawingSize.Width, drawingSize.Height, PixelFormat.Format32bppPArgb); + using Graphics cacheGraphics = Graphics.FromImage(_stretchedImageCache); + cacheGraphics.InterpolationMode = interpolationMode; + cacheGraphics.DrawImage(_image, new Rectangle(Point.Empty, drawingSize)); + + _stretchedImageCacheSize = drawingSize; + _stretchedImageCacheInterpolationMode = interpolationMode; + } + + stretchedImage = _stretchedImageCache; + return true; + } + + private static bool CanCacheStretchImage(Size drawingSize) + => (long)drawingSize.Width * drawingSize.Height * sizeof(uint) <= MaxCachedStretchImageBytes; + + private void DisposeStretchedImageCache() + { + _stretchedImageCache?.Dispose(); + _stretchedImageCache = null; + _stretchedImageCacheSize = Size.Empty; + _stretchedImageCacheInterpolationMode = InterpolationMode.Invalid; + } + protected override void OnVisibleChanged(EventArgs e) { base.OnVisibleChanged(e); @@ -1153,6 +1225,7 @@ protected override void OnParentChanged(EventArgs e) protected override void OnResize(EventArgs e) { base.OnResize(e); + if (_sizeMode == PictureBoxSizeMode.Zoom || _sizeMode == PictureBoxSizeMode.StretchImage || _sizeMode == PictureBoxSizeMode.CenterImage diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/PictureBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/PictureBoxTests.cs index 85f328a4c71..3a234b10a18 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/PictureBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/PictureBoxTests.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using System.Drawing; +using System.Drawing.Drawing2D; using System.Windows.Forms.TestUtilities; using Moq; using Point = System.Drawing.Point; @@ -2608,6 +2609,105 @@ public void PictureBox_OnPaint_InvokeWithImage_CallsPaint(PaintEventArgs eventAr Assert.Equal(1, callCount); } + [WinFormsFact] + public void PictureBox_OnPaint_StretchImage_CachesScaledImage() + { + using Bitmap sourceImage = new(16, 16); + using SubPictureBox pictureBox = new() + { + SizeMode = PictureBoxSizeMode.StretchImage, + Image = sourceImage, + Size = new Size(100, 50) + }; + using Bitmap canvas = new(100, 50); + using Graphics graphics = Graphics.FromImage(canvas); + using PaintEventArgs eventArgs = new(graphics, new Rectangle(Point.Empty, canvas.Size)); + + pictureBox.OnPaint(eventArgs); + Bitmap firstCache = GetStretchedImageCache(pictureBox); + Assert.NotNull(firstCache); + + pictureBox.OnPaint(eventArgs); + Bitmap secondCache = GetStretchedImageCache(pictureBox); + Assert.Same(firstCache, secondCache); + } + + [WinFormsFact] + public void PictureBox_OnPaint_StretchImage_WithNewImage_RecreatesScaledImageCache() + { + using Bitmap firstImage = new(16, 16); + using Bitmap secondImage = new(16, 16); + using SubPictureBox pictureBox = new() + { + SizeMode = PictureBoxSizeMode.StretchImage, + Image = firstImage, + Size = new Size(100, 50) + }; + using Bitmap canvas = new(100, 50); + using Graphics graphics = Graphics.FromImage(canvas); + using PaintEventArgs eventArgs = new(graphics, new Rectangle(Point.Empty, canvas.Size)); + + pictureBox.OnPaint(eventArgs); + Bitmap firstCache = GetStretchedImageCache(pictureBox); + Assert.NotNull(firstCache); + + pictureBox.Image = secondImage; + pictureBox.OnPaint(eventArgs); + Bitmap secondCache = GetStretchedImageCache(pictureBox); + Assert.NotNull(secondCache); + Assert.NotSame(firstCache, secondCache); + } + + [WinFormsFact] + public void PictureBox_OnPaint_StretchImage_WithOversizedTarget_DoesNotCreateScaledImageCache() + { + using Bitmap sourceImage = new(16, 16); + using SubPictureBox pictureBox = new() + { + SizeMode = PictureBoxSizeMode.StretchImage, + Image = sourceImage, + Size = new Size(3000, 2000) + }; + + using Bitmap canvas = new(1, 1); + using Graphics graphics = Graphics.FromImage(canvas); + using PaintEventArgs eventArgs = new(graphics, new Rectangle(Point.Empty, canvas.Size)); + + pictureBox.OnPaint(eventArgs); + + Assert.Null(pictureBox.TestAccessor.Dynamic._stretchedImageCache); + } + + [WinFormsFact] + public void PictureBox_OnPaint_StretchImage_WithInterpolationModeChanged_RecreatesScaledImageCache() + { + using Bitmap sourceImage = new(16, 16); + using SubPictureBox pictureBox = new() + { + SizeMode = PictureBoxSizeMode.StretchImage, + Image = sourceImage, + Size = new Size(100, 50) + }; + + using Bitmap firstCanvas = new(100, 50); + using Graphics firstGraphics = Graphics.FromImage(firstCanvas); + firstGraphics.InterpolationMode = InterpolationMode.Bilinear; + using PaintEventArgs firstEventArgs = new(firstGraphics, new Rectangle(Point.Empty, firstCanvas.Size)); + + pictureBox.OnPaint(firstEventArgs); + Bitmap firstCache = GetStretchedImageCache(pictureBox); + + using Bitmap secondCanvas = new(100, 50); + using Graphics secondGraphics = Graphics.FromImage(secondCanvas); + secondGraphics.InterpolationMode = InterpolationMode.NearestNeighbor; + using PaintEventArgs secondEventArgs = new(secondGraphics, new Rectangle(Point.Empty, secondCanvas.Size)); + + pictureBox.OnPaint(secondEventArgs); + Bitmap secondCache = GetStretchedImageCache(pictureBox); + + Assert.NotSame(firstCache, secondCache); + } + [WinFormsTheory] [NewAndDefaultData] public void PictureBox_OnParentChanged_Invoke_CallsParentChanged(EventArgs eventArgs) @@ -2929,4 +3029,9 @@ private class SubPictureBox : PictureBox public new void SetStyle(ControlStyles flag, bool value) => base.SetStyle(flag, value); } + + private static Bitmap GetStretchedImageCache(PictureBox pictureBox) + { + return Assert.IsType(pictureBox.TestAccessor.Dynamic._stretchedImageCache); + } }