A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System.IO;
5using System.Runtime.InteropServices;
6using System;
7using System.Linq;
8using JetBrains.Annotations;
9
10namespace osu.Framework.Platform.Windows.Native
11{
12 internal class IconGroup
13 {
14 [StructLayout(LayoutKind.Sequential)]
15 internal struct IconDirEntry
16 {
17 internal byte Width;
18 internal byte Height;
19 internal byte ColourCount;
20 internal byte Reserved;
21 internal ushort Planes;
22 internal ushort BitCount;
23 internal uint BytesInResource;
24 internal uint ImageOffset;
25
26 // larger icons are defined as 0x0 and point to PNG data
27 internal readonly bool HasRawData => Width == 0 && Height == 0;
28 }
29
30 [StructLayout(LayoutKind.Sequential)]
31 internal struct IconDir
32 {
33 internal ushort Reserved;
34 internal ushort Type;
35 internal ushort Count;
36 internal IconDirEntry[] Entries;
37 }
38
39 private const uint lr_defaultcolor = 0x00000000;
40
41 private IconDir iconDir;
42 private byte[] data;
43
44 public static bool TryParse(byte[] data, out IconGroup iconGroup)
45 {
46 try
47 {
48 iconGroup = new IconGroup(data);
49 return true;
50 }
51 catch (Exception)
52 {
53 }
54
55 iconGroup = null;
56 return false;
57 }
58
59 public IconGroup(Stream stream)
60 {
61 if (stream == null || stream.Length == 0)
62 throw new ArgumentException("Missing icon stream.", nameof(stream));
63
64 using (var ms = new MemoryStream())
65 {
66 stream.CopyTo(ms);
67 loadMemoryStream(ms);
68 }
69 }
70
71 public IconGroup(byte[] data)
72 {
73 if (data == null || data.Length == 0)
74 throw new ArgumentException("Missing icon data.", nameof(data));
75
76 using (var ms = new MemoryStream(data))
77 loadMemoryStream(ms);
78 }
79
80 private void loadMemoryStream(MemoryStream stream)
81 {
82 data = stream.ToArray();
83 stream.Position = 0;
84
85 var reader = new BinaryReader(stream);
86 iconDir.Reserved = reader.ReadUInt16();
87 if (iconDir.Reserved != 0)
88 throw new ArgumentException("Invalid icon format.", nameof(stream));
89
90 iconDir.Type = reader.ReadUInt16();
91 if (iconDir.Type != 1)
92 throw new ArgumentException("Invalid icon format.", nameof(stream));
93
94 iconDir.Count = reader.ReadUInt16();
95 iconDir.Entries = new IconDirEntry[iconDir.Count];
96
97 for (int i = 0; i < iconDir.Count; i++)
98 {
99 iconDir.Entries[i] = new IconDirEntry
100 {
101 Width = reader.ReadByte(),
102 Height = reader.ReadByte(),
103 ColourCount = reader.ReadByte(),
104 Reserved = reader.ReadByte(),
105 Planes = reader.ReadUInt16(),
106 BitCount = reader.ReadUInt16(),
107 BytesInResource = reader.ReadUInt32(),
108 ImageOffset = reader.ReadUInt32()
109 };
110 }
111 }
112
113 /// <summary>
114 /// Finds the closest icon entry index that is less than or equal to the requested size and bit depth.
115 /// Icon size takes priority over bit depth since a scaled 32-bit icon will look better than a correctly-sized 8-bit icon.
116 /// </summary>
117 /// <param name="width">The maximum desired width in pixels.</param>
118 /// <param name="height">The maximum desired height in pixels.</param>
119 /// <param name="bpp">The maximum desired bit depth.</param>
120 /// <param name="requireRawData">If true, only icon entries that provide raw PNG data will be considered.</param>
121 /// <returns>The index of the icon in the icon directory, or -1 if a valid icon could not be found.</returns>
122 private int findClosestEntry(int width, int height, int bpp, bool requireRawData) =>
123 Enumerable.Range(0, iconDir.Count)
124 .Where(i => iconDir.Entries[i].Width <= width && iconDir.Entries[i].Height <= height && iconDir.Entries[i].BitCount <= bpp)
125 .Where(i => iconDir.Entries[i].HasRawData || !requireRawData)
126 .OrderByDescending(i => iconDir.Entries[i].Width)
127 .ThenByDescending(i => iconDir.Entries[i].Height)
128 .ThenByDescending(i => iconDir.Entries[i].BitCount)
129 .DefaultIfEmpty(-1).First();
130
131 /// <summary>
132 /// Attempts to create a Windows-specific icon matching the requested dimensions as closely as possible.
133 /// Will return null if a matching size could not be found.
134 /// </summary>
135 /// <param name="width">The maximum desired width in pixels.</param>
136 /// <param name="height">The maximum desired height in pixels.</param>
137 /// <param name="bpp">The maximum desired bit count. Defaults to 32 bit.</param>
138 /// <returns>An <see cref="Icon"/> instance, or null if a valid size could not be found.</returns>
139 /// <exception cref="InvalidOperationException">If the native icon handle could not be created.</exception>
140 [CanBeNull]
141 public Icon CreateIcon(int width, int height, int bpp = 32)
142 {
143 int closest = findClosestEntry(width, height, bpp, false);
144 if (closest < 0)
145 return null;
146
147 var entry = iconDir.Entries[closest];
148 IntPtr hIcon = IntPtr.Zero;
149 var span = new ReadOnlySpan<byte>(data, (int)entry.ImageOffset, (int)entry.BytesInResource);
150
151 if (!entry.HasRawData)
152 hIcon = CreateIconFromResourceEx(span.ToArray(), entry.BytesInResource, true, 0x00030000, width, height, lr_defaultcolor);
153
154 if (hIcon == IntPtr.Zero)
155 throw new InvalidOperationException("Couldn't create native icon handle.");
156
157 return new Icon(hIcon, width, height);
158 }
159
160 /// <summary>
161 /// Attempts to load the raw PNG data from a supported icon, matching the requested dimensions as closely as possible.
162 /// Not all icons in a .ico file are stored as raw PNG data. Will return null if a matching raw PNG could not be found.
163 /// </summary>
164 /// <param name="width">The maximum desired width in pixels.</param>
165 /// <param name="height">The maximum desired height in pixels.</param>
166 /// <param name="bpp">The maximum desired bit count. Defaults to 32 bit.</param>
167 /// <returns>A byte array of raw PNG data, or null if a valid size could not be found.</returns>
168 [CanBeNull]
169 public byte[] LoadRawIcon(int width, int height, int bpp = 32)
170 {
171 int closest = findClosestEntry(width, height, bpp, true);
172 if (closest < 0)
173 return null;
174
175 var entry = iconDir.Entries[closest];
176 var span = new ReadOnlySpan<byte>(data, (int)entry.ImageOffset, (int)entry.BytesInResource);
177
178 return span.ToArray();
179 }
180
181 [DllImport("user32.dll")]
182 private static extern IntPtr CreateIconFromResourceEx(byte[] pbIconBits, uint cbIconBits, bool fIcon, uint dwVersion, int cxDesired, int cyDesired, uint uFlags);
183 }
184}