Skip to content

Commit acf5773

Browse files
authored
Merge pull request #114 from DarkPro1337/develop
Improve icon selection to pick the best-fitting icon size from ICO files
2 parents 41a30c7 + f089aba commit acf5773

File tree

2 files changed

+137
-34
lines changed

2 files changed

+137
-34
lines changed

src/NotifyIconWpf/Interop/SystemInfo.cs

Lines changed: 5 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -64,48 +64,20 @@ public static Point ScaleWithDpi(this Point point)
6464
};
6565
}
6666

67-
/// <summary>
68-
/// Scale the supplied size to the current DPI settings
69-
/// </summary>
70-
/// <param name="size"></param>
71-
/// <returns>Size</returns>
72-
[Pure]
73-
public static Size ScaleWithDpi(this Size size)
74-
{
75-
return new Size
76-
{
77-
Height = (int)(size.Height / DpiFactorY),
78-
Width = (int)(size.Width / DpiFactorX)
79-
};
80-
}
81-
8267
#region SmallIconSize
8368

84-
private static Size? _smallIconSize = null;
85-
8669
private const int CXSMICON = 49;
8770
private const int CYSMICON = 50;
8871

8972
/// <summary>
9073
/// Gets a value indicating the recommended size, in pixels, of a small icon
9174
/// </summary>
92-
public static Size SmallIconSize
93-
{
94-
get
75+
public static Size SmallIconSize =>
76+
new()
9577
{
96-
if (!_smallIconSize.HasValue)
97-
{
98-
Size smallIconSize = new Size
99-
{
100-
Height = WinApi.GetSystemMetrics(CYSMICON),
101-
Width = WinApi.GetSystemMetrics(CXSMICON)
102-
};
103-
_smallIconSize = smallIconSize.ScaleWithDpi();
104-
}
105-
106-
return _smallIconSize.Value;
107-
}
108-
}
78+
Height = WinApi.GetSystemMetrics(CYSMICON),
79+
Width = WinApi.GetSystemMetrics(CXSMICON)
80+
};
10981

11082
#endregion
11183
}

src/NotifyIconWpf/Util.cs

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
// Contact and Information: http://www.hardcodet.net
55

66
using System;
7+
using System.Collections.Generic;
78
using System.ComponentModel;
89
using System.Drawing;
10+
using System.IO;
11+
using System.Linq;
912
using System.Windows;
1013
using System.Windows.Input;
1114
using System.Windows.Media;
@@ -161,7 +164,135 @@ public static Icon ToIcon(this ImageSource imageSource)
161164
}
162165

163166
Interop.Size iconSize = SystemInfo.SmallIconSize;
164-
return new Icon(streamInfo.Stream, new System.Drawing.Size(iconSize.Width, iconSize.Height));
167+
168+
using var stream = streamInfo.Stream;
169+
var bestIcon = GetBestFitIcon(stream, new System.Drawing.Size(iconSize.Width, iconSize.Height));
170+
return bestIcon;
171+
}
172+
173+
/// <summary>
174+
/// Finds the best fitting icon from a stream based on the desired size.
175+
/// </summary>
176+
/// <param name="iconStream">The stream containing the icon data.</param>
177+
/// <param name="desiredSize">The desired size of the icon.</param>
178+
/// <returns>The best fitting icon as an <see cref="Icon"/> object.</returns>
179+
/// <exception cref="InvalidDataException">Thrown if the ICO file header is invalid or contains no images.</exception>
180+
/// <exception cref="EndOfStreamException">Thrown if the complete icon image data could not be read.</exception>
181+
private static Icon GetBestFitIcon(Stream iconStream, System.Drawing.Size desiredSize)
182+
{
183+
// Read the icon entries
184+
iconStream.Seek(0, SeekOrigin.Begin);
185+
using var reader = new BinaryReader(iconStream);
186+
187+
// Read and validate the ICONDIR header
188+
var idReserved = reader.ReadUInt16(); // Reserved (must be 0)
189+
var idType = reader.ReadUInt16(); // Resource Type (1 for icons)
190+
var idCount = reader.ReadUInt16(); // Number of images
191+
192+
if (idReserved != 0 || idType != 1)
193+
throw new InvalidDataException("Invalid ICO file header.");
194+
195+
if (idCount == 0)
196+
throw new InvalidDataException("The ICO file contains no images.");
197+
198+
// Read ICONDIRENTRYs
199+
var iconEntries = new List<IconEntry>();
200+
for (var i = 0; i < idCount; i++)
201+
{
202+
var entry = new IconEntry
203+
{
204+
Width = reader.ReadByte(),
205+
Height = reader.ReadByte(),
206+
ColorCount = reader.ReadByte(),
207+
Reserved = reader.ReadByte(),
208+
Planes = reader.ReadUInt16(),
209+
BitCount = reader.ReadUInt16(),
210+
BytesInRes = reader.ReadUInt32(),
211+
ImageOffset = reader.ReadUInt32()
212+
};
213+
214+
// Adjust for 256x256 icons, which are stored with width and height as 0
215+
if (entry.Width == 0) entry.Width = 256;
216+
if (entry.Height == 0) entry.Height = 256;
217+
218+
iconEntries.Add(entry);
219+
}
220+
221+
// Find icons greater than or equal to the desired size
222+
IconEntry bestEntry;
223+
var largerOrEqualIcons = iconEntries
224+
.Where(entry => entry.Width >= desiredSize.Width && entry.Height >= desiredSize.Height)
225+
.OrderBy(entry => entry.Width * entry.Height)
226+
.ThenBy(entry => entry.Width)
227+
.ThenBy(entry => entry.Height)
228+
.ToList();
229+
230+
if (largerOrEqualIcons.Any())
231+
{
232+
// Select the smallest icon among those larger or equal to the desired size
233+
bestEntry = largerOrEqualIcons.First();
234+
}
235+
else
236+
{
237+
// No larger icons; select the largest icon smaller than the desired size
238+
var smallerIcons = iconEntries
239+
.Where(entry => entry.Width < desiredSize.Width && entry.Height < desiredSize.Height)
240+
.OrderByDescending(entry => entry.Width * entry.Height)
241+
.ThenByDescending(entry => entry.Width)
242+
.ThenByDescending(entry => entry.Height)
243+
.ToList();
244+
245+
// If no icons are smaller or larger, select any available icon (unlikely case)
246+
bestEntry = smallerIcons.Any() ? smallerIcons.First() : iconEntries.FirstOrDefault();
247+
}
248+
249+
if (bestEntry == null)
250+
return null;
251+
252+
// Read the image data of the selected icon
253+
var iconImageData = new byte[bestEntry.BytesInRes];
254+
iconStream.Seek(bestEntry.ImageOffset, SeekOrigin.Begin);
255+
var bytesRead = iconStream.Read(iconImageData, 0, (int)bestEntry.BytesInRes);
256+
if (bytesRead != bestEntry.BytesInRes)
257+
throw new EndOfStreamException("Could not read the complete icon image data.");
258+
259+
// Create a new .ico file with the single best-matching image
260+
using var destStream = new MemoryStream();
261+
using var writer = new BinaryWriter(destStream);
262+
263+
writer.Write((ushort)0); // idReserved
264+
writer.Write((ushort)1); // idType
265+
writer.Write((ushort)1); // idCount
266+
267+
writer.Write(bestEntry.Width == 256 ? (byte)0 : (byte)bestEntry.Width);
268+
writer.Write(bestEntry.Height == 256 ? (byte)0 : (byte)bestEntry.Height);
269+
writer.Write(bestEntry.ColorCount);
270+
writer.Write(bestEntry.Reserved);
271+
writer.Write(bestEntry.Planes);
272+
writer.Write(bestEntry.BitCount);
273+
writer.Write(bestEntry.BytesInRes);
274+
writer.Write((uint)(6 + 16)); // Image data offset
275+
276+
// Write the image data
277+
writer.Write(iconImageData);
278+
279+
destStream.Seek(0, SeekOrigin.Begin);
280+
return new Icon(destStream);
281+
}
282+
283+
/// <summary>
284+
/// Represents an entry in the icon directory.
285+
/// </summary>
286+
private class IconEntry
287+
{
288+
public int Width;
289+
public int Height;
290+
public byte ColorCount;
291+
public byte Reserved;
292+
public ushort Planes;
293+
public ushort BitCount;
294+
public uint BytesInRes;
295+
public uint ImageOffset;
165296
}
166297

167298
#endregion

0 commit comments

Comments
 (0)