Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -841,6 +848,7 @@ public PictureBoxSizeMode SizeMode
}

_sizeMode = value;
DisposeStretchedImageCache();
AdjustSize();
Invalidate();
OnSizeModeChanged(EventArgs.Empty);
Expand Down Expand Up @@ -1001,6 +1009,7 @@ protected override void Dispose(bool disposing)
if (disposing)
{
StopAnimate();
DisposeStretchedImageCache();
}

DisposeImageStream();
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<EventArgs>]
public void PictureBox_OnParentChanged_Invoke_CallsParentChanged(EventArgs eventArgs)
Expand Down Expand Up @@ -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<Bitmap>(pictureBox.TestAccessor.Dynamic._stretchedImageCache);
}
}