diff --git a/src/NotifyIconWpf/Interop/SystemInfo.cs b/src/NotifyIconWpf/Interop/SystemInfo.cs index f9c8b22..9e72d46 100644 --- a/src/NotifyIconWpf/Interop/SystemInfo.cs +++ b/src/NotifyIconWpf/Interop/SystemInfo.cs @@ -64,48 +64,20 @@ public static Point ScaleWithDpi(this Point point) }; } - /// - /// Scale the supplied size to the current DPI settings - /// - /// - /// Size - [Pure] - public static Size ScaleWithDpi(this Size size) - { - return new Size - { - Height = (int)(size.Height / DpiFactorY), - Width = (int)(size.Width / DpiFactorX) - }; - } - #region SmallIconSize - private static Size? _smallIconSize = null; - private const int CXSMICON = 49; private const int CYSMICON = 50; /// /// Gets a value indicating the recommended size, in pixels, of a small icon /// - public static Size SmallIconSize - { - get + public static Size SmallIconSize => + new() { - if (!_smallIconSize.HasValue) - { - Size smallIconSize = new Size - { - Height = WinApi.GetSystemMetrics(CYSMICON), - Width = WinApi.GetSystemMetrics(CXSMICON) - }; - _smallIconSize = smallIconSize.ScaleWithDpi(); - } - - return _smallIconSize.Value; - } - } + Height = WinApi.GetSystemMetrics(CYSMICON), + Width = WinApi.GetSystemMetrics(CXSMICON) + }; #endregion } diff --git a/src/NotifyIconWpf/Util.cs b/src/NotifyIconWpf/Util.cs index a188064..6552e3b 100644 --- a/src/NotifyIconWpf/Util.cs +++ b/src/NotifyIconWpf/Util.cs @@ -4,8 +4,11 @@ // Contact and Information: http://www.hardcodet.net using System; +using System.Collections.Generic; using System.ComponentModel; using System.Drawing; +using System.IO; +using System.Linq; using System.Windows; using System.Windows.Input; using System.Windows.Media; @@ -161,7 +164,135 @@ public static Icon ToIcon(this ImageSource imageSource) } Interop.Size iconSize = SystemInfo.SmallIconSize; - return new Icon(streamInfo.Stream, new System.Drawing.Size(iconSize.Width, iconSize.Height)); + + using var stream = streamInfo.Stream; + var bestIcon = GetBestFitIcon(stream, new System.Drawing.Size(iconSize.Width, iconSize.Height)); + return bestIcon; + } + + /// + /// Finds the best fitting icon from a stream based on the desired size. + /// + /// The stream containing the icon data. + /// The desired size of the icon. + /// The best fitting icon as an object. + /// Thrown if the ICO file header is invalid or contains no images. + /// Thrown if the complete icon image data could not be read. + private static Icon GetBestFitIcon(Stream iconStream, System.Drawing.Size desiredSize) + { + // Read the icon entries + iconStream.Seek(0, SeekOrigin.Begin); + using var reader = new BinaryReader(iconStream); + + // Read and validate the ICONDIR header + var idReserved = reader.ReadUInt16(); // Reserved (must be 0) + var idType = reader.ReadUInt16(); // Resource Type (1 for icons) + var idCount = reader.ReadUInt16(); // Number of images + + if (idReserved != 0 || idType != 1) + throw new InvalidDataException("Invalid ICO file header."); + + if (idCount == 0) + throw new InvalidDataException("The ICO file contains no images."); + + // Read ICONDIRENTRYs + var iconEntries = new List(); + for (var i = 0; i < idCount; i++) + { + var entry = new IconEntry + { + Width = reader.ReadByte(), + Height = reader.ReadByte(), + ColorCount = reader.ReadByte(), + Reserved = reader.ReadByte(), + Planes = reader.ReadUInt16(), + BitCount = reader.ReadUInt16(), + BytesInRes = reader.ReadUInt32(), + ImageOffset = reader.ReadUInt32() + }; + + // Adjust for 256x256 icons, which are stored with width and height as 0 + if (entry.Width == 0) entry.Width = 256; + if (entry.Height == 0) entry.Height = 256; + + iconEntries.Add(entry); + } + + // Find icons greater than or equal to the desired size + IconEntry bestEntry; + var largerOrEqualIcons = iconEntries + .Where(entry => entry.Width >= desiredSize.Width && entry.Height >= desiredSize.Height) + .OrderBy(entry => entry.Width * entry.Height) + .ThenBy(entry => entry.Width) + .ThenBy(entry => entry.Height) + .ToList(); + + if (largerOrEqualIcons.Any()) + { + // Select the smallest icon among those larger or equal to the desired size + bestEntry = largerOrEqualIcons.First(); + } + else + { + // No larger icons; select the largest icon smaller than the desired size + var smallerIcons = iconEntries + .Where(entry => entry.Width < desiredSize.Width && entry.Height < desiredSize.Height) + .OrderByDescending(entry => entry.Width * entry.Height) + .ThenByDescending(entry => entry.Width) + .ThenByDescending(entry => entry.Height) + .ToList(); + + // If no icons are smaller or larger, select any available icon (unlikely case) + bestEntry = smallerIcons.Any() ? smallerIcons.First() : iconEntries.FirstOrDefault(); + } + + if (bestEntry == null) + return null; + + // Read the image data of the selected icon + var iconImageData = new byte[bestEntry.BytesInRes]; + iconStream.Seek(bestEntry.ImageOffset, SeekOrigin.Begin); + var bytesRead = iconStream.Read(iconImageData, 0, (int)bestEntry.BytesInRes); + if (bytesRead != bestEntry.BytesInRes) + throw new EndOfStreamException("Could not read the complete icon image data."); + + // Create a new .ico file with the single best-matching image + using var destStream = new MemoryStream(); + using var writer = new BinaryWriter(destStream); + + writer.Write((ushort)0); // idReserved + writer.Write((ushort)1); // idType + writer.Write((ushort)1); // idCount + + writer.Write(bestEntry.Width == 256 ? (byte)0 : (byte)bestEntry.Width); + writer.Write(bestEntry.Height == 256 ? (byte)0 : (byte)bestEntry.Height); + writer.Write(bestEntry.ColorCount); + writer.Write(bestEntry.Reserved); + writer.Write(bestEntry.Planes); + writer.Write(bestEntry.BitCount); + writer.Write(bestEntry.BytesInRes); + writer.Write((uint)(6 + 16)); // Image data offset + + // Write the image data + writer.Write(iconImageData); + + destStream.Seek(0, SeekOrigin.Begin); + return new Icon(destStream); + } + + /// + /// Represents an entry in the icon directory. + /// + private class IconEntry + { + public int Width; + public int Height; + public byte ColorCount; + public byte Reserved; + public ushort Planes; + public ushort BitCount; + public uint BytesInRes; + public uint ImageOffset; } #endregion