diff --git a/src/System.Drawing.Common/src/System.Drawing.Common.csproj b/src/System.Drawing.Common/src/System.Drawing.Common.csproj index ecf5b2eb9a5..8fd027ef221 100644 --- a/src/System.Drawing.Common/src/System.Drawing.Common.csproj +++ b/src/System.Drawing.Common/src/System.Drawing.Common.csproj @@ -176,6 +176,7 @@ Since .NET 7, non-Windows platforms are not supported, even with the runtime con + diff --git a/src/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs b/src/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs index ac218b2e94d..dd8df18bf68 100644 --- a/src/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs +++ b/src/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs @@ -4455,6 +4455,25 @@ internal static partial int GdipComment( [LibraryImport(LibraryName)] internal static partial int GdipCreateBitmapFromStreamICM(IntPtr stream, IntPtr* bitmap); + +#if NET8_0_OR_GREATER + [LibraryImport(LibraryName)] + internal static partial int GdipCreateCachedBitmap( + [MarshalUsing(typeof(HandleRefMarshaller))] HandleRef bitmap, + [MarshalUsing(typeof(HandleRefMarshaller))] HandleRef graphics, + out nint cachedBitmap); + + [LibraryImport(LibraryName)] + internal static partial int GdipDeleteCachedBitmap( + nint cachedBitmap); + + [LibraryImport(LibraryName)] + internal static partial int GdipDrawCachedBitmap( + [MarshalUsing(typeof(HandleRefMarshaller))] HandleRef graphics, + [MarshalUsing(typeof(HandleRefMarshaller))] HandleRef cachedBitmap, + int x, + int y); +#endif } } } diff --git a/src/System.Drawing.Common/src/System/Drawing/Graphics.cs b/src/System.Drawing.Common/src/System/Drawing/Graphics.cs index 865ff419cc1..f4cfdc1e7f7 100644 --- a/src/System.Drawing.Common/src/System/Drawing/Graphics.cs +++ b/src/System.Drawing.Common/src/System/Drawing/Graphics.cs @@ -3851,5 +3851,36 @@ private static void CheckErrorStatus(int status) // Legitimate error, throw our status exception. throw Gdip.StatusException(status); } + +#if NET8_0_OR_GREATER + + /// + /// Draws the given . + /// + /// The that contains the image to be drawn. + /// The x-coordinate of the upper-left corner of the drawn image. + /// The y-coordinate of the upper-left corner of the drawn image. + /// is . + /// + /// + /// The is not compatible with the device state. + /// + /// + /// - or - + /// + /// + /// The object has a transform applied other than a translation. + /// + /// + public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y) + { + ArgumentNullException.ThrowIfNull(cachedBitmap); + + Gdip.CheckStatus(Gdip.GdipDrawCachedBitmap( + new(this, NativeGraphics), + new(cachedBitmap, cachedBitmap.Handle), + x, y)); + } +#endif } } diff --git a/src/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.cs b/src/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.cs new file mode 100644 index 00000000000..8421141b474 --- /dev/null +++ b/src/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; + +#if NET8_0_OR_GREATER + + +using static System.Drawing.SafeNativeMethods; + +namespace System.Drawing.Imaging; + +/// +/// A device dependent copy of a matching a specified object's current +/// device (display) settings. Avoids reformatting step when rendering, which can significantly improve performance. +/// +/// +/// +/// matches the current bit depth of the 's device. If the device bit +/// depth changes, the will no longer be usable and a new instance will need to be created +/// that matches. If the was created against it +/// will always work. +/// +/// +/// will not work with any transformations other than translation. +/// +/// +/// cannot be used to draw to a printer or metafile. +/// +/// +public sealed class CachedBitmap : IDisposable +{ + private nint _handle; + + /// + /// Create a device dependent copy of the given for the device settings of the given + /// + /// + /// The to convert. + /// The object to use to format the cached copy of the . + /// or is . + public CachedBitmap(Bitmap bitmap, Graphics graphics) + { + ArgumentNullException.ThrowIfNull(bitmap); + ArgumentNullException.ThrowIfNull(graphics); + + Gdip.CheckStatus(Gdip.GdipCreateCachedBitmap( + new(bitmap, bitmap.nativeImage), + new(graphics, graphics.NativeGraphics), + out _handle)); + } + + internal nint Handle => _handle; + + private void Dispose(bool disposing) + { + nint handle = Interlocked.Exchange(ref _handle, 0); + if (handle == 0) + { + return; + } + + int status = Gdip.GdipDeleteCachedBitmap(handle); + if (disposing) + { + // Don't want to throw on the finalizer thread. + Gdip.CheckStatus(status); + } + } + + ~CachedBitmap() => Dispose(disposing: false); + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} +#endif diff --git a/src/System.Drawing.Common/tests/GraphicsTests.cs b/src/System.Drawing.Common/tests/GraphicsTests.cs index 308bfec8488..72518f54af8 100644 --- a/src/System.Drawing.Common/tests/GraphicsTests.cs +++ b/src/System.Drawing.Common/tests/GraphicsTests.cs @@ -3197,5 +3197,85 @@ private static void VerifyGraphics(Graphics graphics, RectangleF expectedVisible Assert.Equal(new Matrix(), graphics.Transform); Assert.Equal(expectedVisibleClipBounds, graphics.VisibleClipBounds); } + +#if NET8_0_OR_GREATER + [Fact] + public void DrawCachedBitmap_ThrowsArgumentNullException() + { + using Bitmap bitmap = new(10, 10); + using Graphics graphics = Graphics.FromImage(bitmap); + + Assert.Throws(() => graphics.DrawCachedBitmap(null!, 0, 0)); + } + + [Fact] + public void DrawCachedBitmap_DisposedBitmap_ThrowsArgumentException() + { + using Bitmap bitmap = new(10, 10); + using Graphics graphics = Graphics.FromImage(bitmap); + CachedBitmap cachedBitmap = new(bitmap, graphics); + cachedBitmap.Dispose(); + + Assert.Throws(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0)); + } + + [Fact] + public void DrawCachedBitmap_Simple() + { + using Bitmap bitmap = new(10, 10); + using Graphics graphics = Graphics.FromImage(bitmap); + using CachedBitmap cachedBitmap = new(bitmap, graphics); + + graphics.DrawCachedBitmap(cachedBitmap, 0, 0); + } + + [Fact] + public void DrawCachedBitmap_Translate() + { + using Bitmap bitmap = new(10, 10); + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.TranslateTransform(1.0f, 8.0f); + using CachedBitmap cachedBitmap = new(bitmap, graphics); + + graphics.DrawCachedBitmap(cachedBitmap, 0, 0); + } + + [Fact] + public void DrawCachedBitmap_Rotation_ThrowsInvalidOperation() + { + using Bitmap bitmap = new(10, 10); + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.RotateTransform(36.0f); + using CachedBitmap cachedBitmap = new(bitmap, graphics); + + Assert.Throws(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0)); + } + + [Theory] + [InlineData(PixelFormat.Format16bppRgb555, PixelFormat.Format32bppRgb, false)] + [InlineData(PixelFormat.Format32bppRgb, PixelFormat.Format16bppRgb555, true)] + [InlineData(PixelFormat.Format32bppArgb, PixelFormat.Format16bppRgb555, false)] + public void DrawCachedBitmap_ColorDepthChange_ThrowsInvalidOperation( + PixelFormat sourceFormat, + PixelFormat destinationFormat, + bool shouldSucceed) + { + using Bitmap bitmap = new(10, 10, sourceFormat); + using Graphics graphics = Graphics.FromImage(bitmap); + using CachedBitmap cachedBitmap = new(bitmap, graphics); + + using Bitmap bitmap2 = new(10, 10, destinationFormat); + using Graphics graphics2 = Graphics.FromImage(bitmap2); + + if (shouldSucceed) + { + graphics2.DrawCachedBitmap(cachedBitmap, 0, 0); + } + else + { + Assert.Throws(() => graphics2.DrawCachedBitmap(cachedBitmap, 0, 0)); + } + } +#endif } } diff --git a/src/System.Drawing.Common/tests/Imaging/BitmapDataTests.cs b/src/System.Drawing.Common/tests/Imaging/BitmapDataTests.cs index 2a0ae84e55d..0eacf968005 100644 --- a/src/System.Drawing.Common/tests/Imaging/BitmapDataTests.cs +++ b/src/System.Drawing.Common/tests/Imaging/BitmapDataTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; diff --git a/src/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs b/src/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs new file mode 100644 index 00000000000..474dc8e4efe --- /dev/null +++ b/src/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Drawing.Imaging.Tests; + +#if NET8_0_OR_GREATER +public class CachedBitmapTests +{ + [Fact] + public void Ctor_Throws_ArgumentNullException() + { + using var bitmap = new Bitmap(10, 10); + using var graphics = Graphics.FromImage(bitmap); + + Assert.Throws(() => new CachedBitmap(bitmap, null)); + Assert.Throws(() => new CachedBitmap(null, graphics)); + } +} +#endif diff --git a/src/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj b/src/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj index 2b377f5c823..f584caaf7d3 100644 --- a/src/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj +++ b/src/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj @@ -37,6 +37,7 @@ +