Browse Source

Removed dependency on System.Drawing.Common (#13431)

release/11.0.8
Nikita Tsukanov 2 years ago
committed by Max Katz
parent
commit
c2dfabf1f4
  1. 1
      Avalonia.sln
  2. 6
      build/System.Drawing.Common.props
  3. 1
      src/Windows/Avalonia.Win32/Avalonia.Win32.csproj
  4. 102
      src/Windows/Avalonia.Win32/CursorFactory.cs
  5. 13
      src/Windows/Avalonia.Win32/IconImpl.cs
  6. 8
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  7. 337
      src/Windows/Avalonia.Win32/Interop/Win32Icon.cs
  8. 7
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  9. 18
      src/Windows/Avalonia.Win32/Win32Platform.cs

1
Avalonia.sln

@ -116,7 +116,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\SkiaSharp.props = build\SkiaSharp.props
build\SourceGenerators.props = build\SourceGenerators.props
build\SourceLink.props = build\SourceLink.props
build\System.Drawing.Common.props = build\System.Drawing.Common.props
build\System.Memory.props = build\System.Memory.props
build\TrimmingEnable.props = build\TrimmingEnable.props
build\UnitTests.NetFX.props = build\UnitTests.NetFX.props

6
build/System.Drawing.Common.props

@ -1,6 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Condition="'$(TargetFramework)'!='netstandard2.0'" Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Condition="'$(TargetFramework)'=='netstandard2.0'" Include="System.Drawing.Common" Version="4.5.0" />
</ItemGroup>
</Project>

1
src/Windows/Avalonia.Win32/Avalonia.Win32.csproj

@ -14,7 +14,6 @@
<MicroComIdl Include="Win32Com\win32.idl" CSharpInteropPath="Win32Com\Win32.Generated.cs" />
<MicroComIdl Include="DirectX\directx.idl" CSharpInteropPath="DirectX\directx.Generated.cs" />
</ItemGroup>
<Import Project="..\..\..\build\System.Drawing.Common.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\SourceGenerators.props" />

102
src/Windows/Avalonia.Win32/CursorFactory.cs

@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Utilities;
using Avalonia.Win32.Interop;
using SdBitmap = System.Drawing.Bitmap;
using SdPixelFormat = System.Drawing.Imaging.PixelFormat;
namespace Avalonia.Win32
{
@ -34,7 +35,7 @@ namespace Avalonia.Win32
IntPtr cursor = UnmanagedMethods.LoadCursor(mh, new IntPtr(id));
if (cursor != IntPtr.Zero)
{
Cache.Add(cursorType, new CursorImpl(cursor, false));
Cache.Add(cursorType, new CursorImpl(cursor));
}
}
}
@ -82,8 +83,7 @@ namespace Avalonia.Win32
if (!Cache.TryGetValue(cursorType, out var rv))
{
rv = new CursorImpl(
UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])),
false);
UnmanagedMethods.LoadCursor(IntPtr.Zero, new IntPtr(CursorTypeMapping[cursorType])));
Cache.Add(cursorType, rv);
}
@ -92,89 +92,22 @@ namespace Avalonia.Win32
public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot)
{
using var source = LoadSystemDrawingBitmap(cursor);
using var mask = AlphaToMask(source);
var info = new UnmanagedMethods.ICONINFO
{
IsIcon = false,
xHotspot = hotSpot.X,
yHotspot = hotSpot.Y,
MaskBitmap = mask.GetHbitmap(),
ColorBitmap = source.GetHbitmap(),
};
return new CursorImpl(UnmanagedMethods.CreateIconIndirect(ref info), true);
}
private static SdBitmap LoadSystemDrawingBitmap(IBitmapImpl bitmap)
{
using var memoryStream = new MemoryStream();
bitmap.Save(memoryStream);
return new SdBitmap(memoryStream);
}
private unsafe SdBitmap AlphaToMask(SdBitmap source)
{
var dest = new SdBitmap(source.Width, source.Height, SdPixelFormat.Format1bppIndexed);
if (source.PixelFormat == SdPixelFormat.Format32bppPArgb)
{
throw new NotSupportedException(
"Images with premultiplied alpha not yet supported as cursor images.");
}
if (source.PixelFormat != SdPixelFormat.Format32bppArgb)
{
return dest;
}
var sourceData = source.LockBits(
new Rectangle(default, source.Size),
ImageLockMode.ReadOnly,
SdPixelFormat.Format32bppArgb);
var destData = dest.LockBits(
new Rectangle(default, source.Size),
ImageLockMode.ReadOnly,
SdPixelFormat.Format1bppIndexed);
try
{
var pSource = (byte*)sourceData.Scan0.ToPointer();
var pDest = (byte*)destData.Scan0.ToPointer();
for (var y = 0; y < dest.Height; ++y)
{
for (var x = 0; x < dest.Width; ++x)
{
if (pSource[x * 4] == 0)
{
pDest[x / 8] |= (byte)(1 << (x % 8));
}
}
pSource += sourceData.Stride;
pDest += destData.Stride;
}
return dest;
}
finally
{
source.UnlockBits(sourceData);
dest.UnlockBits(destData);
}
return new CursorImpl(new Win32Icon(cursor, hotSpot));
}
}
internal class CursorImpl : ICursorImpl, IPlatformHandle
{
private readonly bool _isCustom;
private Win32Icon? _icon;
public CursorImpl(IntPtr handle, bool isCustom)
public CursorImpl(Win32Icon icon) : this(icon.Handle)
{
_icon = icon;
}
public CursorImpl(IntPtr handle)
{
Handle = handle;
_isCustom = isCustom;
}
public IntPtr Handle { get; private set; }
@ -182,9 +115,10 @@ namespace Avalonia.Win32
public void Dispose()
{
if (_isCustom && Handle != IntPtr.Zero)
if (_icon != null)
{
UnmanagedMethods.DestroyIcon(Handle);
_icon.Dispose();
_icon = null;
Handle = IntPtr.Zero;
}
}

13
src/Windows/Avalonia.Win32/IconImpl.cs

@ -2,23 +2,26 @@
using System.Drawing;
using System.IO;
using Avalonia.Platform;
using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
class IconImpl : IWindowIconImpl
{
private readonly Icon icon;
private readonly Win32Icon _icon;
private readonly byte[] _iconData;
public IconImpl(Icon icon)
public IconImpl(Win32Icon icon, byte[] iconData)
{
this.icon = icon;
_icon = icon;
_iconData = iconData;
}
public IntPtr HIcon => icon.Handle;
public IntPtr HIcon => _icon.Handle;
public void Save(Stream outputStream)
{
icon.Save(outputStream);
outputStream.Write(_iconData, 0, _iconData.Length);
}
}
}

8
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1324,6 +1324,10 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
public static extern IntPtr CreateIconIndirect([In] ref ICONINFO iconInfo);
[DllImport("user32.dll")]
public static extern IntPtr CreateIconFromResourceEx(byte* pbIconBits, uint cbIconBits,
int fIcon, int dwVersion, int csDesired, int cyDesired, int flags);
[DllImport("user32.dll")]
public static extern bool DestroyIcon(IntPtr hIcon);
@ -1611,6 +1615,8 @@ namespace Avalonia.Win32.Interop
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr CreateDIBSection(IntPtr hDC, ref BITMAPINFOHEADER pBitmapInfo, int un, out IntPtr lplpVoid, IntPtr handle, int dw);
[DllImport("gdi32.dll", SetLastError = true)]
public static extern IntPtr CreateBitmap(int width, int height, int planes, int bitCount, IntPtr data);
[DllImport("gdi32.dll")]
public static extern int DeleteObject(IntPtr hObject);
[DllImport("gdi32.dll", SetLastError = true)]
@ -2114,6 +2120,8 @@ namespace Avalonia.Win32.Interop
public enum DEVICECAP
{
HORZRES = 8,
BITSPIXEL = 12,
PLANES = 14,
DESKTOPHORZRES = 118
}

337
src/Windows/Avalonia.Win32/Interop/Win32Icon.cs

@ -0,0 +1,337 @@
using System;
using System.Buffers;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace Avalonia.Win32.Interop;
internal class Win32Icon : IDisposable
{
public Win32Icon(Bitmap bitmap, PixelPoint hotSpot = default)
{
Handle = CreateIcon(bitmap, hotSpot);
}
public Win32Icon(IBitmapImpl bitmap, PixelPoint hotSpot = default)
{
using var memoryStream = new MemoryStream();
bitmap.Save(memoryStream);
memoryStream.Position = 0;
using var bmp = new Bitmap(memoryStream);
Handle = CreateIcon(bmp, hotSpot);
}
public Win32Icon(byte[] iconData)
{
Handle = LoadIconFromData(iconData);
if (Handle == IntPtr.Zero)
{
using var bmp = new Bitmap(new MemoryStream(iconData));
Handle = CreateIcon(bmp);
}
}
public IntPtr Handle { get; private set; }
IntPtr CreateIcon(Bitmap bitmap, PixelPoint hotSpot = default)
{
var mainBitmap = CreateHBitmap(bitmap);
if (mainBitmap == IntPtr.Zero)
throw new Win32Exception();
var alphaBitmap = AlphaToMask(bitmap);
try
{
if (alphaBitmap == IntPtr.Zero)
throw new Win32Exception();
var info = new UnmanagedMethods.ICONINFO
{
IsIcon = false,
xHotspot = hotSpot.X,
yHotspot = hotSpot.Y,
MaskBitmap = alphaBitmap,
ColorBitmap = mainBitmap
};
var hIcon = UnmanagedMethods.CreateIconIndirect(ref info);
if (hIcon == IntPtr.Zero)
throw new Win32Exception();
return hIcon;
}
finally
{
UnmanagedMethods.DeleteObject(mainBitmap);
UnmanagedMethods.DeleteObject(alphaBitmap);
}
}
static IntPtr CreateHBitmap(Bitmap source)
{
using var fb = AllocFramebuffer(source.PixelSize, PixelFormats.Bgra8888);
source.CopyPixels(fb, AlphaFormat.Unpremul);
return UnmanagedMethods.CreateBitmap(source.PixelSize.Width, source.PixelSize.Height, 1, 32, fb.Address);
}
static unsafe IntPtr AlphaToMask(Bitmap source)
{
using var alphaMaskBuffer = AllocFramebuffer(source.PixelSize, PixelFormats.BlackWhite);
var height = alphaMaskBuffer.Size.Height;
var width = alphaMaskBuffer.Size.Width;
if (!source.Format!.Value.HasAlpha)
{
Unsafe.InitBlock((void*)alphaMaskBuffer.Address, 0xff,
(uint)(alphaMaskBuffer.RowBytes * alphaMaskBuffer.Size.Height));
}
else
{
using var argbBuffer = AllocFramebuffer(source.PixelSize, PixelFormat.Bgra8888);
source.CopyPixels(argbBuffer, AlphaFormat.Unpremul);
var pSource = (byte*)argbBuffer.Address;
var pDest = (byte*)alphaMaskBuffer.Address;
for (var y = 0; y < height; ++y)
{
for (var x = 0; x < width; ++x)
{
if (pSource[x * 4] == 0)
{
pDest[x / 8] |= (byte)(1 << (x % 8));
}
}
pSource += argbBuffer.RowBytes;
pDest += alphaMaskBuffer.RowBytes;
}
}
return UnmanagedMethods.CreateBitmap(width, height, 1, 1, alphaMaskBuffer.Address);
}
static LockedFramebuffer AllocFramebuffer(PixelSize size, PixelFormat format)
{
if (size.Width < 1 || size.Height < 1)
throw new ArgumentOutOfRangeException();
int stride = (size.Width * format.BitsPerPixel + 7) / 8;
var data = Marshal.AllocHGlobal(size.Height * stride);
if (data == IntPtr.Zero)
throw new OutOfMemoryException();
return new LockedFramebuffer(data, size, stride, new Vector(96, 96), format,
() => Marshal.FreeHGlobal(data));
}
// Needs to be packed to 2 to get ICONDIRENTRY to follow immediately after idCount.
[StructLayout(LayoutKind.Sequential, Pack = 2)]
public struct ICONDIR
{
// Must be 0
public ushort idReserved;
// Must be 1
public ushort idType;
// Count of entries
public ushort idCount;
// First entry (anysize array)
public ICONDIRENTRY idEntries;
}
[StructLayout(LayoutKind.Sequential)]
public struct ICONDIRENTRY
{
// Width and height are 1 - 255 or 0 for 256
public byte bWidth;
public byte bHeight;
public byte bColorCount;
public byte bReserved;
public ushort wPlanes;
public ushort wBitCount;
public uint dwBytesInRes;
public uint dwImageOffset;
}
private static int s_bitDepth;
static unsafe IntPtr LoadIconFromData(byte[] iconData, int width = 0, int height = 0)
{
if (iconData.Length < sizeof(ICONDIR))
return IntPtr.Zero;
// Get the correct width and height.
if (width == 0)
width = UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CXICON);
if (height == 0)
height = UnmanagedMethods.GetSystemMetrics(UnmanagedMethods.SystemMetric.SM_CYICON);
if (s_bitDepth == 0)
{
IntPtr dc = UnmanagedMethods.GetDC(IntPtr.Zero);
s_bitDepth = UnmanagedMethods.GetDeviceCaps(dc, UnmanagedMethods.DEVICECAP.BITSPIXEL);
s_bitDepth *= UnmanagedMethods.GetDeviceCaps(dc, UnmanagedMethods.DEVICECAP.PLANES);
UnmanagedMethods.ReleaseDC(IntPtr.Zero, dc);
// If the bitdepth is 8, make it 4 because windows does not
// choose a 256 color icon if the display is running in 256 color mode
// due to palette flicker.
if (s_bitDepth == 8)
{
s_bitDepth = 4;
}
}
fixed (byte* b = iconData)
{
var dir = (ICONDIR*)b;
if (dir->idReserved != 0 || dir->idType != 1 || dir->idCount == 0)
{
return IntPtr.Zero;
}
byte bestWidth = 0;
byte bestHeight = 0;
if (sizeof(ICONDIRENTRY) * (dir->idCount - 1) + sizeof(ICONDIR) > iconData.Length)
return IntPtr.Zero;
var entries = new ReadOnlySpan<ICONDIRENTRY>(&dir->idEntries, dir->idCount);
var _bestBytesInRes = 0u;
var _bestBitDepth = 0u;
var _bestImageOffset = 0u;
foreach (ICONDIRENTRY entry in entries)
{
bool fUpdateBestFit = false;
uint iconBitDepth;
if (entry.bColorCount != 0)
{
iconBitDepth = 4;
if (entry.bColorCount < 0x10)
{
iconBitDepth = 1;
}
}
else
{
iconBitDepth = entry.wBitCount;
}
// If it looks like if nothing is specified at this point then set the bits per pixel to 8.
if (iconBitDepth == 0)
{
iconBitDepth = 8;
}
// Windows rules for specifing an icon:
//
// 1. The icon with the closest size match.
// 2. For matching sizes, the image with the closest bit depth.
// 3. If there is no color depth match, the icon with the closest color depth that does not exceed the display.
// 4. If all icon color depth > display, lowest color depth is chosen.
// 5. color depth of > 8bpp are all equal.
// 6. Never choose an 8bpp icon on an 8bpp system.
if (_bestBytesInRes == 0)
{
fUpdateBestFit = true;
}
else
{
int bestDelta = Math.Abs(bestWidth - width) + Math.Abs(bestHeight - height);
int thisDelta = Math.Abs(entry.bWidth - width) + Math.Abs(entry.bHeight - height);
if ((thisDelta < bestDelta) ||
(thisDelta == bestDelta && (iconBitDepth <= s_bitDepth && iconBitDepth > _bestBitDepth ||
_bestBitDepth > s_bitDepth && iconBitDepth < _bestBitDepth)))
{
fUpdateBestFit = true;
}
}
if (fUpdateBestFit)
{
bestWidth = entry.bWidth;
bestHeight = entry.bHeight;
_bestImageOffset = entry.dwImageOffset;
_bestBytesInRes = entry.dwBytesInRes;
_bestBitDepth = iconBitDepth;
}
}
if (_bestImageOffset > int.MaxValue)
{
return IntPtr.Zero;
}
if (_bestBytesInRes > int.MaxValue)
{
return IntPtr.Zero;
}
uint endOffset;
try
{
endOffset = checked(_bestImageOffset + _bestBytesInRes);
}
catch (OverflowException)
{
return IntPtr.Zero;
}
if (endOffset > iconData.Length)
{
return IntPtr.Zero;
}
// Copy the bytes into an aligned buffer if needed.
if ((_bestImageOffset % IntPtr.Size) != 0)
{
// Beginning of icon's content is misaligned.
byte[] alignedBuffer = ArrayPool<byte>.Shared.Rent((int)_bestBytesInRes);
Array.Copy(iconData, _bestImageOffset, alignedBuffer, 0, _bestBytesInRes);
try
{
fixed (byte* pbAlignedBuffer = alignedBuffer)
{
return UnmanagedMethods.CreateIconFromResourceEx(pbAlignedBuffer, _bestBytesInRes, 1,
0x00030000, 0, 0, 0);
}
}
finally
{
ArrayPool<byte>.Shared.Return(alignedBuffer);
}
}
else
{
try
{
return UnmanagedMethods.CreateIconFromResourceEx(checked(b + _bestImageOffset), _bestBytesInRes,
1, 0x00030000, 0, 0, 0);
}
catch (OverflowException)
{
return IntPtr.Zero;
}
}
}
}
public void Dispose()
{
UnmanagedMethods.DestroyIcon(Handle);
Handle = IntPtr.Zero;
}
}

7
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -5,6 +5,7 @@ using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.LogicalTree;
using Avalonia.Media.Imaging;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Styling;
@ -15,7 +16,9 @@ namespace Avalonia.Win32
{
internal class TrayIconImpl : ITrayIconImpl
{
private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon();
private static readonly Win32Icon s_emptyIcon = new(new WriteableBitmap(new PixelSize(32, 32),
new Vector(96, 96),
PixelFormats.Bgra8888, AlphaFormat.Unpremul));
private readonly int _uniqueId;
private static int s_nextUniqueId;
private bool _iconAdded;
@ -91,7 +94,7 @@ namespace Avalonia.Win32
{
iconData.uFlags = NIF.TIP | NIF.MESSAGE | NIF.ICON;
iconData.uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE;
iconData.hIcon = _icon?.HIcon ?? s_emptyIcon;
iconData.hIcon = _icon?.HIcon ?? s_emptyIcon.Handle;
iconData.szTip = _tooltipText ?? "";
if (!_iconAdded)

18
src/Windows/Avalonia.Win32/Win32Platform.cs

@ -16,6 +16,7 @@ using Avalonia.Rendering.Composition;
using Avalonia.Threading;
using Avalonia.Utilities;
using Avalonia.Win32.Input;
using Avalonia.Win32.Interop;
using static Avalonia.Win32.Interop.UnmanagedMethods;
namespace Avalonia
@ -242,18 +243,11 @@ namespace Avalonia.Win32
private static IconImpl CreateIconImpl(Stream stream)
{
try
{
// new Icon() will work only if stream is an "ico" file.
return new IconImpl(new System.Drawing.Icon(stream));
}
catch (ArgumentException)
{
// Fallback to Bitmap creation and converting into a windows icon.
using var icon = new System.Drawing.Bitmap(stream);
var hIcon = icon.GetHicon();
return new IconImpl(System.Drawing.Icon.FromHandle(hIcon));
}
var ms = new MemoryStream();
stream.CopyTo(ms);
ms.Position = 0;
var iconData = ms.ToArray();
return new IconImpl(new Win32Icon(iconData), iconData);
}
private static void SetDpiAwareness()

Loading…
Cancel
Save