diff --git a/ico.d b/ico.d index 0bd5d00..4c53866 100644 --- a/ico.d +++ b/ico.d @@ -1,9 +1,11 @@ /++ - Load (and, in the future, save) support for Windows .ico icon files. + Load and save support for Windows .ico icon files. It also supports .cur files, but I've not actually tested them yet. History: Written July 21, 2022 (dub v10.9) + Save support added April 21, 2023 (dub v11.0) + Examples: --- @@ -36,12 +38,30 @@ module arsd.ico; import arsd.png; import arsd.bmp; +/++ + A representation of a cursor image as found in a .cur file. + + History: + Added April 21, 2023 (dub v11.0) ++/ +struct IcoCursor { + MemoryImage image; + int hotspotX; + int hotspotY; +} + +/++ + The header of a .ico or .cur file. Note the alignment is $(I not) correct for slurping the file. ++/ struct IcoHeader { ushort reserved; ushort imageType; // 1 = icon, 2 = cursor ushort numberOfImages; } +/++ + The icon directory entry of a .ico or .cur file. Note the alignment is $(I not) correct for slurping the file. ++/ struct ICONDIRENTRY { ubyte width; // 0 == 256 ubyte height; // 0 == 256 @@ -75,6 +95,62 @@ MemoryImage[] loadIco(string filename) { /// ditto MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { + MemoryImage[] images; + int spot; + loadIcoOrCurFromMemoryCallback( + data, + (int imageType, int numberOfImages) { + if(imageType > 1) + throw new Exception("Not an icon file - invalid image type header"); + + images.length = numberOfImages; + }, + (MemoryImage mi, int hotspotX, int hotspotY) { + images[spot++] = mi; + } + ); + + assert(spot == images.length); + + return images; +} + +/++ + Loads a .cur file. + + History: + Added April 21, 2023 (dub v11.0) ++/ +IcoCursor[] loadCurFromMemory(const(ubyte)[] data) { + IcoCursor[] images; + int spot; + loadIcoOrCurFromMemoryCallback( + data, + (int imageType, int numberOfImages) { + if(imageType != 2) + throw new Exception("Not an cursor file - invalid image type header"); + + images.length = numberOfImages; + }, + (MemoryImage mi, int hotspotX, int hotspotY) { + images[spot++] = IcoCursor(mi, hotspotX, hotspotY); + } + ); + + assert(spot == images.length); + + return images; + +} + +/++ + Load implementation. Api subject to change. ++/ +void loadIcoOrCurFromMemoryCallback( + const(ubyte)[] data, + scope void delegate(int imageType, int numberOfImages) imageTypeChecker, + scope void delegate(MemoryImage mi, int hotspotX, int hotspotY) encounteredImage, +) { IcoHeader header; if(data.length < 6) throw new Exception("Not an icon file - too short to have a header"); @@ -89,8 +165,8 @@ MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { if(header.reserved != 0) throw new Exception("Not an icon file - first bytes incorrect"); - if(header.imageType > 1) - throw new Exception("Not an icon file - invalid image type header"); + + imageTypeChecker(header.imageType, header.numberOfImages); auto originalData = data; data = data[6 .. $]; @@ -133,7 +209,6 @@ MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { foreach(i; 0 .. header.numberOfImages) ides ~= readDirEntry(); - MemoryImage[] images; foreach(image; ides) { if(image.imageDataOffset >= originalData.length) throw new Exception("Invalid icon file - image data offset beyond file size"); @@ -146,12 +221,104 @@ MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { throw new Exception("Invalid image, not long enough to identify"); if(idata[0 .. 4] == "\x89PNG") { - images ~= readPngFromBytes(idata); + encounteredImage(readPngFromBytes(idata), image.planesOrHotspotX, image.bppOrHotspotY); } else { - images ~= readBmp(idata, false, false, true); + encounteredImage(readBmp(idata, false, false, true), image.planesOrHotspotX, image.bppOrHotspotY); } } - - return images; } +/++ + History: + Added April 21, 2023 (dub v11.0) ++/ +void writeIco(string filename, MemoryImage[] images) { + writeIcoOrCur(filename, false, cast(int) images.length, (int idx) { return IcoCursor(images[idx]); }); +} + +/// ditto +void writeCur(string filename, IcoCursor[] images) { + writeIcoOrCur(filename, true, cast(int) images.length, (int idx) { return images[idx]; }); +} + +/++ + Save implementation. Api subject to change. ++/ +void writeIcoOrCur(string filename, bool isCursor, int count, scope IcoCursor delegate(int) getImageAndHotspots) { + IcoHeader header; + header.reserved = 0; + header.imageType = isCursor ? 2 : 1; + if(count > ushort.max) + throw new Exception("too many images for icon file"); + header.numberOfImages = cast(ushort) count; + + enum headerSize = 6; + enum dirEntrySize = 16; + + int dataFilePos = headerSize + dirEntrySize * cast(int) count; + + ubyte[][] pngs; + ICONDIRENTRY[] dirEntries; + dirEntries.length = count; + pngs.length = count; + foreach(idx, ref entry; dirEntries) { + auto image = getImageAndHotspots(cast(int) idx); + if(image.image.width > 256 || image.image.height > 256) + throw new Exception("image too big for icon file"); + entry.width = image.image.width == 256 ? 0 : cast(ubyte) image.image.width; + entry.height = image.image.height == 256 ? 0 : cast(ubyte) image.image.height; + + entry.planesOrHotspotX = isCursor ? cast(ushort) image.hotspotX : 0; + entry.bppOrHotspotY = isCursor ? cast(ushort) image.hotspotY : 0; + + auto png = writePngToArray(image.image); + + entry.imageDataSize = cast(uint) png.length; + entry.imageDataOffset = dataFilePos; + dataFilePos += entry.imageDataSize; + + pngs[idx] = png; + } + + ubyte[] data; + data.length = dataFilePos; + int pos = 0; + + data[pos++] = (header.reserved >> 0) & 0xff; + data[pos++] = (header.reserved >> 8) & 0xff; + data[pos++] = (header.imageType >> 0) & 0xff; + data[pos++] = (header.imageType >> 8) & 0xff; + data[pos++] = (header.numberOfImages >> 0) & 0xff; + data[pos++] = (header.numberOfImages >> 8) & 0xff; + + foreach(entry; dirEntries) { + data[pos++] = (entry.width >> 0) & 0xff; + data[pos++] = (entry.height >> 0) & 0xff; + data[pos++] = (entry.numColors >> 0) & 0xff; + data[pos++] = (entry.reserved >> 0) & 0xff; + data[pos++] = (entry.planesOrHotspotX >> 0) & 0xff; + data[pos++] = (entry.planesOrHotspotX >> 8) & 0xff; + data[pos++] = (entry.bppOrHotspotY >> 0) & 0xff; + data[pos++] = (entry.bppOrHotspotY >> 8) & 0xff; + + data[pos++] = (entry.imageDataSize >> 0) & 0xff; + data[pos++] = (entry.imageDataSize >> 8) & 0xff; + data[pos++] = (entry.imageDataSize >> 16) & 0xff; + data[pos++] = (entry.imageDataSize >> 24) & 0xff; + + data[pos++] = (entry.imageDataOffset >> 0) & 0xff; + data[pos++] = (entry.imageDataOffset >> 8) & 0xff; + data[pos++] = (entry.imageDataOffset >> 16) & 0xff; + data[pos++] = (entry.imageDataOffset >> 24) & 0xff; + } + + foreach(png; pngs) { + data[pos .. pos + png.length] = png[]; + pos += png.length; + } + + assert(pos == dataFilePos); + + import std.file; + std.file.write(filename, data); +}