Files
LanMountainDesktop/LanMontainDesktop/Services/WindowsIconService.cs
lincube f0e44c0f87 0.19
2026-03-01 00:34:07 +08:00

860 lines
26 KiB
C#

using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
namespace LanMontainDesktop.Services;
[SupportedOSPlatform("windows")]
internal static class WindowsIconService
{
private const int HighResolutionIconSize = 256;
private const int MaxShellPath = 1024;
private const int StgmRead = 0x00000000;
private const uint SiigbfBiggerSizeOk = 0x00000001;
private const uint SiigbfIconOnly = 0x00000004;
private const uint ShgfiIcon = 0x00000100;
private const uint ShgfiLargeIcon = 0x00000000;
private const uint ShgfiUseFileAttributes = 0x00000010;
private const uint FileAttributeNormal = 0x00000080;
private const uint FileAttributeDirectory = 0x00000010;
private const uint CoinitApartmentThreaded = 0x2;
private const int SOk = 0;
private const int SFalse = 1;
private const int RpcEChangedMode = unchecked((int)0x80010106);
private static readonly Guid CLSID_ShellLink = new("00021401-0000-0000-C000-000000000046");
private static readonly Guid IID_IShellItemImageFactory = new("BCC18B79-BA16-442F-80C4-8A59C30C463B");
private static readonly Regex AumidRegex =
new(@"shell:AppsFolder\\(?<aumid>[^\s""]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static byte[]? TryGetIconPngBytes(string filePath)
{
if (!OperatingSystem.IsWindows() || string.IsNullOrWhiteSpace(filePath))
{
return null;
}
var normalizedEntryPath = Path.GetFullPath(filePath);
if (!File.Exists(normalizedEntryPath))
{
return null;
}
try
{
var extension = Path.GetExtension(normalizedEntryPath);
if (extension.Equals(".lnk", StringComparison.OrdinalIgnoreCase))
{
if (TryReadLnkIconLocation(normalizedEntryPath, out var iconLocation, out var iconIndex) &&
TryResolveIconPath(iconLocation, normalizedEntryPath, out var resolvedIconPath) &&
TryExtractIconFromResourceFile(resolvedIconPath, iconIndex, out var pngBytesFromLnkIconLocation))
{
return pngBytesFromLnkIconLocation;
}
if (TryReadLnkArguments(normalizedEntryPath, out var arguments) &&
TryParseAumidFromArguments(arguments, out var aumid))
{
var appsFolderPath = $"shell:AppsFolder\\{aumid}";
if (TryExtractIconWithShellItemImageFactory(appsFolderPath, out var pngBytesFromAppsFolder))
{
return pngBytesFromAppsFolder;
}
if (UwpManifestIconResolver.TryGetIconPngBytesFromAumid(aumid, out var pngBytesFromManifest))
{
return pngBytesFromManifest;
}
}
if (TryReadLnkTargetPath(normalizedEntryPath, out var targetPath) &&
TryExtractIconFromResourceFile(targetPath, 0, out var pngBytesFromLnkTarget))
{
return pngBytesFromLnkTarget;
}
}
else if (extension.Equals(".url", StringComparison.OrdinalIgnoreCase))
{
if (TryReadUrlIconLocation(normalizedEntryPath, out var iconFile, out var iconIndex) &&
TryResolveIconPath(iconFile, normalizedEntryPath, out var resolvedIconPath) &&
TryExtractIconFromResourceFile(resolvedIconPath, iconIndex, out var pngBytesFromUrlIconLocation))
{
return pngBytesFromUrlIconLocation;
}
}
if (TryExtractIconWithShellItemImageFactory(normalizedEntryPath, out var pngBytesFromShellItem))
{
return pngBytesFromShellItem;
}
if (TryExtractIconWithShGetFileInfo(normalizedEntryPath, out var pngBytesFromShGetFileInfo))
{
return pngBytesFromShGetFileInfo;
}
return null;
}
catch
{
return null;
}
}
public static byte[]? TryGetSystemFolderIconPngBytes()
{
if (!OperatingSystem.IsWindows())
{
return null;
}
// Prefer the HICON-based path first to preserve alpha better for folder glyphs.
if (TryExtractFolderIconWithShGetFileInfo(out var shGetFolderIcon) &&
shGetFolderIcon is not null)
{
return shGetFolderIcon;
}
var isWin11 = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000);
var preferredProbePaths = isWin11
? new[]
{
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
Environment.SystemDirectory
}
: new[]
{
Environment.SystemDirectory,
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
};
foreach (var probePath in preferredProbePaths)
{
if (string.IsNullOrWhiteSpace(probePath) || !Directory.Exists(probePath))
{
continue;
}
if (TryExtractIconWithShellItemImageFactory(probePath, out var shellFolderIcon))
{
return shellFolderIcon;
}
}
return null;
}
private static bool TryParseAumidFromArguments(string arguments, out string aumid)
{
aumid = string.Empty;
if (string.IsNullOrWhiteSpace(arguments))
{
return false;
}
var match = AumidRegex.Match(arguments);
if (!match.Success)
{
return false;
}
aumid = match.Groups["aumid"].Value.Trim().Trim('"');
return !string.IsNullOrWhiteSpace(aumid);
}
private static bool TryReadLnkIconLocation(string lnkFilePath, out string iconLocation, out int iconIndex)
{
iconLocation = string.Empty;
iconIndex = 0;
if (!TryInitializeCom(out var shouldUninitialize))
{
return false;
}
try
{
if (!TryCreateShellLink(out var shellLink))
{
return false;
}
try
{
if (!TryLoadShellLink(shellLink, lnkFilePath))
{
return false;
}
var iconPathBuilder = new StringBuilder(MaxShellPath);
if (shellLink.GetIconLocation(iconPathBuilder, iconPathBuilder.Capacity, out iconIndex) < 0)
{
return false;
}
iconLocation = iconPathBuilder.ToString().Trim();
return !string.IsNullOrWhiteSpace(iconLocation);
}
finally
{
Marshal.FinalReleaseComObject(shellLink);
}
}
catch
{
return false;
}
finally
{
UninitializeCom(shouldUninitialize);
}
}
private static bool TryReadLnkTargetPath(string lnkFilePath, out string targetPath)
{
targetPath = string.Empty;
if (!TryInitializeCom(out var shouldUninitialize))
{
return false;
}
try
{
if (!TryCreateShellLink(out var shellLink))
{
return false;
}
try
{
if (!TryLoadShellLink(shellLink, lnkFilePath))
{
return false;
}
var targetPathBuilder = new StringBuilder(MaxShellPath);
if (shellLink.GetPath(targetPathBuilder, targetPathBuilder.Capacity, IntPtr.Zero, 0) < 0)
{
return false;
}
targetPath = targetPathBuilder.ToString().Trim();
return !string.IsNullOrWhiteSpace(targetPath);
}
finally
{
Marshal.FinalReleaseComObject(shellLink);
}
}
catch
{
return false;
}
finally
{
UninitializeCom(shouldUninitialize);
}
}
private static bool TryReadLnkArguments(string lnkFilePath, out string arguments)
{
arguments = string.Empty;
if (!TryInitializeCom(out var shouldUninitialize))
{
return false;
}
try
{
if (!TryCreateShellLink(out var shellLink))
{
return false;
}
try
{
if (!TryLoadShellLink(shellLink, lnkFilePath))
{
return false;
}
var argumentsBuilder = new StringBuilder(MaxShellPath);
if (shellLink.GetArguments(argumentsBuilder, argumentsBuilder.Capacity) < 0)
{
return false;
}
arguments = argumentsBuilder.ToString().Trim();
return !string.IsNullOrWhiteSpace(arguments);
}
finally
{
Marshal.FinalReleaseComObject(shellLink);
}
}
catch
{
return false;
}
finally
{
UninitializeCom(shouldUninitialize);
}
}
private static bool TryCreateShellLink(out IShellLinkW shellLink)
{
shellLink = null!;
var shellLinkType = Type.GetTypeFromCLSID(CLSID_ShellLink);
if (shellLinkType is null)
{
return false;
}
shellLink = (IShellLinkW?)Activator.CreateInstance(shellLinkType)!;
return shellLink is not null;
}
private static bool TryLoadShellLink(IShellLinkW shellLink, string lnkFilePath)
{
if (shellLink is not IPersistFile persistFile)
{
return false;
}
try
{
persistFile.Load(lnkFilePath, StgmRead);
return true;
}
catch
{
return false;
}
}
private static bool TryReadUrlIconLocation(string urlFilePath, out string iconFile, out int iconIndex)
{
iconFile = string.Empty;
iconIndex = 0;
if (!File.Exists(urlFilePath))
{
return false;
}
try
{
foreach (var rawLine in File.ReadLines(urlFilePath))
{
var line = rawLine.Trim();
if (line.StartsWith("IconFile=", StringComparison.OrdinalIgnoreCase))
{
iconFile = line["IconFile=".Length..].Trim();
continue;
}
if (line.StartsWith("IconIndex=", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(line["IconIndex=".Length..].Trim(), out var parsedIndex))
{
iconIndex = parsedIndex;
}
}
}
catch
{
return false;
}
if (string.IsNullOrWhiteSpace(iconFile))
{
return false;
}
if (TrySplitIconFileAndIndex(iconFile, out var splitIconFile, out var splitIndex))
{
iconFile = splitIconFile;
if (iconIndex == 0)
{
iconIndex = splitIndex;
}
}
return true;
}
private static bool TryResolveIconPath(string rawIconLocation, string shortcutPath, out string resolvedIconPath)
{
resolvedIconPath = string.Empty;
if (string.IsNullOrWhiteSpace(rawIconLocation))
{
return false;
}
var cleaned = rawIconLocation.Trim().Trim('"');
if (cleaned.StartsWith("@", StringComparison.Ordinal))
{
cleaned = cleaned[1..];
}
if (TrySplitIconFileAndIndex(cleaned, out var splitPath, out _))
{
cleaned = splitPath;
}
cleaned = Environment.ExpandEnvironmentVariables(cleaned);
if (cleaned.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
cleaned.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!Path.IsPathRooted(cleaned))
{
var shortcutDirectory = Path.GetDirectoryName(shortcutPath);
if (!string.IsNullOrWhiteSpace(shortcutDirectory))
{
var relativeResolved = Path.GetFullPath(Path.Combine(shortcutDirectory, cleaned));
if (File.Exists(relativeResolved))
{
resolvedIconPath = relativeResolved;
return true;
}
}
var systemResolved = Path.Combine(Environment.SystemDirectory, cleaned);
if (File.Exists(systemResolved))
{
resolvedIconPath = systemResolved;
return true;
}
}
if (File.Exists(cleaned))
{
resolvedIconPath = cleaned;
return true;
}
return false;
}
private static bool TrySplitIconFileAndIndex(string rawValue, out string iconFile, out int iconIndex)
{
iconFile = rawValue.Trim();
iconIndex = 0;
if (string.IsNullOrWhiteSpace(iconFile))
{
return false;
}
var commaIndex = iconFile.LastIndexOf(',');
if (commaIndex <= 0 || commaIndex >= iconFile.Length - 1)
{
return false;
}
var possibleIndex = iconFile[(commaIndex + 1)..].Trim();
if (!int.TryParse(possibleIndex, out iconIndex))
{
return false;
}
iconFile = iconFile[..commaIndex].Trim().Trim('"');
return !string.IsNullOrWhiteSpace(iconFile);
}
private static bool TryExtractIconFromResourceFile(string resourceFilePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
if (string.IsNullOrWhiteSpace(resourceFilePath) || !File.Exists(resourceFilePath))
{
return false;
}
if (TryExtractIconWithShDefExtractIcon(resourceFilePath, iconIndex, out pngBytes))
{
return true;
}
if (TryExtractIconWithPrivateExtractIcons(resourceFilePath, iconIndex, out pngBytes))
{
return true;
}
return TryExtractIconWithExtractIconEx(resourceFilePath, iconIndex, out pngBytes);
}
private static bool TryExtractIconWithShDefExtractIcon(string filePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
var requestedSize = MakeLong(HighResolutionIconSize, HighResolutionIconSize);
var hr = SHDefExtractIcon(filePath, iconIndex, 0, out var largeIcon, out _, (uint)requestedSize);
if (hr < 0 || largeIcon == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(largeIcon);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(largeIcon);
}
}
private static bool TryExtractIconWithPrivateExtractIcons(string filePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
var iconHandles = new IntPtr[1];
var iconIds = new uint[1];
var extracted = PrivateExtractIcons(
filePath,
iconIndex,
HighResolutionIconSize,
HighResolutionIconSize,
iconHandles,
iconIds,
1,
0);
if (extracted == 0 || extracted == 0xFFFFFFFF || iconHandles[0] == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(iconHandles[0]);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(iconHandles[0]);
}
}
private static bool TryExtractIconWithExtractIconEx(string filePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
var largeIcons = new IntPtr[1];
var extracted = ExtractIconEx(filePath, iconIndex, largeIcons, null, 1);
if (extracted <= 0 || largeIcons[0] == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(largeIcons[0]);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(largeIcons[0]);
}
}
private static bool TryExtractIconWithShellItemImageFactory(string filePath, out byte[]? pngBytes)
{
pngBytes = null;
if (SHCreateItemFromParsingName(filePath, IntPtr.Zero, IID_IShellItemImageFactory, out var imageFactoryObject) < 0 ||
imageFactoryObject is null)
{
return false;
}
try
{
var imageFactory = (IShellItemImageFactory)imageFactoryObject;
var size = new SizeStruct { cx = HighResolutionIconSize, cy = HighResolutionIconSize };
var flags = SiigbfIconOnly | SiigbfBiggerSizeOk;
if (imageFactory.GetImage(size, flags, out var hBitmap) < 0 || hBitmap == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHbitmapToPngBytes(hBitmap);
return pngBytes is not null;
}
finally
{
_ = DeleteObject(hBitmap);
}
}
catch
{
return false;
}
finally
{
Marshal.FinalReleaseComObject(imageFactoryObject);
}
}
private static bool TryExtractIconWithShGetFileInfo(string filePath, out byte[]? pngBytes)
{
pngBytes = null;
if (SHGetFileInfo(
filePath,
FileAttributeNormal,
out var fileInfo,
(uint)Marshal.SizeOf<SHFILEINFO>(),
ShgfiIcon | ShgfiUseFileAttributes) == IntPtr.Zero ||
fileInfo.hIcon == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(fileInfo.hIcon);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(fileInfo.hIcon);
}
}
private static bool TryExtractFolderIconWithShGetFileInfo(out byte[]? pngBytes)
{
pngBytes = null;
if (SHGetFileInfo(
"folder",
FileAttributeDirectory,
out var fileInfo,
(uint)Marshal.SizeOf<SHFILEINFO>(),
ShgfiIcon | ShgfiLargeIcon | ShgfiUseFileAttributes) == IntPtr.Zero ||
fileInfo.hIcon == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(fileInfo.hIcon);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(fileInfo.hIcon);
}
}
private static byte[]? ConvertHiconToPngBytes(IntPtr iconHandle)
{
if (iconHandle == IntPtr.Zero)
{
return null;
}
try
{
using var icon = Icon.FromHandle(iconHandle);
var width = Math.Max(16, icon.Width);
var height = Math.Max(16, icon.Height);
using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.Transparent);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawIcon(icon, new Rectangle(0, 0, width, height));
}
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
}
catch
{
return null;
}
}
private static byte[]? ConvertHbitmapToPngBytes(IntPtr bitmapHandle)
{
if (bitmapHandle == IntPtr.Zero)
{
return null;
}
try
{
using var source = Image.FromHbitmap(bitmapHandle);
using var bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.Transparent);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, source.Width, source.Height);
}
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
}
catch
{
return null;
}
}
private static bool TryInitializeCom(out bool shouldUninitialize)
{
shouldUninitialize = false;
var result = CoInitializeEx(IntPtr.Zero, CoinitApartmentThreaded);
if (result is SOk or SFalse)
{
shouldUninitialize = true;
return true;
}
return result == RpcEChangedMode;
}
private static void UninitializeCom(bool shouldUninitialize)
{
if (shouldUninitialize)
{
CoUninitialize();
}
}
private static int MakeLong(int lowWord, int highWord)
{
return (highWord << 16) | (lowWord & 0xFFFF);
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
[StructLayout(LayoutKind.Sequential)]
private struct SizeStruct
{
public int cx;
public int cy;
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
[PreserveSig]
int GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cch, IntPtr pfd, uint fFlags);
[PreserveSig] int GetIDList(out IntPtr ppidl);
[PreserveSig] int SetIDList(IntPtr pidl);
[PreserveSig] int GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cch);
[PreserveSig] int SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
[PreserveSig] int GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cch);
[PreserveSig] int SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
[PreserveSig] int GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cch);
[PreserveSig] int SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
[PreserveSig] int GetHotkey(out short pwHotkey);
[PreserveSig] int SetHotkey(short wHotkey);
[PreserveSig] int GetShowCmd(out int piShowCmd);
[PreserveSig] int SetShowCmd(int iShowCmd);
[PreserveSig] int GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int iIcon);
[PreserveSig] int SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
[PreserveSig] int SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
[PreserveSig] int Resolve(IntPtr hwnd, uint fFlags);
[PreserveSig] int SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("BCC18B79-BA16-442F-80C4-8A59C30C463B")]
private interface IShellItemImageFactory
{
[PreserveSig]
int GetImage(SizeStruct size, uint flags, out IntPtr phbm);
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SHGetFileInfo(
string pszPath,
uint dwFileAttributes,
out SHFILEINFO psfi,
uint cbFileInfo,
uint uFlags);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern int SHDefExtractIcon(
string pszIconFile,
int iIndex,
uint uFlags,
out IntPtr phiconLarge,
out IntPtr phiconSmall,
uint nIconSize);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern int SHCreateItemFromParsingName(
[MarshalAs(UnmanagedType.LPWStr)] string pszPath,
IntPtr pbc,
[MarshalAs(UnmanagedType.LPStruct)] Guid riid,
[MarshalAs(UnmanagedType.Interface)] out object ppv);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern uint PrivateExtractIcons(
string szFileName,
int nIconIndex,
int cxIcon,
int cyIcon,
IntPtr[] phicon,
uint[] piconid,
uint nIcons,
uint flags);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern uint ExtractIconEx(
string lpszFile,
int nIconIndex,
IntPtr[]? phiconLarge,
IntPtr[]? phiconSmall,
uint nIcons);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DestroyIcon(IntPtr hIcon);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("ole32.dll")]
private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
[DllImport("ole32.dll")]
private static extern void CoUninitialize();
}