diff --git a/src/libraries/Common/tests/System/Drawing/Helpers.cs b/src/libraries/Common/tests/System/Drawing/Helpers.cs index bf154135a8c0cb..2e0652ccce1897 100644 --- a/src/libraries/Common/tests/System/Drawing/Helpers.cs +++ b/src/libraries/Common/tests/System/Drawing/Helpers.cs @@ -14,6 +14,7 @@ public static class Helpers { public const string IsDrawingSupported = nameof(Helpers) + "." + nameof(GetIsDrawingSupported); public const string IsWindowsOrAtLeastLibgdiplus6 = nameof(Helpers) + "." + nameof(GetIsWindowsOrAtLeastLibgdiplus6); + public const string IsCachedBitmapSupported = nameof(Helpers) + "." + nameof(GetIsCachedBitmapSupported); public const string RecentGdiplusIsAvailable = nameof(Helpers) + "." + nameof(GetRecentGdiPlusIsAvailable); public const string RecentGdiplusIsAvailable2 = nameof(Helpers) + "." + nameof(GetRecentGdiPlusIsAvailable2); public const string GdiPlusIsAvailableNotRedhat73 = nameof(Helpers) + "." + nameof(GetGdiPlusIsAvailableNotRedhat73); @@ -23,7 +24,10 @@ public static class Helpers public static bool GetIsDrawingSupported() => PlatformDetection.IsDrawingSupported; - public static bool GetIsWindowsOrAtLeastLibgdiplus6() + public static bool GetIsCachedBitmapSupported() => GetIsWindowsOrAtLeastLibgdiplus(6, 1); + public static bool GetIsWindowsOrAtLeastLibgdiplus6() => GetIsWindowsOrAtLeastLibgdiplus(6, 0); + + public static bool GetIsWindowsOrAtLeastLibgdiplus(int major, int minor) { if (!PlatformDetection.IsDrawingSupported) { @@ -50,7 +54,7 @@ public static bool GetIsWindowsOrAtLeastLibgdiplus6() return false; } - return installedVersion.Major >= 6; + return installedVersion.Major > major || installedVersion.Major == major && installedVersion.Minor >= minor; } public static bool IsNotUnix => PlatformDetection.IsWindows; diff --git a/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs b/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs index 9f54ee11672f92..f732546d6153e6 100644 --- a/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs +++ b/src/libraries/System.Drawing.Common/ref/System.Drawing.Common.cs @@ -408,6 +408,7 @@ public void DrawBezier(System.Drawing.Pen pen, System.Drawing.PointF pt1, System public void DrawBezier(System.Drawing.Pen pen, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4) { } public void DrawBeziers(System.Drawing.Pen pen, System.Drawing.PointF[] points) { } public void DrawBeziers(System.Drawing.Pen pen, System.Drawing.Point[] points) { } + public void DrawCachedBitmap(System.Drawing.Imaging.CachedBitmap cachedBitmap, int x, int y) { } public void DrawClosedCurve(System.Drawing.Pen pen, System.Drawing.PointF[] points) { } public void DrawClosedCurve(System.Drawing.Pen pen, System.Drawing.PointF[] points, float tension, System.Drawing.Drawing2D.FillMode fillmode) { } public void DrawClosedCurve(System.Drawing.Pen pen, System.Drawing.Point[] points) { } @@ -1752,6 +1753,11 @@ public BitmapData() { } public int Stride { get { throw null; } set { } } public int Width { get { throw null; } set { } } } + public sealed class CachedBitmap : System.IDisposable + { + public CachedBitmap(System.Drawing.Bitmap bitmap, System.Drawing.Graphics graphics) { throw null; } + public void Dispose() { } + } public enum ColorAdjustType { Default = 0, diff --git a/src/libraries/System.Drawing.Common/src/Resources/Strings.resx b/src/libraries/System.Drawing.Common/src/Resources/Strings.resx index f5ba77ee8ed377..5b3e673d95d68f 100644 --- a/src/libraries/System.Drawing.Common/src/Resources/Strings.resx +++ b/src/libraries/System.Drawing.Common/src/Resources/Strings.resx @@ -461,4 +461,8 @@ System.Drawing.Common is not supported on this platform. - \ No newline at end of file + + CachedBitmap is not supported on the installed version of libgdiplus. It is supported from version 6.1 onwards. + + + diff --git a/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj b/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj index 0a20f12215117d..bbd1b238afa910 100644 --- a/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj +++ b/src/libraries/System.Drawing.Common/src/System.Drawing.Common.csproj @@ -27,6 +27,7 @@ + @@ -186,6 +187,7 @@ + @@ -303,6 +305,7 @@ + diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.Unix.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.Unix.cs index 06b37317862db3..1a994b5df544f7 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.Unix.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.Unix.cs @@ -19,6 +19,8 @@ internal unsafe partial class Gdip internal const string LibraryName = "libgdiplus"; public static IntPtr Display = IntPtr.Zero; + public static GetLibgdiplusVersion? GetLibgdiplusVersion; + // Indicates whether X11 is available. It's available on Linux but not on recent macOS versions // When set to false, where Carbon Drawing is used instead. // macOS users can force X11 by setting the SYSTEM_DRAWING_COMMON_FORCE_X11 flag. @@ -43,10 +45,18 @@ internal static IntPtr LoadNativeLibrary() // the name suffixed with ".0". if (!NativeLibrary.TryLoad("libgdiplus.so", assembly, default, out lib)) { - NativeLibrary.TryLoad("libgdiplus.so.0", assembly, default, out lib); + NativeLibrary.TryLoad("libgdiplus.so.0", assembly, default, out lib); } } + // The GetLibgdiplusVersion function is relatively new. It is needed to check for CachedBitmap support. + // Instead of blindly P/Invoking into this function, we should safely search for the export. + // If it's not present, then we will know that CachedBitmap is not supported anyway. + if (lib != IntPtr.Zero && NativeLibrary.TryGetExport(lib, "GetLibgdiplusVersion", out IntPtr func)) + { + GetLibgdiplusVersion = Marshal.GetDelegateForFunctionPointer(func); + } + // This function may return a null handle. If it does, individual functions loaded from it will throw a DllNotFoundException, // but not until an attempt is made to actually use the function (rather than load it). This matches how PInvokes behave. return lib; @@ -418,4 +428,7 @@ internal static extern int GdipGetPostScriptGraphicsContext( internal unsafe delegate int StreamPutBytesDelegate(byte* buf, int bufsz); internal delegate void StreamCloseDelegate(); internal delegate long StreamSizeDelegate(); + + [UnmanagedFunctionPointer(CallingConvention.Winapi, CharSet = CharSet.Ansi)] + internal unsafe delegate string GetLibgdiplusVersion(); } diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs index f35039fb59561a..b1b52c8174aa29 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/GdiplusNative.cs @@ -1353,6 +1353,15 @@ internal static partial class Gdip [DllImport(LibraryName, ExactSpelling = true)] internal static extern int GdipGetEncoderParameterList(HandleRef image, ref Guid encoder, int size, IntPtr buffer); + + [DllImport(LibraryName, ExactSpelling = true)] + internal static extern int GdipCreateCachedBitmap(HandleRef bitmap, HandleRef graphics, out IntPtr cachedBitmap); + + [DllImport(LibraryName, ExactSpelling = true)] + internal static extern int GdipDeleteCachedBitmap(HandleRef cachedBitmap); + + [DllImport(LibraryName, ExactSpelling = true)] + internal static extern int GdipDrawCachedBitmap(HandleRef graphics, HandleRef cachedBitmap, int x, int y); } [StructLayout(LayoutKind.Sequential)] diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs index 29b6b24749241d..466a97b1b37bb8 100644 --- a/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs @@ -2091,6 +2091,26 @@ public void DrawImage( CheckErrorStatus(status); } + /// + /// Draws the image stored in the a object. + /// + /// 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 . + public void DrawCachedBitmap(CachedBitmap cachedBitmap, int x, int y) + { + if (cachedBitmap is null) + throw new ArgumentNullException(nameof(cachedBitmap)); + + int status = Gdip.GdipDrawCachedBitmap( + new HandleRef(this, NativeGraphics), + new HandleRef(cachedBitmap, cachedBitmap.nativeCachedBitmap), + x, y); + + CheckErrorStatus(status); + } + /// /// Draws a line connecting the two specified points. /// diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.Unix.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.Unix.cs new file mode 100644 index 00000000000000..09e9cc98984bf1 --- /dev/null +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.Unix.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Gdip = System.Drawing.SafeNativeMethods.Gdip; + +namespace System.Drawing.Imaging +{ + public sealed partial class CachedBitmap + { + internal static bool IsCachedBitmapSupported() + { + // CachedBitmap is only supported on libgdiplus 6.1 and above. + // The function to check for the version is only present on libgdiplus 6.0 and above. + if (Gdip.GetLibgdiplusVersion is null) + return false; + + var version = new Version(Gdip.GetLibgdiplusVersion()); + return version.Major > 6 || version.Major == 6 && version.Minor >= 1; + } + } +} diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.Windows.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.Windows.cs new file mode 100644 index 00000000000000..e3650c568b7090 --- /dev/null +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.Windows.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Drawing.Imaging +{ + public sealed partial class CachedBitmap + { + internal static bool IsCachedBitmapSupported() => true; + } +} diff --git a/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.cs b/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.cs new file mode 100644 index 00000000000000..a407292b373f3b --- /dev/null +++ b/src/libraries/System.Drawing.Common/src/System/Drawing/Imaging/CachedBitmap.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Gdip = System.Drawing.SafeNativeMethods.Gdip; + +namespace System.Drawing.Imaging +{ + /// + /// Stores a in a format that is optimized for display on a particular device. + /// + public sealed partial class CachedBitmap : IDisposable + { + internal static readonly bool IsSupported = IsCachedBitmapSupported(); + internal IntPtr nativeCachedBitmap; + + /// + /// Initializes a new instance of the class. + /// + /// The bitmap to take the pixel data from. + /// A object, representing the display device to optimize the bitmap for. + /// + /// is . + /// - or - + /// is + /// + /// + /// The installed version of libgdiplus is lower than 6.1. This does not apply on Windows. + /// + public CachedBitmap(Bitmap bitmap, Graphics graphics) + { + if (bitmap is null) + throw new ArgumentNullException(nameof(bitmap)); + + if (graphics is null) + throw new ArgumentNullException(nameof(graphics)); + + if (!IsSupported) + throw new PlatformNotSupportedException(SR.CachedBitmapNotSupported); + + int status = Gdip.GdipCreateCachedBitmap(new HandleRef(bitmap, bitmap.nativeImage), + new HandleRef(graphics, graphics.NativeGraphics), + out nativeCachedBitmap); + + Gdip.CheckStatus(status); + } + + /// + /// Releases all resources used by this . + /// + public void Dispose() + { + if (nativeCachedBitmap != IntPtr.Zero) + { + int status = Gdip.GdipDeleteCachedBitmap(new HandleRef(this, nativeCachedBitmap)); + nativeCachedBitmap = IntPtr.Zero; + Gdip.CheckStatus(status); + } + } + } +} diff --git a/src/libraries/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs b/src/libraries/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs new file mode 100644 index 00000000000000..6556be0b62f087 --- /dev/null +++ b/src/libraries/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Drawing.Imaging; +using System.Linq; +using System.Security.Permissions; +using System.Text; +using System.Threading.Tasks; + +using Xunit; + +namespace System.Drawing.Imaging.Tests +{ + public class CachedBitmapTests + { + [ConditionalFact(Helpers.IsCachedBitmapSupported)] + 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)); + } + + [ConditionalFact(Helpers.IsCachedBitmapSupported)] + public void Disposed_CachedBitmap_Throws_ArgumentException() + { + using var bitmap = new Bitmap(10, 10); + using var graphics = Graphics.FromImage(bitmap); + using var cached = new CachedBitmap(bitmap, graphics); + + cached.Dispose(); + + Assert.Throws(() => graphics.DrawCachedBitmap(cached, 0, 0)); + } + + [ConditionalFact(Helpers.IsCachedBitmapSupported)] + public void DrawCachedBitmap_Throws_ArgumentNullException() + { + using var bitmap = new Bitmap(10, 10); + using var graphics = Graphics.FromImage(bitmap); + Assert.Throws(() => graphics.DrawCachedBitmap(null, 0, 0)); + } + + static string[] bitmaps = new string[] + { + "81674-2bpp.png", + "64x64_one_entry_8bit.ico", + "16x16_one_entry_4bit.ico", + "16x16_nonindexed_24bit.png" + }; + + public class CachedBitmapOffsetTestData : IEnumerable + { + public IEnumerator GetEnumerator() + { + foreach (string bitmap in bitmaps) + { + yield return new object[] { bitmap, 0, 0 }; + yield return new object[] { bitmap, 20, 20 }; + yield return new object[] { bitmap, 200, 200 }; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + static void CompareEqual(Bitmap expected, Bitmap actual, int xOffset = 0, int yOffset = 0) + { + for (int x = 0; x < expected.Width; x++) + { + for (int y = 0; y < expected.Height; y++) + { + Color expectedColor = expected.GetPixel(x, y); + Color actualColor = actual.GetPixel(x + xOffset, y + yOffset); + Assert.Equal(expectedColor, actualColor); + } + } + } + + [ConditionalTheory(Helpers.IsCachedBitmapSupported)] + [ClassData(typeof(CachedBitmapOffsetTestData))] + public void CachedBitmap_Drawing_Roundtrips(string filename, int xOffset, int yOffset) + { + using var originalBitmap = new Bitmap(Helpers.GetTestBitmapPath(filename)); + + using var surface = new Bitmap(originalBitmap.Width + xOffset, originalBitmap.Height + yOffset); + using var graphics = Graphics.FromImage(surface); + using var cachedBitmap = new CachedBitmap(originalBitmap, graphics); + + graphics.DrawCachedBitmap(cachedBitmap, xOffset, yOffset); + + CompareEqual(originalBitmap, surface, xOffset, yOffset); + } + + [ConditionalFact(Helpers.IsCachedBitmapSupported)] + public void CachedBitmap_Respects_ClipRectangle() + { + using var originalBitmap = new Bitmap(Helpers.GetTestBitmapPath("cachedbitmap_test_original.png")); + using var clippedBitmap = new Bitmap(Helpers.GetTestBitmapPath("cachedbitmap_test_clip_20_20_20_20.png")); + + using var surface = new Bitmap(originalBitmap.Width, originalBitmap.Height); + using var graphics = Graphics.FromImage(surface); + using var cachedBitmap = new CachedBitmap(originalBitmap, graphics); + + graphics.Clip = new Region(new Rectangle(20, 20, 20, 20)); + graphics.DrawCachedBitmap(cachedBitmap, 0, 0); + + CompareEqual(clippedBitmap, surface); + } + + [ConditionalFact(Helpers.IsCachedBitmapSupported)] + public void CachedBitmap_Respects_TranslationMatrix() + { + using var originalBitmap = new Bitmap(Helpers.GetTestBitmapPath("cachedbitmap_test_original.png")); + using var translatedBitmap = new Bitmap(Helpers.GetTestBitmapPath("cachedbitmap_test_translate_30_30.png")); + + using var surface = new Bitmap(originalBitmap.Width, originalBitmap.Height); + using var graphics = Graphics.FromImage(surface); + using var cachedBitmap = new CachedBitmap(originalBitmap, graphics); + + graphics.TranslateTransform(30, 30); + graphics.DrawCachedBitmap(cachedBitmap, 0, 0); + + CompareEqual(translatedBitmap, surface); + + graphics.ScaleTransform(30, 30); + Assert.Throws(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0)); + graphics.RotateTransform(30); + Assert.Throws(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0)); + } + } +} diff --git a/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj b/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj index 0b1354728a100f..6d8c434cb06f14 100644 --- a/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj +++ b/src/libraries/System.Drawing.Common/tests/System.Drawing.Common.Tests.csproj @@ -88,6 +88,9 @@ + + +