committed by
Max Katz
9 changed files with 382 additions and 111 deletions
@ -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> |
|||
@ -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; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue