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