mirror of https://github.com/adamdruppe/arsd.git
686 lines
21 KiB
D
686 lines
21 KiB
D
/++
|
|
Basic .bmp file format implementation for [arsd.color.MemoryImage].
|
|
Compare with [arsd.png] basic functionality.
|
|
+/
|
|
module arsd.bmp;
|
|
|
|
import arsd.color;
|
|
|
|
//version = arsd_debug_bitmap_loader;
|
|
|
|
|
|
/// Reads a .bmp file from the given `filename`
|
|
MemoryImage readBmp(string filename) {
|
|
import core.stdc.stdio;
|
|
|
|
FILE* fp = fopen((filename ~ "\0").ptr, "rb".ptr);
|
|
if(fp is null)
|
|
throw new Exception("can't open save file");
|
|
scope(exit) fclose(fp);
|
|
|
|
void specialFread(void* tgt, size_t size) {
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("ofs: 0x%08x\n", cast(uint)ftell(fp)); }
|
|
fread(tgt, size, 1, fp);
|
|
}
|
|
|
|
return readBmpIndirect(&specialFread);
|
|
}
|
|
|
|
/++
|
|
Reads a bitmap out of an in-memory array of data. For example, from the data returned from [std.file.read].
|
|
|
|
It forwards the arguments to [readBmpIndirect], so see that for more details.
|
|
|
|
If you are given a raw pointer to some data, you might just slice it: bytes 2-6 of the file header (if present)
|
|
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, bool hasAndMask = false) {
|
|
int position;
|
|
const(ubyte)[] current = data;
|
|
void specialFread(void* tgt, size_t size) {
|
|
while(size) {
|
|
if (current.length == 0) throw new Exception("out of bmp data"); // it's not *that* fatal, so don't throw RangeError
|
|
//import std.stdio; writefln("%04x", position);
|
|
*cast(ubyte*)(tgt) = current[0];
|
|
current = current[1 .. $];
|
|
position++;
|
|
tgt++;
|
|
size--;
|
|
}
|
|
}
|
|
|
|
return readBmpIndirect(&specialFread, lookForFileHeader, hackAround64BitLongs, hasAndMask);
|
|
}
|
|
|
|
/++
|
|
Reads using a delegate to read instead of assuming a direct file. View the source of `readBmp`'s overloads for fairly simple examples of how you can use it
|
|
|
|
History:
|
|
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, bool hasAndMask = false) {
|
|
uint read4() { uint what; fread(&what, 4); return what; }
|
|
uint readLONG() {
|
|
auto le = read4();
|
|
/++
|
|
A user on discord encountered a file in the wild that wouldn't load
|
|
by any other bmp viewer. After looking at the raw bytes, it appeared it
|
|
wrote out the LONG fields on the bitmap info header as 64 bit values when
|
|
they are supposed to always be 32 bit values. This hack gives a chance to work
|
|
around that and load the file anyway.
|
|
+/
|
|
if(hackAround64BitLongs)
|
|
if(read4() != 0)
|
|
throw new Exception("hackAround64BitLongs is true, but the file doesn't appear to use 64 bit longs");
|
|
return le;
|
|
}
|
|
ushort read2(){ ushort what; fread(&what, 2); return what; }
|
|
|
|
bool headerRead = false;
|
|
int hackCounter;
|
|
|
|
ubyte read1() {
|
|
if(hackAround64BitLongs && headerRead && hackCounter < 16) {
|
|
hackCounter++;
|
|
return 0;
|
|
}
|
|
ubyte what;
|
|
fread(&what, 1);
|
|
return what;
|
|
}
|
|
|
|
void require1(ubyte t, size_t line = __LINE__) {
|
|
if(read1() != t)
|
|
throw new Exception("didn't get expected byte value", __FILE__, line);
|
|
}
|
|
void require2(ushort t) {
|
|
auto got = read2();
|
|
if(got != t) {
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("expected: %d, got %d\n", cast(int) t, cast(int) got); }
|
|
throw new Exception("didn't get expected short value");
|
|
}
|
|
}
|
|
void require4(uint t, size_t line = __LINE__) {
|
|
auto got = read4();
|
|
//import std.conv;
|
|
if(got != t)
|
|
throw new Exception("didn't get expected int value " /*~ to!string(got)*/, __FILE__, line);
|
|
}
|
|
|
|
if(lookForFileHeader) {
|
|
require1('B');
|
|
require1('M');
|
|
|
|
auto fileSize = read4(); // size of file in bytes
|
|
require2(0); // reserved
|
|
require2(0); // reserved
|
|
|
|
auto offsetToBits = read4();
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("pixel data offset: 0x%08x\n", cast(uint)offsetToBits); }
|
|
}
|
|
|
|
auto sizeOfBitmapInfoHeader = read4();
|
|
if (sizeOfBitmapInfoHeader < 12) throw new Exception("invalid bitmap header size");
|
|
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size of bitmap info header: %d\n", cast(uint)sizeOfBitmapInfoHeader); }
|
|
|
|
int width, height, rdheight;
|
|
|
|
if (sizeOfBitmapInfoHeader == 12) {
|
|
width = read2();
|
|
rdheight = cast(short)read2();
|
|
} else {
|
|
if (sizeOfBitmapInfoHeader < 16) throw new Exception("invalid bitmap header size");
|
|
sizeOfBitmapInfoHeader -= 4; // hack!
|
|
width = readLONG();
|
|
rdheight = cast(int)readLONG();
|
|
}
|
|
|
|
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); }
|
|
if (width < 1 || height < 1) throw new Exception("invalid bitmap dimensions");
|
|
|
|
require2(1); // planes
|
|
|
|
auto bitsPerPixel = read2();
|
|
switch (bitsPerPixel) {
|
|
case 1: case 2: case 4: case 8: case 16: case 24: case 32: break;
|
|
default: throw new Exception("invalid bitmap depth");
|
|
}
|
|
|
|
/*
|
|
0 = BI_RGB
|
|
1 = BI_RLE8 RLE 8-bit/pixel Can be used only with 8-bit/pixel bitmaps
|
|
2 = BI_RLE4 RLE 4-bit/pixel Can be used only with 4-bit/pixel bitmaps
|
|
3 = BI_BITFIELDS
|
|
*/
|
|
uint compression = 0;
|
|
uint sizeOfUncompressedData = 0;
|
|
uint xPixelsPerMeter = 0;
|
|
uint yPixelsPerMeter = 0;
|
|
uint colorsUsed = 0;
|
|
uint colorsImportant = 0;
|
|
|
|
sizeOfBitmapInfoHeader -= 12;
|
|
if (sizeOfBitmapInfoHeader > 0) {
|
|
if (sizeOfBitmapInfoHeader < 6*4) throw new Exception("invalid bitmap header size");
|
|
sizeOfBitmapInfoHeader -= 6*4;
|
|
compression = read4();
|
|
sizeOfUncompressedData = read4();
|
|
xPixelsPerMeter = readLONG();
|
|
yPixelsPerMeter = readLONG();
|
|
colorsUsed = read4();
|
|
colorsImportant = read4();
|
|
}
|
|
|
|
if (compression > 3) throw new Exception("invalid bitmap compression");
|
|
if (compression == 1 && bitsPerPixel != 8) throw new Exception("invalid bitmap compression");
|
|
if (compression == 2 && bitsPerPixel != 4) throw new Exception("invalid bitmap compression");
|
|
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("compression: %u; bpp: %u\n", compression, cast(uint)bitsPerPixel); }
|
|
|
|
uint redMask;
|
|
uint greenMask;
|
|
uint blueMask;
|
|
uint alphaMask;
|
|
if (compression == 3) {
|
|
if (sizeOfBitmapInfoHeader < 4*4) throw new Exception("invalid bitmap compression");
|
|
sizeOfBitmapInfoHeader -= 4*4;
|
|
redMask = read4();
|
|
greenMask = read4();
|
|
blueMask = read4();
|
|
alphaMask = read4();
|
|
}
|
|
// FIXME: we could probably handle RLE4 as well
|
|
|
|
// I don't know about the rest of the header, so I'm just skipping it.
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("header bytes left: %u\n", cast(uint)sizeOfBitmapInfoHeader); }
|
|
foreach (skip; 0..sizeOfBitmapInfoHeader) read1();
|
|
|
|
headerRead = true;
|
|
|
|
|
|
|
|
// the dg returns the change in offset
|
|
void processAndMask(scope int delegate(int x, int y, bool transparent) apply) {
|
|
try {
|
|
// the and mask is always 1bpp and i want to translate it into transparent pixels
|
|
|
|
for(int y = (height - 1); y >= 0; y--) {
|
|
//version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" reading and mask %d\n", y); }
|
|
int read;
|
|
for(int x = 0; x < width; x++) {
|
|
const b = read1();
|
|
//import std.stdio; writefln("%02x", b);
|
|
read++;
|
|
foreach_reverse(lol; 0 .. 8) {
|
|
bool transparent = !!((b & (1 << lol)));
|
|
version(arsd_debug_bitmap_loader) { import std.stdio; write(transparent ? "o":"x"); }
|
|
apply(x, y, transparent);
|
|
|
|
x++;
|
|
if(x >= width)
|
|
break;
|
|
}
|
|
x--; // we do this once too many times in the loop
|
|
}
|
|
while(read % 4) {
|
|
read1();
|
|
read++;
|
|
}
|
|
version(arsd_debug_bitmap_loader) {import std.stdio; writeln(""); }
|
|
}
|
|
|
|
/+
|
|
this the algorithm btw
|
|
keep.imageData.bytes[] &= tci.imageData.bytes[andOffset .. $];
|
|
keep.imageData.bytes[] ^= tci.imageData.bytes[0 .. andOffset];
|
|
+/
|
|
} catch(Exception e) {
|
|
// discard; the and mask is optional in practice since using all 0's
|
|
// gives a result and some files in the wild deliberately truncate the
|
|
// file (though they aren't supposed to....) expecting readers to do this.
|
|
version(arsd_debug_bitmap_loader) { import std.stdio; writeln(e); }
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if(bitsPerPixel <= 8) {
|
|
// indexed image
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("colorsUsed=%u; colorsImportant=%u\n", colorsUsed, colorsImportant); }
|
|
if (colorsUsed == 0 || colorsUsed > (1 << bitsPerPixel)) colorsUsed = (1 << bitsPerPixel);
|
|
auto img = new IndexedImage(width, height);
|
|
img.palette.reserve(1 << bitsPerPixel);
|
|
|
|
foreach(idx; 0 .. /*(1 << bitsPerPixel)*/colorsUsed) {
|
|
auto b = read1();
|
|
auto g = read1();
|
|
auto r = read1();
|
|
auto reserved = read1();
|
|
|
|
img.palette ~= Color(r, g, b);
|
|
}
|
|
while (img.palette.length < (1 << bitsPerPixel)) img.palette ~= Color.transparent;
|
|
|
|
// and the data
|
|
int bytesPerPixel = 1;
|
|
auto offsetStart = (rdheight > 0 ? 0 : width * height * bytesPerPixel);
|
|
int bytesRead = 0;
|
|
|
|
if (compression == 1) {
|
|
// this is complicated
|
|
assert(bitsPerPixel == 8); // always
|
|
int x = 0, y = (rdheight > 0 ? 0 : height-1);
|
|
void setpix (int v) {
|
|
if (x >= 0 && y >= 0 && x < width && y < height) img.data.ptr[y*width+x] = v&0xff;
|
|
++x;
|
|
}
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("width=%d; height=%d; rdheight=%d\n", width, height, rdheight); }
|
|
for (;;) {
|
|
ubyte codelen = read1();
|
|
ubyte codecode = read1();
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("x=%d; y=%d; len=%u; code=%u\n", x, y, cast(uint)codelen, cast(uint)codecode); }
|
|
bytesRead += 2;
|
|
if (codelen == 0) {
|
|
// special code
|
|
if (codecode == 0) {
|
|
// end of line
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" EOL\n"); }
|
|
while (x < width) setpix(1);
|
|
x = 0;
|
|
y += rdheight;
|
|
if (y < 0 || y >= height) break; // ooops
|
|
} else if (codecode == 1) {
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" EOB\n"); }
|
|
// end of bitmap
|
|
break;
|
|
} else if (codecode == 2) {
|
|
// delta
|
|
int xofs = read1();
|
|
int yofs = read1();
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" deltax=%d; deltay=%d\n", xofs, yofs); }
|
|
bytesRead += 2;
|
|
x += xofs;
|
|
y += yofs*rdheight;
|
|
if (y < 0 || y >= height) break; // ooops
|
|
} else {
|
|
version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" LITERAL: %u\n", cast(uint)codecode); }
|
|
// literal copy
|
|
while (codecode-- > 0) {
|
|
setpix(read1());
|
|
++bytesRead;
|
|
}
|
|
version(arsd_debug_bitmap_loader) if (bytesRead%2) { import core.stdc.stdio; printf(" LITERAL SKIP\n"); }
|
|
if (bytesRead%2) { read1(); ++bytesRead; }
|
|
assert(bytesRead%2 == 0);
|
|
}
|
|
} else {
|
|
while (codelen-- > 0) setpix(codecode);
|
|
}
|
|
}
|
|
} else if (compression == 2) {
|
|
throw new Exception("4RLE for bitmaps aren't supported yet");
|
|
} else {
|
|
for(int y = height; y > 0; y--) {
|
|
if (rdheight < 0) offsetStart -= width * bytesPerPixel;
|
|
int offset = offsetStart;
|
|
while (bytesRead%4 != 0) {
|
|
read1();
|
|
++bytesRead;
|
|
}
|
|
bytesRead = 0;
|
|
|
|
for(int x = 0; x < width; x++) {
|
|
auto b = read1();
|
|
++bytesRead;
|
|
if(bitsPerPixel == 8) {
|
|
img.data[offset++] = b;
|
|
} else if(bitsPerPixel == 4) {
|
|
img.data[offset++] = (b&0xf0) >> 4;
|
|
x++;
|
|
if(offset == img.data.length)
|
|
break;
|
|
img.data[offset++] = (b&0x0f);
|
|
} else if(bitsPerPixel == 2) {
|
|
img.data[offset++] = (b & 0b11000000) >> 6;
|
|
x++;
|
|
if(offset == img.data.length)
|
|
break;
|
|
img.data[offset++] = (b & 0b00110000) >> 4;
|
|
x++;
|
|
if(offset == img.data.length)
|
|
break;
|
|
img.data[offset++] = (b & 0b00001100) >> 2;
|
|
x++;
|
|
if(offset == img.data.length)
|
|
break;
|
|
img.data[offset++] = (b & 0b00000011) >> 0;
|
|
} else if(bitsPerPixel == 1) {
|
|
foreach_reverse(lol; 0 .. 8) {
|
|
bool value = !!((b & (1 << lol)));
|
|
img.data[offset++] = value ? 1 : 0;
|
|
x++;
|
|
if(offset == img.data.length)
|
|
break;
|
|
}
|
|
x--; // we do this once too many times in the loop
|
|
} else assert(0);
|
|
// I don't think these happen in the wild but I could be wrong, my bmp knowledge is somewhat outdated
|
|
}
|
|
if (rdheight > 0) offsetStart += width * bytesPerPixel;
|
|
}
|
|
}
|
|
|
|
if(hasAndMask) {
|
|
auto tp = img.palette.length;
|
|
if(tp < 256) {
|
|
// easy, there's room, just add an entry.
|
|
img.palette ~= Color.transparent;
|
|
img.hasAlpha = true;
|
|
} else {
|
|
// not enough room, gotta try to find something unused to overwrite...
|
|
// FIXME: could prolly use more caution here
|
|
auto selection = 39;
|
|
|
|
img.palette[selection] = Color.transparent;
|
|
img.hasAlpha = true;
|
|
tp = selection;
|
|
}
|
|
|
|
if(tp < 256) {
|
|
processAndMask(delegate int(int x, int y, bool transparent) {
|
|
auto existing = img.data[y * img.width + x];
|
|
|
|
if(img.palette[existing] == Color.black && transparent) {
|
|
// import std.stdio; write("O");
|
|
img.data[y * img.width + x] = cast(ubyte) tp;
|
|
} else {
|
|
// import std.stdio; write("X");
|
|
}
|
|
|
|
return 1;
|
|
});
|
|
} else {
|
|
//import std.stdio; writeln("no room in palette for transparency alas");
|
|
}
|
|
}
|
|
|
|
return img;
|
|
} else {
|
|
if (compression != 0) throw new Exception("invalid bitmap compression");
|
|
// true color image
|
|
auto img = new TrueColorImage(width, height);
|
|
|
|
// no palette, so straight into the data
|
|
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;
|
|
foreach(x; 0 .. width) {
|
|
if(compression == 3) {
|
|
ubyte[8] buffer;
|
|
assert(bitsPerPixel / 8 < 8);
|
|
foreach(lol; 0 .. bitsPerPixel / 8) {
|
|
if(lol >= buffer.length)
|
|
throw new Exception("wtf");
|
|
buffer[lol] = read1();
|
|
b++;
|
|
}
|
|
|
|
ulong data = *(cast(ulong*) buffer.ptr);
|
|
|
|
auto blue = data & blueMask;
|
|
auto green = data & greenMask;
|
|
auto red = data & redMask;
|
|
auto alpha = data & alphaMask;
|
|
|
|
if(blueMask)
|
|
blue = blue * 255 / blueMask;
|
|
if(greenMask)
|
|
green = green * 255 / greenMask;
|
|
if(redMask)
|
|
red = red * 255 / redMask;
|
|
if(alphaMask)
|
|
alpha = alpha * 255 / alphaMask;
|
|
else
|
|
alpha = 255;
|
|
|
|
img.imageData.bytes[offset + 2] = cast(ubyte) blue;
|
|
img.imageData.bytes[offset + 1] = cast(ubyte) green;
|
|
img.imageData.bytes[offset + 0] = cast(ubyte) red;
|
|
img.imageData.bytes[offset + 3] = cast(ubyte) alpha;
|
|
} else {
|
|
assert(compression == 0);
|
|
|
|
if(bitsPerPixel == 24 || bitsPerPixel == 32) {
|
|
img.imageData.bytes[offset + 2] = read1(); // b
|
|
img.imageData.bytes[offset + 1] = read1(); // g
|
|
img.imageData.bytes[offset + 0] = read1(); // r
|
|
if(bitsPerPixel == 32) {
|
|
img.imageData.bytes[offset + 3] = read1(); // a
|
|
b++;
|
|
} else {
|
|
img.imageData.bytes[offset + 3] = 255; // a
|
|
}
|
|
b += 3;
|
|
} else {
|
|
assert(bitsPerPixel == 16);
|
|
// these are stored xrrrrrgggggbbbbb
|
|
ushort d = read1();
|
|
d |= cast(ushort)read1() << 8;
|
|
// we expect 8 bit numbers but these only give 5 bits of info,
|
|
// therefore we shift left 3 to get the right stuff.
|
|
img.imageData.bytes[offset + 0] = (d & 0b0111110000000000) >> (10-3);
|
|
img.imageData.bytes[offset + 1] = (d & 0b0000001111100000) >> (5-3);
|
|
img.imageData.bytes[offset + 2] = (d & 0b0000000000011111) << 3;
|
|
img.imageData.bytes[offset + 3] = 255; // r
|
|
b += 2;
|
|
}
|
|
}
|
|
|
|
offset += bytesPerPixel;
|
|
}
|
|
|
|
int w = b%4;
|
|
if(w)
|
|
for(int a = 0; a < 4-w; a++)
|
|
read1(); // pad until divisible by four
|
|
}
|
|
|
|
if(hasAndMask) {
|
|
processAndMask(delegate int(int x, int y, bool transparent) {
|
|
int offset = (y * img.width + x) * 4;
|
|
auto existing = img.imageData.bytes[offset + 3];
|
|
// only use the and mask if the alpha channel appears unused
|
|
if(transparent && existing == 255)
|
|
img.imageData.bytes[offset + 3] = 0;
|
|
//import std.stdio; write(transparent ? "o":"x");
|
|
|
|
return 4;
|
|
});
|
|
}
|
|
|
|
|
|
return img;
|
|
}
|
|
|
|
assert(0);
|
|
}
|
|
|
|
/// Writes the `img` out to `filename`, in .bmp format. Writes [TrueColorImage] out
|
|
/// as a 24 bmp and [IndexedImage] out as an 8 bit bmp. Drops transparency information.
|
|
void writeBmp(MemoryImage img, string filename) {
|
|
import core.stdc.stdio;
|
|
FILE* fp = fopen((filename ~ "\0").ptr, "wb".ptr);
|
|
if(fp is null)
|
|
throw new Exception("can't open save file");
|
|
scope(exit) fclose(fp);
|
|
|
|
int written;
|
|
void my_fwrite(ubyte b) {
|
|
written++;
|
|
fputc(b, fp);
|
|
}
|
|
|
|
writeBmpIndirect(img, &my_fwrite, true);
|
|
}
|
|
|
|
/+
|
|
void main() {
|
|
import arsd.simpledisplay;
|
|
//import std.file;
|
|
//auto img = readBmp(cast(ubyte[]) std.file.read("/home/me/test2.bmp"));
|
|
auto img = readBmp("/home/me/test2.bmp");
|
|
import std.stdio;
|
|
writeln((cast(Object)img).toString());
|
|
displayImage(Image.fromMemoryImage(img));
|
|
//img.writeBmp("/home/me/test2.bmp");
|
|
}
|
|
+/
|
|
|
|
/++
|
|
Writes a bitmap file to a delegate, byte by byte, with data from the given image.
|
|
|
|
If `prependFileHeader` is `true`, it will add the bitmap file header too.
|
|
+/
|
|
void writeBmpIndirect(MemoryImage img, scope void delegate(ubyte) fwrite, bool prependFileHeader) {
|
|
|
|
void write4(uint what){
|
|
fwrite(what & 0xff);
|
|
fwrite((what >> 8) & 0xff);
|
|
fwrite((what >> 16) & 0xff);
|
|
fwrite((what >> 24) & 0xff);
|
|
}
|
|
void write2(ushort what){
|
|
fwrite(what & 0xff);
|
|
fwrite(what >> 8);
|
|
}
|
|
void write1(ubyte what) { fwrite(what); }
|
|
|
|
int width = img.width;
|
|
int height = img.height;
|
|
ushort bitsPerPixel;
|
|
|
|
ubyte[] data;
|
|
Color[] palette;
|
|
|
|
// FIXME we should be able to write RGBA bitmaps too, though it seems like not many
|
|
// programs correctly read them!
|
|
|
|
if(auto tci = cast(TrueColorImage) img) {
|
|
bitsPerPixel = 24;
|
|
data = tci.imageData.bytes;
|
|
// we could also realistically do 16 but meh
|
|
} else if(auto pi = cast(IndexedImage) img) {
|
|
// FIXME: implement other bpps for more efficiency
|
|
/*
|
|
if(pi.palette.length == 2)
|
|
bitsPerPixel = 1;
|
|
else if(pi.palette.length <= 16)
|
|
bitsPerPixel = 4;
|
|
else
|
|
*/
|
|
bitsPerPixel = 8;
|
|
data = pi.data;
|
|
palette = pi.palette;
|
|
} else throw new Exception("I can't save this image type " ~ img.classinfo.name);
|
|
|
|
ushort offsetToBits;
|
|
if(bitsPerPixel == 8)
|
|
offsetToBits = 1078;
|
|
else if (bitsPerPixel == 24 || bitsPerPixel == 16)
|
|
offsetToBits = 54;
|
|
else
|
|
offsetToBits = cast(ushort)(54 * (1 << bitsPerPixel)); // room for the palette...
|
|
|
|
uint fileSize = offsetToBits;
|
|
if(bitsPerPixel == 8) {
|
|
fileSize += height * (width + width%4);
|
|
} else if(bitsPerPixel == 24)
|
|
fileSize += height * ((width * 3) + (!((width*3)%4) ? 0 : 4-((width*3)%4)));
|
|
else assert(0, "not implemented"); // FIXME
|
|
|
|
if(prependFileHeader) {
|
|
write1('B');
|
|
write1('M');
|
|
|
|
write4(fileSize); // size of file in bytes
|
|
write2(0); // reserved
|
|
write2(0); // reserved
|
|
write4(offsetToBits); // offset to the bitmap data
|
|
}
|
|
|
|
write4(40); // size of BITMAPINFOHEADER
|
|
|
|
write4(width); // width
|
|
write4(height); // height
|
|
|
|
write2(1); // planes
|
|
write2(bitsPerPixel); // bpp
|
|
write4(0); // compression
|
|
write4(0); // size of uncompressed
|
|
write4(0); // x pels per meter
|
|
write4(0); // y pels per meter
|
|
write4(0); // colors used
|
|
write4(0); // colors important
|
|
|
|
// And here we write the palette
|
|
if(bitsPerPixel <= 8)
|
|
foreach(c; palette[0..(1 << bitsPerPixel)]){
|
|
write1(c.b);
|
|
write1(c.g);
|
|
write1(c.r);
|
|
write1(0);
|
|
}
|
|
|
|
// And finally the data
|
|
|
|
int bytesPerPixel;
|
|
if(bitsPerPixel == 8)
|
|
bytesPerPixel = 1;
|
|
else if(bitsPerPixel == 24)
|
|
bytesPerPixel = 4;
|
|
else assert(0, "not implemented"); // FIXME
|
|
|
|
int offsetStart = cast(int) data.length;
|
|
for(int y = height; y > 0; y--) {
|
|
offsetStart -= width * bytesPerPixel;
|
|
int offset = offsetStart;
|
|
int b = 0;
|
|
foreach(x; 0 .. width) {
|
|
if(bitsPerPixel == 8) {
|
|
write1(data[offset]);
|
|
b++;
|
|
} else if(bitsPerPixel == 24) {
|
|
write1(data[offset + 2]); // blue
|
|
write1(data[offset + 1]); // green
|
|
write1(data[offset + 0]); // red
|
|
b += 3;
|
|
} else assert(0); // FIXME
|
|
offset += bytesPerPixel;
|
|
}
|
|
|
|
int w = b%4;
|
|
if(w)
|
|
for(int a = 0; a < 4-w; a++)
|
|
write1(0); // pad until divisible by four
|
|
}
|
|
}
|