1 /++
2 	Basic .bmp file format implementation for [arsd.color.MemoryImage].
3 	Compare with [arsd.png] basic functionality.
4 +/
5 module arsd.bmp;
6 
7 import arsd.color;
8 
9 //version = arsd_debug_bitmap_loader;
10 
11 
12 /// Reads a .bmp file from the given `filename`
13 MemoryImage readBmp(string filename) {
14 	import core.stdc.stdio;
15 
16 	FILE* fp = fopen((filename ~ "\0").ptr, "rb".ptr);
17 	if(fp is null)
18 		throw new Exception("can't open save file");
19 	scope(exit) fclose(fp);
20 
21 	void specialFread(void* tgt, size_t size) {
22 		version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("ofs: 0x%08x\n", cast(uint)ftell(fp)); }
23 		fread(tgt, size, 1, fp);
24 	}
25 
26 	return readBmpIndirect(&specialFread);
27 }
28 
29 /++
30 	Reads a bitmap out of an in-memory array of data. For example, from the data returned from [std.file.read].
31 
32 	It forwards the arguments to [readBmpIndirect], so see that for more details.
33 
34 	If you are given a raw pointer to some data, you might just slice it: bytes 2-6 of the file header (if present)
35 	are a little-endian uint giving the file size. You might slice only to that, or you could slice right to `int.max`
36 	and trust the library to bounds check for you based on data integrity checks.
37 +/
38 MemoryImage readBmp(in ubyte[] data, bool lookForFileHeader = true, bool hackAround64BitLongs = false, bool hasAndMask = false) {
39 	int position;
40 	const(ubyte)[] current = data;
41 	void specialFread(void* tgt, size_t size) {
42 		while(size) {
43 			if (current.length == 0) throw new Exception("out of bmp data"); // it's not *that* fatal, so don't throw RangeError
44 			//import std.stdio; writefln("%04x", position);
45 			*cast(ubyte*)(tgt) = current[0];
46 			current = current[1 .. $];
47 			position++;
48 			tgt++;
49 			size--;
50 		}
51 	}
52 
53 	return readBmpIndirect(&specialFread, lookForFileHeader, hackAround64BitLongs, hasAndMask);
54 }
55 
56 /++
57 	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
58 
59 	History:
60 		The `lookForFileHeader` param was added in July 2020.
61 
62 		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.
63 
64 		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.
65 +/
66 MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookForFileHeader = true, bool hackAround64BitLongs = false, bool hasAndMask = false) {
67 	uint read4()  { uint what; fread(&what, 4); return what; }
68 	uint readLONG()  {
69 		auto le = read4();
70 		/++
71 			A user on discord encountered a file in the wild that wouldn't load
72 			by any other bmp viewer. After looking at the raw bytes, it appeared it
73 			wrote out the LONG fields on the bitmap info header as 64 bit values when
74 			they are supposed to always be 32 bit values. This hack gives a chance to work
75 			around that and load the file anyway.
76 		+/
77 		if(hackAround64BitLongs)
78 			if(read4() != 0)
79 				throw new Exception("hackAround64BitLongs is true, but the file doesn't appear to use 64 bit longs");
80 		return le;
81 	}
82 	ushort read2(){ ushort what; fread(&what, 2); return what; }
83 
84 	bool headerRead = false;
85 	int hackCounter;
86 
87 	ubyte read1() {
88 		if(hackAround64BitLongs && headerRead && hackCounter < 16) {
89 			hackCounter++;
90 			return 0;
91 		}
92 		ubyte what;
93 		fread(&what, 1);
94 		return what;
95 	}
96 
97 	void require1(ubyte t, size_t line = __LINE__) {
98 		if(read1() != t)
99 			throw new Exception("didn't get expected byte value", __FILE__, line);
100 	}
101 	void require2(ushort t) {
102 		auto got = read2();
103 		if(got != t) {
104 			version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("expected: %d, got %d\n", cast(int) t, cast(int) got); }
105 			throw new Exception("didn't get expected short value");
106 		}
107 	}
108 	void require4(uint t, size_t line = __LINE__) {
109 		auto got = read4();
110 		//import std.conv;
111 		if(got != t)
112 			throw new Exception("didn't get expected int value " /*~ to!string(got)*/, __FILE__, line);
113 	}
114 
115 	if(lookForFileHeader) {
116 		require1('B');
117 		require1('M');
118 
119 		auto fileSize = read4(); // size of file in bytes
120 		require2(0); // reserved
121 		require2(0); 	// reserved
122 
123 		auto offsetToBits = read4();
124 		version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("pixel data offset: 0x%08x\n", cast(uint)offsetToBits); }
125 	}
126 
127 	auto sizeOfBitmapInfoHeader = read4();
128 	if (sizeOfBitmapInfoHeader < 12) throw new Exception("invalid bitmap header size");
129 
130 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size of bitmap info header: %d\n", cast(uint)sizeOfBitmapInfoHeader); }
131 
132 	int width, height, rdheight;
133 
134 	if (sizeOfBitmapInfoHeader == 12) {
135 		width = read2();
136 		rdheight = cast(short)read2();
137 	} else {
138 		if (sizeOfBitmapInfoHeader < 16) throw new Exception("invalid bitmap header size");
139 		sizeOfBitmapInfoHeader -= 4; // hack!
140 		width = readLONG();
141 		rdheight = cast(int)readLONG();
142 	}
143 
144 	height = (rdheight < 0 ? -rdheight : rdheight);
145 
146 	if(hasAndMask) {
147 		version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("has and mask so height slashed %d\n", height / 2); }
148 		height = height / 2;
149 	}
150 
151 	rdheight = (rdheight < 0 ? 1 : -1); // so we can use it as delta (note the inverted sign)
152 
153 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size: %dx%d\n", cast(int)width, cast(int) height); }
154 	if (width < 1 || height < 1) throw new Exception("invalid bitmap dimensions");
155 
156 	require2(1); // planes
157 
158 	auto bitsPerPixel = read2();
159 	switch (bitsPerPixel) {
160 		case 1: case 2: case 4: case 8: case 16: case 24: case 32: break;
161 		default: throw new Exception("invalid bitmap depth");
162 	}
163 
164 	/*
165 		0 = BI_RGB
166 		1 = BI_RLE8   RLE 8-bit/pixel   Can be used only with 8-bit/pixel bitmaps
167 		2 = BI_RLE4   RLE 4-bit/pixel   Can be used only with 4-bit/pixel bitmaps
168 		3 = BI_BITFIELDS
169 	*/
170 	uint compression = 0;
171 	uint sizeOfUncompressedData = 0;
172 	uint xPixelsPerMeter = 0;
173 	uint yPixelsPerMeter = 0;
174 	uint colorsUsed = 0;
175 	uint colorsImportant = 0;
176 
177 	sizeOfBitmapInfoHeader -= 12;
178 	if (sizeOfBitmapInfoHeader > 0) {
179 		if (sizeOfBitmapInfoHeader < 6*4) throw new Exception("invalid bitmap header size");
180 		sizeOfBitmapInfoHeader -= 6*4;
181 		compression = read4();
182 		sizeOfUncompressedData = read4();
183 		xPixelsPerMeter = readLONG();
184 		yPixelsPerMeter = readLONG();
185 		colorsUsed = read4();
186 		colorsImportant = read4();
187 	}
188 
189 	if (compression > 3) throw new Exception("invalid bitmap compression");
190 	if (compression == 1 && bitsPerPixel != 8) throw new Exception("invalid bitmap compression");
191 	if (compression == 2 && bitsPerPixel != 4) throw new Exception("invalid bitmap compression");
192 
193 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("compression: %u; bpp: %u\n", compression, cast(uint)bitsPerPixel); }
194 
195 	uint redMask;
196 	uint greenMask;
197 	uint blueMask;
198 	uint alphaMask;
199 	if (compression == 3) {
200 		if (sizeOfBitmapInfoHeader < 4*4) throw new Exception("invalid bitmap compression");
201 		sizeOfBitmapInfoHeader -= 4*4;
202 		redMask = read4();
203 		greenMask = read4();
204 		blueMask = read4();
205 		alphaMask = read4();
206 	}
207 	// FIXME: we could probably handle RLE4 as well
208 
209 	// I don't know about the rest of the header, so I'm just skipping it.
210 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("header bytes left: %u\n", cast(uint)sizeOfBitmapInfoHeader); }
211 	foreach (skip; 0..sizeOfBitmapInfoHeader) read1();
212 
213 	headerRead = true;
214 
215 
216 
217 	// the dg returns the change in offset
218 	void processAndMask(scope int delegate(int x, int y, bool transparent) apply) {
219 		try {
220 			// the and mask is always 1bpp and i want to translate it into transparent pixels
221 
222 			for(int y = (height - 1); y >= 0; y--) {
223 				//version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" reading and mask %d\n", y); }
224 				int read;
225 				for(int x = 0; x < width; x++) {
226 					const b = read1();
227 					//import std.stdio; writefln("%02x", b);
228 					read++;
229 					foreach_reverse(lol; 0 .. 8) {
230 						bool transparent = !!((b & (1 << lol)));
231 						version(arsd_debug_bitmap_loader) { import std.stdio; write(transparent ? "o":"x"); }
232 						apply(x, y, transparent);
233 
234 						x++;
235 						if(x >= width)
236 							break;
237 					}
238 					x--; // we do this once too many times in the loop
239 				}
240 				while(read % 4) {
241 					read1();
242 					read++;
243 				}
244 				version(arsd_debug_bitmap_loader) {import std.stdio; writeln(""); }
245 			}
246 
247 			/+
248 			this the algorithm btw
249 			keep.imageData.bytes[] &= tci.imageData.bytes[andOffset .. $];
250 			keep.imageData.bytes[] ^= tci.imageData.bytes[0 .. andOffset];
251 			+/
252 		} catch(Exception e) {
253 			// discard; the and mask is optional in practice since using all 0's
254 			// gives a result and some files in the wild deliberately truncate the
255 			// file (though they aren't supposed to....) expecting readers to do this.
256 			version(arsd_debug_bitmap_loader) { import std.stdio; writeln(e); }
257 		}
258 	}
259 
260 
261 
262 	if(bitsPerPixel <= 8) {
263 		// indexed image
264 		version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("colorsUsed=%u; colorsImportant=%u\n", colorsUsed, colorsImportant); }
265 		if (colorsUsed == 0 || colorsUsed > (1 << bitsPerPixel)) colorsUsed = (1 << bitsPerPixel);
266 		auto img = new IndexedImage(width, height);
267 		img.palette.reserve(1 << bitsPerPixel);
268 
269 		foreach(idx; 0 .. /*(1 << bitsPerPixel)*/colorsUsed) {
270 			auto b = read1();
271 			auto g = read1();
272 			auto r = read1();
273 			auto reserved = read1();
274 
275 			img.palette ~= Color(r, g, b);
276 		}
277 		while (img.palette.length < (1 << bitsPerPixel)) img.palette ~= Color.transparent;
278 
279 		// and the data
280 		int bytesPerPixel = 1;
281 		auto offsetStart = (rdheight > 0 ? 0 : width * height * bytesPerPixel);
282 		int bytesRead = 0;
283 
284 		if (compression == 1) {
285 			// this is complicated
286 			assert(bitsPerPixel == 8); // always
287 			int x = 0, y = (rdheight > 0 ? 0 : height-1);
288 			void setpix (int v) {
289 				if (x >= 0 && y >= 0 && x < width && y < height) img.data.ptr[y*width+x] = v&0xff;
290 				++x;
291 			}
292 			version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("width=%d; height=%d; rdheight=%d\n", width, height, rdheight); }
293 			for (;;) {
294 				ubyte codelen = read1();
295 				ubyte codecode = read1();
296 				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); }
297 				bytesRead += 2;
298 				if (codelen == 0) {
299 					// special code
300 					if (codecode == 0) {
301 						// end of line
302 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  EOL\n"); }
303 						while (x < width) setpix(1);
304 						x = 0;
305 						y += rdheight;
306 						if (y < 0 || y >= height) break; // ooops
307 					} else if (codecode == 1) {
308 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  EOB\n"); }
309 						// end of bitmap
310 						break;
311 					} else if (codecode == 2) {
312 						// delta
313 						int xofs = read1();
314 						int yofs = read1();
315 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  deltax=%d; deltay=%d\n", xofs, yofs); }
316 						bytesRead += 2;
317 						x += xofs;
318 						y += yofs*rdheight;
319 						if (y < 0 || y >= height) break; // ooops
320 					} else {
321 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  LITERAL: %u\n", cast(uint)codecode); }
322 						// literal copy
323 						while (codecode-- > 0) {
324 							setpix(read1());
325 							++bytesRead;
326 						}
327 						version(arsd_debug_bitmap_loader) if (bytesRead%2) { import core.stdc.stdio; printf("  LITERAL SKIP\n"); }
328 						if (bytesRead%2) { read1(); ++bytesRead; }
329 						assert(bytesRead%2 == 0);
330 					}
331 				} else {
332 					while (codelen-- > 0) setpix(codecode);
333 				}
334 			}
335 		} else if (compression == 2) {
336 			throw new Exception("4RLE for bitmaps aren't supported yet");
337 		} else {
338 			for(int y = height; y > 0; y--) {
339 				if (rdheight < 0) offsetStart -= width * bytesPerPixel;
340 				int offset = offsetStart;
341 				while (bytesRead%4 != 0) {
342 					read1();
343 					++bytesRead;
344 				}
345 				bytesRead = 0;
346 
347 				for(int x = 0; x < width; x++) {
348 					auto b = read1();
349 					++bytesRead;
350 					if(bitsPerPixel == 8) {
351 						img.data[offset++] = b;
352 					} else if(bitsPerPixel == 4) {
353 						img.data[offset++] = (b&0xf0) >> 4;
354 						x++;
355 						if(offset == img.data.length)
356 							break;
357 						img.data[offset++] = (b&0x0f);
358 					} else if(bitsPerPixel == 2) {
359 						img.data[offset++] = (b & 0b11000000) >> 6;
360 						x++;
361 						if(offset == img.data.length)
362 							break;
363 						img.data[offset++] = (b & 0b00110000) >> 4;
364 						x++;
365 						if(offset == img.data.length)
366 							break;
367 						img.data[offset++] = (b & 0b00001100) >> 2;
368 						x++;
369 						if(offset == img.data.length)
370 							break;
371 						img.data[offset++] = (b & 0b00000011) >> 0;
372 					} else if(bitsPerPixel == 1) {
373 						foreach_reverse(lol; 0 .. 8) {
374 							bool value = !!((b & (1 << lol)));
375 							img.data[offset++] = value ? 1 : 0;
376 							x++;
377 							if(offset == img.data.length)
378 								break;
379 						}
380 						x--; // we do this once too many times in the loop
381 					} else assert(0);
382 					// I don't think these happen in the wild but I could be wrong, my bmp knowledge is somewhat outdated
383 				}
384 				if (rdheight > 0) offsetStart += width * bytesPerPixel;
385 			}
386 		}
387 
388 		if(hasAndMask) {
389 			auto tp = img.palette.length;
390 			if(tp < 256) {
391 				// easy, there's room, just add an entry.
392 				img.palette ~= Color.transparent;
393 				img.hasAlpha = true;
394 			} else {
395 				// not enough room, gotta try to find something unused to overwrite...
396 				// FIXME: could prolly use more caution here
397 				auto selection = 39;
398 
399 				img.palette[selection] = Color.transparent;
400 				img.hasAlpha = true;
401 				tp = selection;
402 			}
403 
404 			if(tp < 256) {
405 				processAndMask(delegate int(int x, int y, bool transparent) {
406 					auto existing = img.data[y * img.width + x];
407 
408 					if(img.palette[existing] == Color.black && transparent) {
409 						// import std.stdio; write("O");
410 						img.data[y * img.width + x] = cast(ubyte) tp;
411 					} else {
412 						// import std.stdio; write("X");
413 					}
414 
415 					return 1;
416 				});
417 			} else {
418 				//import std.stdio; writeln("no room in palette for transparency alas");
419 			}
420 		}
421 
422 		return img;
423 	} else {
424 		if (compression != 0) throw new Exception("invalid bitmap compression");
425 		// true color image
426 		auto img = new TrueColorImage(width, height);
427 
428 		// no palette, so straight into the data
429 		int offsetStart = width * height * 4;
430 		int bytesPerPixel = 4;
431 		for(int y = height; y > 0; y--) {
432 			version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  true color image: %d\n", y); }
433 			offsetStart -= width * bytesPerPixel;
434 			int offset = offsetStart;
435 			int b = 0;
436 			foreach(x; 0 .. width) {
437 				if(compression == 3) {
438 					ubyte[8] buffer;
439 					assert(bitsPerPixel / 8 < 8);
440 					foreach(lol; 0 .. bitsPerPixel / 8) {
441 						if(lol >= buffer.length)
442 							throw new Exception("wtf");
443 						buffer[lol] = read1();
444 						b++;
445 					}
446 
447 					ulong data = *(cast(ulong*) buffer.ptr);
448 
449 					auto blue = data & blueMask;
450 					auto green = data & greenMask;
451 					auto red = data & redMask;
452 					auto alpha = data & alphaMask;
453 
454 					if(blueMask)
455 						blue = blue * 255 / blueMask;
456 					if(greenMask)
457 						green = green * 255 / greenMask;
458 					if(redMask)
459 						red = red * 255 / redMask;
460 					if(alphaMask)
461 						alpha = alpha * 255 / alphaMask;
462 					else
463 						alpha = 255;
464 
465 					img.imageData.bytes[offset + 2] = cast(ubyte) blue;
466 					img.imageData.bytes[offset + 1] = cast(ubyte) green;
467 					img.imageData.bytes[offset + 0] = cast(ubyte) red;
468 					img.imageData.bytes[offset + 3] = cast(ubyte) alpha;
469 				} else {
470 					assert(compression == 0);
471 
472 					if(bitsPerPixel == 24 || bitsPerPixel == 32) {
473 						img.imageData.bytes[offset + 2] = read1(); // b
474 						img.imageData.bytes[offset + 1] = read1(); // g
475 						img.imageData.bytes[offset + 0] = read1(); // r
476 						if(bitsPerPixel == 32) {
477 							img.imageData.bytes[offset + 3] = read1(); // a
478 							b++;
479 						} else {
480 							img.imageData.bytes[offset + 3] = 255; // a
481 						}
482 						b += 3;
483 					} else {
484 						assert(bitsPerPixel == 16);
485 						// these are stored xrrrrrgggggbbbbb
486 						ushort d = read1();
487 						d |= cast(ushort)read1() << 8;
488 							// we expect 8 bit numbers but these only give 5 bits of info,
489 							// therefore we shift left 3 to get the right stuff.
490 						img.imageData.bytes[offset + 0] = (d & 0b0111110000000000) >> (10-3);
491 						img.imageData.bytes[offset + 1] = (d & 0b0000001111100000) >> (5-3);
492 						img.imageData.bytes[offset + 2] = (d & 0b0000000000011111) << 3;
493 						img.imageData.bytes[offset + 3] = 255; // r
494 						b += 2;
495 					}
496 				}
497 
498 				offset += bytesPerPixel;
499 			}
500 
501 			int w = b%4;
502 			if(w)
503 			for(int a = 0; a < 4-w; a++)
504 				read1(); // pad until divisible by four
505 		}
506 
507 		if(hasAndMask) {
508 			processAndMask(delegate int(int x, int y, bool transparent) {
509 				int offset = (y * img.width + x) * 4;
510 				auto existing = img.imageData.bytes[offset + 3];
511 				// only use the and mask if the alpha channel appears unused
512 				if(transparent && existing == 255)
513 					img.imageData.bytes[offset + 3] = 0;
514 				//import std.stdio; write(transparent ? "o":"x");
515 
516 				return 4;
517 			});
518 		}
519 
520 
521 		return img;
522 	}
523 
524 	assert(0);
525 }
526 
527 /// Writes the `img` out to `filename`, in .bmp format. Writes [TrueColorImage] out
528 /// as a 24 bmp and [IndexedImage] out as an 8 bit bmp. Drops transparency information.
529 void writeBmp(MemoryImage img, string filename) {
530 	import core.stdc.stdio;
531 	FILE* fp = fopen((filename ~ "\0").ptr, "wb".ptr);
532 	if(fp is null)
533 		throw new Exception("can't open save file");
534 	scope(exit) fclose(fp);
535 
536 	int written;
537 	void my_fwrite(ubyte b) {
538 		written++;
539 		fputc(b, fp);
540 	}
541 
542 	writeBmpIndirect(img, &my_fwrite, true);
543 }
544 
545 /+
546 void main() {
547 	import arsd.simpledisplay;
548 	//import std.file;
549 	//auto img = readBmp(cast(ubyte[]) std.file.read("/home/me/test2.bmp"));
550 	auto img = readBmp("/home/me/test2.bmp");
551 	import std.stdio;
552 	writeln((cast(Object)img).toString());
553 	displayImage(Image.fromMemoryImage(img));
554 	//img.writeBmp("/home/me/test2.bmp");
555 }
556 +/
557 
558 /++
559 	Writes a bitmap file to a delegate, byte by byte, with data from the given image.
560 
561 	If `prependFileHeader` is `true`, it will add the bitmap file header too.
562 +/
563 void writeBmpIndirect(MemoryImage img, scope void delegate(ubyte) fwrite, bool prependFileHeader) {
564 
565 	void write4(uint what){
566 		fwrite(what & 0xff);
567 		fwrite((what >> 8) & 0xff);
568 		fwrite((what >> 16) & 0xff);
569 		fwrite((what >> 24) & 0xff);
570 	}
571 	void write2(ushort what){
572 		fwrite(what & 0xff);
573 		fwrite(what >> 8);
574 	}
575 	void write1(ubyte what) { fwrite(what); }
576 
577 	int width = img.width;
578 	int height = img.height;
579 	ushort bitsPerPixel;
580 
581 	ubyte[] data;
582 	Color[] palette;
583 
584 	// FIXME we should be able to write RGBA bitmaps too, though it seems like not many
585 	// programs correctly read them!
586 
587 	if(auto tci = cast(TrueColorImage) img) {
588 		bitsPerPixel = 24;
589 		data = tci.imageData.bytes;
590 		// we could also realistically do 16 but meh
591 	} else if(auto pi = cast(IndexedImage) img) {
592 		// FIXME: implement other bpps for more efficiency
593 		/*
594 		if(pi.palette.length == 2)
595 			bitsPerPixel = 1;
596 		else if(pi.palette.length <= 16)
597 			bitsPerPixel = 4;
598 		else
599 		*/
600 			bitsPerPixel = 8;
601 		data = pi.data;
602 		palette = pi.palette;
603 	} else throw new Exception("I can't save this image type " ~ img.classinfo.name);
604 
605 	ushort offsetToBits;
606 	if(bitsPerPixel == 8)
607 		offsetToBits = 1078;
608 	else if (bitsPerPixel == 24 || bitsPerPixel == 16)
609 		offsetToBits = 54;
610 	else
611 		offsetToBits = cast(ushort)(54 * (1 << bitsPerPixel)); // room for the palette...
612 
613 	uint fileSize = offsetToBits;
614 	if(bitsPerPixel == 8) {
615 		fileSize += height * (width + width%4);
616 	} else if(bitsPerPixel == 24)
617 		fileSize += height * ((width * 3) + (!((width*3)%4) ? 0 : 4-((width*3)%4)));
618 	else assert(0, "not implemented"); // FIXME
619 
620 	if(prependFileHeader) {
621 		write1('B');
622 		write1('M');
623 
624 		write4(fileSize); // size of file in bytes
625 		write2(0); 	// reserved
626 		write2(0); 	// reserved
627 		write4(offsetToBits); // offset to the bitmap data
628 	}
629 
630 	write4(40); // size of BITMAPINFOHEADER
631 
632 	write4(width); // width
633 	write4(height); // height
634 
635 	write2(1); // planes
636 	write2(bitsPerPixel); // bpp
637 	write4(0); // compression
638 	write4(0); // size of uncompressed
639 	write4(0); // x pels per meter
640 	write4(0); // y pels per meter
641 	write4(0); // colors used
642 	write4(0); // colors important
643 
644 	// And here we write the palette
645 	if(bitsPerPixel <= 8)
646 		foreach(c; palette[0..(1 << bitsPerPixel)]){
647 			write1(c.b);
648 			write1(c.g);
649 			write1(c.r);
650 			write1(0);
651 		}
652 
653 	// And finally the data
654 
655 	int bytesPerPixel;
656 	if(bitsPerPixel == 8)
657 		bytesPerPixel = 1;
658 	else if(bitsPerPixel == 24)
659 		bytesPerPixel = 4;
660 	else assert(0, "not implemented"); // FIXME
661 
662 	int offsetStart = cast(int) data.length;
663 	for(int y = height; y > 0; y--) {
664 		offsetStart -= width * bytesPerPixel;
665 		int offset = offsetStart;
666 		int b = 0;
667 		foreach(x; 0 .. width) {
668 			if(bitsPerPixel == 8) {
669 				write1(data[offset]);
670 				b++;
671 			} else if(bitsPerPixel == 24) {
672 				write1(data[offset + 2]); // blue
673 				write1(data[offset + 1]); // green
674 				write1(data[offset + 0]); // red
675 				b += 3;
676 			} else assert(0); // FIXME
677 			offset += bytesPerPixel;
678 		}
679 
680 		int w = b%4;
681 		if(w)
682 		for(int a = 0; a < 4-w; a++)
683 			write1(0); // pad until divisible by four
684 	}
685 }