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 @@
+