1 /++ 2 Load and save support for Windows .ico icon files. It also supports .cur files, but I've not actually tested them yet. 3 4 History: 5 Written July 21, 2022 (dub v10.9) 6 7 Save support added April 21, 2023 (dub v11.0) 8 9 Examples: 10 11 --- 12 void main() { 13 auto thing = loadIco("test.ico"); 14 import std.stdio; 15 writeln(thing.length); // tell how many things it found 16 17 /+ // just to display one 18 import arsd.simpledisplay; 19 auto img = new SimpleWindow(thing[0].width, thing[0].height); 20 { 21 auto paint = img.draw(); 22 paint.drawImage(Point(0, 0), Image.fromMemoryImage(thing[0])); 23 } 24 25 img.eventLoop(0); 26 +/ 27 28 // and this converts all its versions 29 import arsd.png; 30 import std.format; 31 foreach(idx, t; thing) 32 writePng(format("test-converted-%d-%dx%d.png", idx, t.width, t.height), t); 33 } 34 --- 35 +/ 36 module arsd.ico; 37 38 import arsd.png; 39 import arsd.bmp; 40 41 /++ 42 A representation of a cursor image as found in a .cur file. 43 44 History: 45 Added April 21, 2023 (dub v11.0) 46 +/ 47 struct IcoCursor { 48 MemoryImage image; 49 int hotspotX; 50 int hotspotY; 51 } 52 53 /++ 54 The header of a .ico or .cur file. Note the alignment is $(I not) correct for slurping the file. 55 +/ 56 struct IcoHeader { 57 ushort reserved; 58 ushort imageType; // 1 = icon, 2 = cursor 59 ushort numberOfImages; 60 } 61 62 /++ 63 The icon directory entry of a .ico or .cur file. Note the alignment is $(I not) correct for slurping the file. 64 +/ 65 struct ICONDIRENTRY { 66 ubyte width; // 0 == 256 67 ubyte height; // 0 == 256 68 ubyte numColors; // 0 == no palette 69 ubyte reserved; 70 ushort planesOrHotspotX; 71 ushort bppOrHotspotY; 72 uint imageDataSize; 73 uint imageDataOffset; // from beginning of file 74 } 75 76 // the file goes header, then array of dir entries, then images 77 /* 78 Recall that if an image is stored in BMP format, it must exclude the opening BITMAPFILEHEADER structure, whereas if it is stored in PNG format, it must be stored in its entirety. 79 80 Note that the height of the BMP image must be twice the height declared in the image directory. The second half of the bitmap should be an AND mask for the existing screen pixels, with the output pixels given by the formula Output = (Existing AND Mask) XOR Image. Set the mask to be zero everywhere for a clean overwrite. 81 82 from wikipedia 83 */ 84 85 /++ 86 Loads a ico file off the given file or from the given memory block. 87 88 Returns: 89 Array of individual images found in the icon file. These are typically different size representations of the same icon. 90 +/ 91 MemoryImage[] loadIco(string filename) { 92 import std.file; 93 return loadIcoFromMemory(cast(const(ubyte)[]) std.file.read(filename)); 94 } 95 96 /// ditto 97 MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { 98 MemoryImage[] images; 99 int spot; 100 loadIcoOrCurFromMemoryCallback( 101 data, 102 (int imageType, int numberOfImages) { 103 if(imageType > 1) 104 throw new Exception("Not an icon file - invalid image type header"); 105 106 images.length = numberOfImages; 107 }, 108 (MemoryImage mi, int hotspotX, int hotspotY) { 109 images[spot++] = mi; 110 } 111 ); 112 113 assert(spot == images.length); 114 115 return images; 116 } 117 118 /++ 119 Loads a .cur file. 120 121 History: 122 Added April 21, 2023 (dub v11.0) 123 +/ 124 IcoCursor[] loadCurFromMemory(const(ubyte)[] data) { 125 IcoCursor[] images; 126 int spot; 127 loadIcoOrCurFromMemoryCallback( 128 data, 129 (int imageType, int numberOfImages) { 130 if(imageType != 2) 131 throw new Exception("Not an cursor file - invalid image type header"); 132 133 images.length = numberOfImages; 134 }, 135 (MemoryImage mi, int hotspotX, int hotspotY) { 136 images[spot++] = IcoCursor(mi, hotspotX, hotspotY); 137 } 138 ); 139 140 assert(spot == images.length); 141 142 return images; 143 144 } 145 146 /++ 147 Load implementation. Api subject to change. 148 +/ 149 void loadIcoOrCurFromMemoryCallback( 150 const(ubyte)[] data, 151 scope void delegate(int imageType, int numberOfImages) imageTypeChecker, 152 scope void delegate(MemoryImage mi, int hotspotX, int hotspotY) encounteredImage, 153 ) { 154 IcoHeader header; 155 if(data.length < 6) 156 throw new Exception("Not an icon file - too short to have a header"); 157 header.reserved |= data[0]; 158 header.reserved |= data[1] << 8; 159 160 header.imageType |= data[2]; 161 header.imageType |= data[3] << 8; 162 163 header.numberOfImages |= data[4]; 164 header.numberOfImages |= data[5] << 8; 165 166 if(header.reserved != 0) 167 throw new Exception("Not an icon file - first bytes incorrect"); 168 169 imageTypeChecker(header.imageType, header.numberOfImages); 170 171 auto originalData = data; 172 data = data[6 .. $]; 173 174 ubyte nextByte() { 175 if(data.length == 0) 176 throw new Exception("Invalid icon file, it too short"); 177 ubyte b = data[0]; 178 data = data[1 .. $]; 179 return b; 180 } 181 182 ICONDIRENTRY readDirEntry() { 183 ICONDIRENTRY ide; 184 ide.width = nextByte(); 185 ide.height = nextByte(); 186 ide.numColors = nextByte(); 187 ide.reserved = nextByte(); 188 189 ide.planesOrHotspotX |= nextByte(); 190 ide.planesOrHotspotX |= nextByte() << 8; 191 192 ide.bppOrHotspotY |= nextByte(); 193 ide.bppOrHotspotY |= nextByte() << 8; 194 195 ide.imageDataSize |= nextByte() << 0; 196 ide.imageDataSize |= nextByte() << 8; 197 ide.imageDataSize |= nextByte() << 16; 198 ide.imageDataSize |= nextByte() << 24; 199 200 ide.imageDataOffset |= nextByte() << 0; 201 ide.imageDataOffset |= nextByte() << 8; 202 ide.imageDataOffset |= nextByte() << 16; 203 ide.imageDataOffset |= nextByte() << 24; 204 205 return ide; 206 } 207 208 ICONDIRENTRY[] ides; 209 foreach(i; 0 .. header.numberOfImages) 210 ides ~= readDirEntry(); 211 212 foreach(image; ides) { 213 if(image.imageDataOffset >= originalData.length) 214 throw new Exception("Invalid icon file - image data offset beyond file size"); 215 if(image.imageDataOffset + image.imageDataSize > originalData.length) 216 throw new Exception("Invalid icon file - image data extends beyond file size"); 217 218 auto idata = originalData[image.imageDataOffset .. image.imageDataOffset + image.imageDataSize]; 219 220 if(idata.length < 4) 221 throw new Exception("Invalid image, not long enough to identify"); 222 223 if(idata[0 .. 4] == "\x89PNG") { 224 encounteredImage(readPngFromBytes(idata), image.planesOrHotspotX, image.bppOrHotspotY); 225 } else { 226 encounteredImage(readBmp(idata, false, false, true), image.planesOrHotspotX, image.bppOrHotspotY); 227 } 228 } 229 } 230 231 /++ 232 History: 233 Added April 21, 2023 (dub v11.0) 234 +/ 235 void writeIco(string filename, MemoryImage[] images) { 236 writeIcoOrCur(filename, false, cast(int) images.length, (int idx) { return IcoCursor(images[idx]); }); 237 } 238 239 /// ditto 240 void writeCur(string filename, IcoCursor[] images) { 241 writeIcoOrCur(filename, true, cast(int) images.length, (int idx) { return images[idx]; }); 242 } 243 244 /++ 245 Save implementation. Api subject to change. 246 +/ 247 void writeIcoOrCur(string filename, bool isCursor, int count, scope IcoCursor delegate(int) getImageAndHotspots) { 248 IcoHeader header; 249 header.reserved = 0; 250 header.imageType = isCursor ? 2 : 1; 251 if(count > ushort.max) 252 throw new Exception("too many images for icon file"); 253 header.numberOfImages = cast(ushort) count; 254 255 enum headerSize = 6; 256 enum dirEntrySize = 16; 257 258 int dataFilePos = headerSize + dirEntrySize * cast(int) count; 259 260 ubyte[][] pngs; 261 ICONDIRENTRY[] dirEntries; 262 dirEntries.length = count; 263 pngs.length = count; 264 foreach(idx, ref entry; dirEntries) { 265 auto image = getImageAndHotspots(cast(int) idx); 266 if(image.image.width > 256 || image.image.height > 256) 267 throw new Exception("image too big for icon file"); 268 entry.width = image.image.width == 256 ? 0 : cast(ubyte) image.image.width; 269 entry.height = image.image.height == 256 ? 0 : cast(ubyte) image.image.height; 270 271 entry.planesOrHotspotX = isCursor ? cast(ushort) image.hotspotX : 0; 272 entry.bppOrHotspotY = isCursor ? cast(ushort) image.hotspotY : 0; 273 274 auto png = writePngToArray(image.image); 275 276 entry.imageDataSize = cast(uint) png.length; 277 entry.imageDataOffset = dataFilePos; 278 dataFilePos += entry.imageDataSize; 279 280 pngs[idx] = png; 281 } 282 283 ubyte[] data; 284 data.length = dataFilePos; 285 int pos = 0; 286 287 data[pos++] = (header.reserved >> 0) & 0xff; 288 data[pos++] = (header.reserved >> 8) & 0xff; 289 data[pos++] = (header.imageType >> 0) & 0xff; 290 data[pos++] = (header.imageType >> 8) & 0xff; 291 data[pos++] = (header.numberOfImages >> 0) & 0xff; 292 data[pos++] = (header.numberOfImages >> 8) & 0xff; 293 294 foreach(entry; dirEntries) { 295 data[pos++] = (entry.width >> 0) & 0xff; 296 data[pos++] = (entry.height >> 0) & 0xff; 297 data[pos++] = (entry.numColors >> 0) & 0xff; 298 data[pos++] = (entry.reserved >> 0) & 0xff; 299 data[pos++] = (entry.planesOrHotspotX >> 0) & 0xff; 300 data[pos++] = (entry.planesOrHotspotX >> 8) & 0xff; 301 data[pos++] = (entry.bppOrHotspotY >> 0) & 0xff; 302 data[pos++] = (entry.bppOrHotspotY >> 8) & 0xff; 303 304 data[pos++] = (entry.imageDataSize >> 0) & 0xff; 305 data[pos++] = (entry.imageDataSize >> 8) & 0xff; 306 data[pos++] = (entry.imageDataSize >> 16) & 0xff; 307 data[pos++] = (entry.imageDataSize >> 24) & 0xff; 308 309 data[pos++] = (entry.imageDataOffset >> 0) & 0xff; 310 data[pos++] = (entry.imageDataOffset >> 8) & 0xff; 311 data[pos++] = (entry.imageDataOffset >> 16) & 0xff; 312 data[pos++] = (entry.imageDataOffset >> 24) & 0xff; 313 } 314 315 foreach(png; pngs) { 316 data[pos .. pos + png.length] = png[]; 317 pos += png.length; 318 } 319 320 assert(pos == dataFilePos); 321 322 import std.file; 323 std.file.write(filename, data); 324 }