A game framework written with osu! in mind.
at master 184 lines 7.7 kB view raw
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}