1 /++ 2 Load (and, in the future, save) support for Windows .ico icon files. 3 4 History: 5 Written July 21, 2022 (dub v10.9) 6 7 Examples: 8 9 --- 10 void main() { 11 auto thing = loadIco("test.ico"); 12 import std.stdio; 13 writeln(thing.length); // tell how many things it found 14 15 /+ // just to display one 16 import arsd.simpledisplay; 17 auto img = new SimpleWindow(thing[0].width, thing[0].height); 18 { 19 auto paint = img.draw(); 20 paint.drawImage(Point(0, 0), Image.fromMemoryImage(thing[0])); 21 } 22 23 img.eventLoop(0); 24 +/ 25 26 // and this converts all its versions 27 import arsd.png; 28 import std.format; 29 foreach(idx, t; thing) 30 writePng(format("test-converted-%d-%dx%d.png", idx, t.width, t.height), t); 31 } 32 --- 33 +/ 34 module arsd.ico; 35 36 import arsd.png; 37 import arsd.bmp; 38 39 struct IcoHeader { 40 ushort reserved; 41 ushort imageType; // 1 = icon, 2 = cursor 42 ushort numberOfImages; 43 } 44 45 struct ICONDIRENTRY { 46 ubyte width; // 0 == 256 47 ubyte height; // 0 == 256 48 ubyte numColors; // 0 == no palette 49 ubyte reserved; 50 ushort planesOrHotspotX; 51 ushort bppOrHotspotY; 52 uint imageDataSize; 53 uint imageDataOffset; // from beginning of file 54 } 55 56 // the file goes header, then array of dir entries, then images 57 /* 58 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. 59 60 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. 61 62 from wikipedia 63 */ 64 65 /++ 66 Loads a ico file off the given file or from the given memory block. 67 68 Returns: 69 Array of individual images found in the icon file. These are typically different size representations of the same icon. 70 +/ 71 MemoryImage[] loadIco(string filename) { 72 import std.file; 73 return loadIcoFromMemory(cast(const(ubyte)[]) std.file.read(filename)); 74 } 75 76 /// ditto 77 MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { 78 IcoHeader header; 79 if(data.length < 6) 80 throw new Exception("Not an icon file - too short to have a header"); 81 header.reserved |= data[0]; 82 header.reserved |= data[1] << 8; 83 84 header.imageType |= data[2]; 85 header.imageType |= data[3] << 8; 86 87 header.numberOfImages |= data[4]; 88 header.numberOfImages |= data[5] << 8; 89 90 if(header.reserved != 0) 91 throw new Exception("Not an icon file - first bytes incorrect"); 92 if(header.imageType > 1) 93 throw new Exception("Not an icon file - invalid image type header"); 94 95 auto originalData = data; 96 data = data[6 .. $]; 97 98 ubyte nextByte() { 99 if(data.length == 0) 100 throw new Exception("Invalid icon file, it too short"); 101 ubyte b = data[0]; 102 data = data[1 .. $]; 103 return b; 104 } 105 106 ICONDIRENTRY readDirEntry() { 107 ICONDIRENTRY ide; 108 ide.width = nextByte(); 109 ide.height = nextByte(); 110 ide.numColors = nextByte(); 111 ide.reserved = nextByte(); 112 113 ide.planesOrHotspotX |= nextByte(); 114 ide.planesOrHotspotX |= nextByte() << 8; 115 116 ide.bppOrHotspotY |= nextByte(); 117 ide.bppOrHotspotY |= nextByte() << 8; 118 119 ide.imageDataSize |= nextByte() << 0; 120 ide.imageDataSize |= nextByte() << 8; 121 ide.imageDataSize |= nextByte() << 16; 122 ide.imageDataSize |= nextByte() << 24; 123 124 ide.imageDataOffset |= nextByte() << 0; 125 ide.imageDataOffset |= nextByte() << 8; 126 ide.imageDataOffset |= nextByte() << 16; 127 ide.imageDataOffset |= nextByte() << 24; 128 129 return ide; 130 } 131 132 ICONDIRENTRY[] ides; 133 foreach(i; 0 .. header.numberOfImages) 134 ides ~= readDirEntry(); 135 136 MemoryImage[] images; 137 foreach(image; ides) { 138 if(image.imageDataOffset >= originalData.length) 139 throw new Exception("Invalid icon file - image data offset beyond file size"); 140 if(image.imageDataOffset + image.imageDataSize > originalData.length) 141 throw new Exception("Invalid icon file - image data extends beyond file size"); 142 143 auto idata = originalData[image.imageDataOffset .. image.imageDataOffset + image.imageDataSize]; 144 145 if(idata.length < 4) 146 throw new Exception("Invalid image, not long enough to identify"); 147 148 if(idata[0 .. 4] == "\x89PNG") { 149 images ~= readPngFromBytes(idata); 150 } else { 151 images ~= readBmp(idata, false, false, true); 152 } 153 } 154 155 return images; 156 } 157