Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement CachedBitmap #39245

Closed
wants to merge 1 commit into from
Closed
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
8 changes: 6 additions & 2 deletions src/libraries/Common/tests/System/Drawing/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) { }
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,4 +461,8 @@
<data name="SystemDrawingCommon_PlatformNotSupported" xml:space="preserve">
<value>System.Drawing.Common is not supported on this platform.</value>
</data>
</root>
<data name="CachedBitmapNotSupported" xml:space="preserve">
<value>CachedBitmap is not supported on the installed version of libgdiplus. It is supported from version 6.1 onwards.</value>
</data>
</root>

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<Compile Include="System\Drawing\GraphicsUnit.cs" />
<Compile Include="System\Drawing\Image.cs" />
<Compile Include="System\Drawing\ImageType.cs" />
<Compile Include="System\Drawing\Imaging\CachedBitmap.cs" />
<Compile Include="System\Drawing\Pen.cs" />
<Compile Include="System\Drawing\Pens.cs" />
<Compile Include="System\Drawing\RotateFlipType.cs" />
Expand Down Expand Up @@ -186,6 +187,7 @@
<Compile Include="System\Drawing\ImageAnimator.Windows.cs" />
<Compile Include="System\Drawing\ImageInfo.cs" />
<Compile Include="System\Drawing\Imaging\BitmapData.Windows.cs" />
<Compile Include="System\Drawing\Imaging\CachedBitmap.Windows.cs" />
<Compile Include="System\Drawing\Imaging\Metafile.Windows.cs" />
<Compile Include="System\Drawing\Imaging\MetafileHeader.Windows.cs" />
<Compile Include="System\Drawing\Imaging\MetaHeader.Windows.cs" />
Expand Down Expand Up @@ -303,6 +305,7 @@
<Compile Include="System\Drawing\Icon.Unix.cs" />
<Compile Include="System\Drawing\SystemFonts.Unix.cs" />
<Compile Include="System\Drawing\Imaging\BitmapData.Unix.cs" />
<Compile Include="System\Drawing\Imaging\CachedBitmap.Unix.cs" />
<Compile Include="System\Drawing\Imaging\Metafile.Unix.cs" />
<Compile Include="System\Drawing\Imaging\MetafileHeader.Unix.cs" />
<Compile Include="System\Drawing\Imaging\MetaHeader.Unix.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<GetLibgdiplusVersion>(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;
Expand Down Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
20 changes: 20 additions & 0 deletions src/libraries/System.Drawing.Common/src/System/Drawing/Graphics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2091,6 +2091,26 @@ public void DrawImage(
CheckErrorStatus(status);
}

/// <summary>
/// Draws the image stored in the a <see cref="CachedBitmap"/> object.
/// </summary>
/// <param name="cachedBitmap">The <see cref="CachedBitmap"/> that contains the image to be drawn.</param>
/// <param name="x">The x-coordinate of the upper-left corner of the drawn image.</param>
/// <param name="y">The y-coordinate of the upper-left corner of the drawn image.</param>
/// <exception cref="ArgumentNullException"><paramref name="cachedBitmap"/> is <see langword="null"/>.</exception>
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);
}

/// <summary>
/// Draws a line connecting the two specified points.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
reflectronic marked this conversation as resolved.
Show resolved Hide resolved
{
// 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Stores a <see cref="Bitmap"/> in a format that is optimized for display on a particular device.
/// </summary>
public sealed partial class CachedBitmap : IDisposable
{
internal static readonly bool IsSupported = IsCachedBitmapSupported();
internal IntPtr nativeCachedBitmap;
reflectronic marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Initializes a new instance of the <see cref="CachedBitmap"/> class.
/// </summary>
/// <param name="bitmap">The bitmap to take the pixel data from.</param>
/// <param name="graphics">A <see cref="Graphics"/> object, representing the display device to optimize the bitmap for.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="bitmap"/> is <see langword="null" />.
/// - or -
/// <paramref name="graphics"/> is <see langword="null" />
/// </exception>
/// <exception cref="PlatformNotSupportedException">
/// The installed version of libgdiplus is lower than 6.1. This does not apply on Windows.
/// </exception>
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);
}

/// <summary>
/// Releases all resources used by this <see cref="CachedBitmap"/>.
/// </summary>
public void Dispose()
{
if (nativeCachedBitmap != IntPtr.Zero)
{
int status = Gdip.GdipDeleteCachedBitmap(new HandleRef(this, nativeCachedBitmap));
nativeCachedBitmap = IntPtr.Zero;
Gdip.CheckStatus(status);
}
}
}
}
137 changes: 137 additions & 0 deletions src/libraries/System.Drawing.Common/tests/Imaging/CachedBitmapTests.cs
Original file line number Diff line number Diff line change
@@ -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<ArgumentNullException>(() => new CachedBitmap(bitmap, null));
Assert.Throws<ArgumentNullException>(() => 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<ArgumentException>(() => 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<ArgumentNullException>(() => 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<object[]>
{
public IEnumerator<object[]> 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<InvalidOperationException>(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0));
graphics.RotateTransform(30);
Assert.Throws<InvalidOperationException>(() => graphics.DrawCachedBitmap(cachedBitmap, 0, 0));
}
}
}
Loading