Skip to content

Improve icon selection to pick the best-fitting icon size from ICO files #114

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

Merged
merged 2 commits into from
Oct 3, 2024
Merged
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
38 changes: 5 additions & 33 deletions src/NotifyIconWpf/Interop/SystemInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,48 +64,20 @@ public static Point ScaleWithDpi(this Point point)
};
}

/// <summary>
/// Scale the supplied size to the current DPI settings
/// </summary>
/// <param name="size"></param>
/// <returns>Size</returns>
[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;

/// <summary>
/// Gets a value indicating the recommended size, in pixels, of a small icon
/// </summary>
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
}
Expand Down
133 changes: 132 additions & 1 deletion src/NotifyIconWpf/Util.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

/// <summary>
/// Finds the best fitting icon from a stream based on the desired size.
/// </summary>
/// <param name="iconStream">The stream containing the icon data.</param>
/// <param name="desiredSize">The desired size of the icon.</param>
/// <returns>The best fitting icon as an <see cref="Icon"/> object.</returns>
/// <exception cref="InvalidDataException">Thrown if the ICO file header is invalid or contains no images.</exception>
/// <exception cref="EndOfStreamException">Thrown if the complete icon image data could not be read.</exception>
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<IconEntry>();
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);
}

/// <summary>
/// Represents an entry in the icon directory.
/// </summary>
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
Expand Down