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 }