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