diff --git a/bmp.d b/bmp.d index 831a539..c5aaf23 100644 --- a/bmp.d +++ b/bmp.d @@ -35,7 +35,7 @@ MemoryImage readBmp(string filename) { are a little-endian uint giving the file size. You might slice only to that, or you could slice right to `int.max` and trust the library to bounds check for you based on data integrity checks. +/ -MemoryImage readBmp(in ubyte[] data, bool lookForFileHeader = true, bool hackAround64BitLongs = false) { +MemoryImage readBmp(in ubyte[] data, bool lookForFileHeader = true, bool hackAround64BitLongs = false, bool hasAndMask = false) { const(ubyte)[] current = data; void specialFread(void* tgt, size_t size) { while(size) { @@ -47,7 +47,7 @@ MemoryImage readBmp(in ubyte[] data, bool lookForFileHeader = true, bool hackAro } } - return readBmpIndirect(&specialFread, lookForFileHeader, hackAround64BitLongs); + return readBmpIndirect(&specialFread, lookForFileHeader, hackAround64BitLongs, hasAndMask); } /++ @@ -57,8 +57,10 @@ MemoryImage readBmp(in ubyte[] data, bool lookForFileHeader = true, bool hackAro The `lookForFileHeader` param was added in July 2020. The `hackAround64BitLongs` param was added December 21, 2020. You should probably never use this unless you know for sure you have a file corrupted in this specific way. View the source to see a comment inside the file to describe it a bit more. + + The `hasAndMask` param was added July 21, 2022. This is set to true if it is a bitmap from a .ico file or similar, where the top half of the file (by height) is the xor mask, then the bottom half is the and mask. +/ -MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookForFileHeader = true, bool hackAround64BitLongs = false) { +MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookForFileHeader = true, bool hackAround64BitLongs = false, bool hasAndMask = false) { uint read4() { uint what; fread(&what, 4); return what; } uint readLONG() { auto le = read4(); @@ -137,6 +139,12 @@ MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookF } height = (rdheight < 0 ? -rdheight : rdheight); + + if(hasAndMask) { + version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("has and mask so height slashed %d\n", height / 2); } + height = height / 2; + } + rdheight = (rdheight < 0 ? 1 : -1); // so we can use it as delta (note the inverted sign) version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size: %dx%d\n", cast(int)width, cast(int) height); } @@ -336,6 +344,7 @@ MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookF int offsetStart = width * height * 4; int bytesPerPixel = 4; for(int y = height; y > 0; y--) { + //version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" true color image: %d\n", y); } offsetStart -= width * bytesPerPixel; int offset = offsetStart; int b = 0; @@ -410,6 +419,39 @@ MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookF read1(); // pad until divisible by four } + if(hasAndMask) { + // the and mask is always 1bpp and i want to translate it into transparent pixels + + int offset = 0; + for(int y = height; y > 0; y--) { + int read; + for(int x = 0; x < width; x++) { + auto b = read1(); + read++; + foreach(lol; 0 .. 8) { + bool transparent = cast(bool) ((b & (1 << lol)) >> (7 - lol)); + // FIXME: im just keeping the alpha channel from the bmp here but this is arguably wrong + //img.imageData.bytes[offset + 3] = transparent ? 0 : 255; + //import std.stdio; write(transparent ? "o":"x"); + offset += 4; + x++; + } + x--; // we do this once too many times in the loop + } + while(read % 4) { + read1(); + read++; + } + //import std.stdio; writeln(""); + } + + /+ + this the algorithm btw + keep.imageData.bytes[] &= tci.imageData.bytes[andOffset .. $]; + keep.imageData.bytes[] ^= tci.imageData.bytes[0 .. andOffset]; + +/ + } + return img; } diff --git a/ico.d b/ico.d new file mode 100644 index 0000000..24fb753 --- /dev/null +++ b/ico.d @@ -0,0 +1,157 @@ +/++ + Load (and, in the future, save) support for Windows .ico icon files. + + History: + Written July 21, 2022 (dub v10.9) + + Examples: + + --- + void main() { + auto thing = loadIco("test.ico"); + import std.stdio; + writeln(thing.length); // tell how many things it found + + /+ // just to display one + import arsd.simpledisplay; + auto img = new SimpleWindow(thing[0].width, thing[0].height); + { + auto paint = img.draw(); + paint.drawImage(Point(0, 0), Image.fromMemoryImage(thing[0])); + } + + img.eventLoop(0); + +/ + + // and this converts all its versions + import arsd.png; + import std.format; + foreach(idx, t; thing) + writePng(format("test-converted-%d-%dx%d.png", idx, t.width, t.height), t); + } + --- ++/ +module arsd.ico; + +import arsd.png; +import arsd.bmp; + +struct IcoHeader { + ushort reserved; + ushort imageType; // 1 = icon, 2 = cursor + ushort numberOfImages; +} + +struct ICONDIRENTRY { + ubyte width; // 0 == 256 + ubyte height; // 0 == 256 + ubyte numColors; // 0 == no palette + ubyte reserved; + ushort planesOrHotspotX; + ushort bppOrHotspotY; + uint imageDataSize; + uint imageDataOffset; // from beginning of file +} + +// the file goes header, then array of dir entries, then images +/* +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. + +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. + +from wikipedia +*/ + +/++ + Loads a ico file off the given file or from the given memory block. + + Returns: + Array of individual images found in the icon file. These are typically different size representations of the same icon. ++/ +MemoryImage[] loadIco(string filename) { + import std.file; + return loadIcoFromMemory(cast(const(ubyte)[]) std.file.read(filename)); +} + +/// ditto +MemoryImage[] loadIcoFromMemory(const(ubyte)[] data) { + IcoHeader header; + if(data.length < 6) + throw new Exception("Not an icon file - too short to have a header"); + header.reserved |= data[0]; + header.reserved |= data[1] << 8; + + header.imageType |= data[2]; + header.imageType |= data[3] << 8; + + header.numberOfImages |= data[4]; + header.numberOfImages |= data[5] << 8; + + 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"); + + auto originalData = data; + data = data[6 .. $]; + + ubyte nextByte() { + if(data.length == 0) + throw new Exception("Invalid icon file, it too short"); + ubyte b = data[0]; + data = data[1 .. $]; + return b; + } + + ICONDIRENTRY readDirEntry() { + ICONDIRENTRY ide; + ide.width = nextByte(); + ide.height = nextByte(); + ide.numColors = nextByte(); + ide.reserved = nextByte(); + + ide.planesOrHotspotX |= nextByte(); + ide.planesOrHotspotX |= nextByte() << 8; + + ide.bppOrHotspotY |= nextByte(); + ide.bppOrHotspotY |= nextByte() << 8; + + ide.imageDataSize |= nextByte() << 0; + ide.imageDataSize |= nextByte() << 8; + ide.imageDataSize |= nextByte() << 16; + ide.imageDataSize |= nextByte() << 24; + + ide.imageDataOffset |= nextByte() << 0; + ide.imageDataOffset |= nextByte() << 8; + ide.imageDataOffset |= nextByte() << 16; + ide.imageDataOffset |= nextByte() << 24; + + return ide; + } + + ICONDIRENTRY[] ides; + 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"); + if(image.imageDataOffset + image.imageDataSize > originalData.length) + throw new Exception("Invalid icon file - image data extends beyond file size"); + + auto idata = originalData[image.imageDataOffset .. image.imageDataOffset + image.imageDataSize]; + + if(idata.length < 4) + throw new Exception("Invalid image, not long enough to identify"); + + if(idata[0 .. 4] == "\x89PNG") { + images ~= readPngFromBytes(idata); + } else { + images ~= readBmp(idata, false, false, true); + } + } + + return images; +} +