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) {
39 	const(ubyte)[] current = data;
40 	void specialFread(void* tgt, size_t size) {
41 		while(size) {
42 			if (current.length == 0) throw new Exception("out of bmp data"); // it's not *that* fatal, so don't throw RangeError
43 			*cast(ubyte*)(tgt) = current[0];
44 			current = current[1 .. $];
45 			tgt++;
46 			size--;
47 		}
48 	}
49 
50 	return readBmpIndirect(&specialFread, lookForFileHeader, hackAround64BitLongs);
51 }
52 
53 /++
54 	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
55 
56 	History:
57 		The `lookForFileHeader` param was added in July 2020.
58 
59 		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.
60 +/
61 MemoryImage readBmpIndirect(scope void delegate(void*, size_t) fread, bool lookForFileHeader = true, bool hackAround64BitLongs = false) {
62 	uint read4()  { uint what; fread(&what, 4); return what; }
63 	uint readLONG()  {
64 		auto le = read4();
65 		/++
66 			A user on discord encountered a file in the wild that wouldn't load
67 			by any other bmp viewer. After looking at the raw bytes, it appeared it
68 			wrote out the LONG fields on the bitmap info header as 64 bit values when
69 			they are supposed to always be 32 bit values. This hack gives a chance to work
70 			around that and load the file anyway.
71 		+/
72 		if(hackAround64BitLongs)
73 			if(read4() != 0)
74 				throw new Exception("hackAround64BitLongs is true, but the file doesn't appear to use 64 bit longs");
75 		return le;
76 	}
77 	ushort read2(){ ushort what; fread(&what, 2); return what; }
78 
79 	bool headerRead = false;
80 	int hackCounter;
81 
82 	ubyte read1() {
83 		if(hackAround64BitLongs && headerRead && hackCounter < 16) {
84 			hackCounter++;
85 			return 0;
86 		}
87 		ubyte what;
88 		fread(&what, 1);
89 		return what;
90 	}
91 
92 	void require1(ubyte t, size_t line = __LINE__) {
93 		if(read1() != t)
94 			throw new Exception("didn't get expected byte value", __FILE__, line);
95 	}
96 	void require2(ushort t) {
97 		auto got = read2();
98 		if(got != t) {
99 			version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("expected: %d, got %d\n", cast(int) t, cast(int) got); }
100 			throw new Exception("didn't get expected short value");
101 		}
102 	}
103 	void require4(uint t, size_t line = __LINE__) {
104 		auto got = read4();
105 		//import std.conv;
106 		if(got != t)
107 			throw new Exception("didn't get expected int value " /*~ to!string(got)*/, __FILE__, line);
108 	}
109 
110 	if(lookForFileHeader) {
111 		require1('B');
112 		require1('M');
113 
114 		auto fileSize = read4(); // size of file in bytes
115 		require2(0); // reserved
116 		require2(0); 	// reserved
117 
118 		auto offsetToBits = read4();
119 		version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("pixel data offset: 0x%08x\n", cast(uint)offsetToBits); }
120 	}
121 
122 	auto sizeOfBitmapInfoHeader = read4();
123 	if (sizeOfBitmapInfoHeader < 12) throw new Exception("invalid bitmap header size");
124 
125 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size of bitmap info header: %d\n", cast(uint)sizeOfBitmapInfoHeader); }
126 
127 	int width, height, rdheight;
128 
129 	if (sizeOfBitmapInfoHeader == 12) {
130 		width = read2();
131 		rdheight = cast(short)read2();
132 	} else {
133 		if (sizeOfBitmapInfoHeader < 16) throw new Exception("invalid bitmap header size");
134 		sizeOfBitmapInfoHeader -= 4; // hack!
135 		width = readLONG();
136 		rdheight = cast(int)readLONG();
137 	}
138 
139 	height = (rdheight < 0 ? -rdheight : rdheight);
140 	rdheight = (rdheight < 0 ? 1 : -1); // so we can use it as delta (note the inverted sign)
141 
142 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size: %dx%d\n", cast(int)width, cast(int) height); }
143 	if (width < 1 || height < 1) throw new Exception("invalid bitmap dimensions");
144 
145 	require2(1); // planes
146 
147 	auto bitsPerPixel = read2();
148 	switch (bitsPerPixel) {
149 		case 1: case 2: case 4: case 8: case 16: case 24: case 32: break;
150 		default: throw new Exception("invalid bitmap depth");
151 	}
152 
153 	/*
154 		0 = BI_RGB
155 		1 = BI_RLE8   RLE 8-bit/pixel   Can be used only with 8-bit/pixel bitmaps
156 		2 = BI_RLE4   RLE 4-bit/pixel   Can be used only with 4-bit/pixel bitmaps
157 		3 = BI_BITFIELDS
158 	*/
159 	uint compression = 0;
160 	uint sizeOfUncompressedData = 0;
161 	uint xPixelsPerMeter = 0;
162 	uint yPixelsPerMeter = 0;
163 	uint colorsUsed = 0;
164 	uint colorsImportant = 0;
165 
166 	sizeOfBitmapInfoHeader -= 12;
167 	if (sizeOfBitmapInfoHeader > 0) {
168 		if (sizeOfBitmapInfoHeader < 6*4) throw new Exception("invalid bitmap header size");
169 		sizeOfBitmapInfoHeader -= 6*4;
170 		compression = read4();
171 		sizeOfUncompressedData = read4();
172 		xPixelsPerMeter = readLONG();
173 		yPixelsPerMeter = readLONG();
174 		colorsUsed = read4();
175 		colorsImportant = read4();
176 	}
177 
178 	if (compression > 3) throw new Exception("invalid bitmap compression");
179 	if (compression == 1 && bitsPerPixel != 8) throw new Exception("invalid bitmap compression");
180 	if (compression == 2 && bitsPerPixel != 4) throw new Exception("invalid bitmap compression");
181 
182 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("compression: %u; bpp: %u\n", compression, cast(uint)bitsPerPixel); }
183 
184 	uint redMask;
185 	uint greenMask;
186 	uint blueMask;
187 	uint alphaMask;
188 	if (compression == 3) {
189 		if (sizeOfBitmapInfoHeader < 4*4) throw new Exception("invalid bitmap compression");
190 		sizeOfBitmapInfoHeader -= 4*4;
191 		redMask = read4();
192 		greenMask = read4();
193 		blueMask = read4();
194 		alphaMask = read4();
195 	}
196 	// FIXME: we could probably handle RLE4 as well
197 
198 	// I don't know about the rest of the header, so I'm just skipping it.
199 	version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("header bytes left: %u\n", cast(uint)sizeOfBitmapInfoHeader); }
200 	foreach (skip; 0..sizeOfBitmapInfoHeader) read1();
201 
202 	headerRead = true;
203 
204 	if(bitsPerPixel <= 8) {
205 		// indexed image
206 		version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("colorsUsed=%u; colorsImportant=%u\n", colorsUsed, colorsImportant); }
207 		if (colorsUsed == 0 || colorsUsed > (1 << bitsPerPixel)) colorsUsed = (1 << bitsPerPixel);
208 		auto img = new IndexedImage(width, height);
209 		img.palette.reserve(1 << bitsPerPixel);
210 
211 		foreach(idx; 0 .. /*(1 << bitsPerPixel)*/colorsUsed) {
212 			auto b = read1();
213 			auto g = read1();
214 			auto r = read1();
215 			auto reserved = read1();
216 
217 			img.palette ~= Color(r, g, b);
218 		}
219 		while (img.palette.length < (1 << bitsPerPixel)) img.palette ~= Color.transparent;
220 
221 		// and the data
222 		int bytesPerPixel = 1;
223 		auto offsetStart = (rdheight > 0 ? 0 : width * height * bytesPerPixel);
224 		int bytesRead = 0;
225 
226 		if (compression == 1) {
227 			// this is complicated
228 			assert(bitsPerPixel == 8); // always
229 			int x = 0, y = (rdheight > 0 ? 0 : height-1);
230 			void setpix (int v) {
231 				if (x >= 0 && y >= 0 && x < width && y < height) img.data.ptr[y*width+x] = v&0xff;
232 				++x;
233 			}
234 			version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("width=%d; height=%d; rdheight=%d\n", width, height, rdheight); }
235 			for (;;) {
236 				ubyte codelen = read1();
237 				ubyte codecode = read1();
238 				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); }
239 				bytesRead += 2;
240 				if (codelen == 0) {
241 					// special code
242 					if (codecode == 0) {
243 						// end of line
244 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  EOL\n"); }
245 						while (x < width) setpix(1);
246 						x = 0;
247 						y += rdheight;
248 						if (y < 0 || y >= height) break; // ooops
249 					} else if (codecode == 1) {
250 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  EOB\n"); }
251 						// end of bitmap
252 						break;
253 					} else if (codecode == 2) {
254 						// delta
255 						int xofs = read1();
256 						int yofs = read1();
257 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  deltax=%d; deltay=%d\n", xofs, yofs); }
258 						bytesRead += 2;
259 						x += xofs;
260 						y += yofs*rdheight;
261 						if (y < 0 || y >= height) break; // ooops
262 					} else {
263 						version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("  LITERAL: %u\n", cast(uint)codecode); }
264 						// literal copy
265 						while (codecode-- > 0) {
266 							setpix(read1());
267 							++bytesRead;
268 						}
269 						version(arsd_debug_bitmap_loader) if (bytesRead%2) { import core.stdc.stdio; printf("  LITERAL SKIP\n"); }
270 						if (bytesRead%2) { read1(); ++bytesRead; }
271 						assert(bytesRead%2 == 0);
272 					}
273 				} else {
274 					while (codelen-- > 0) setpix(codecode);
275 				}
276 			}
277 		} else if (compression == 2) {
278 			throw new Exception("4RLE for bitmaps aren't supported yet");
279 		} else {
280 			for(int y = height; y > 0; y--) {
281 				if (rdheight < 0) offsetStart -= width * bytesPerPixel;
282 				int offset = offsetStart;
283 				while (bytesRead%4 != 0) {
284 					read1();
285 					++bytesRead;
286 				}
287 				bytesRead = 0;
288 
289 				for(int x = 0; x < width; x++) {
290 					auto b = read1();
291 					++bytesRead;
292 					if(bitsPerPixel == 8) {
293 						img.data[offset++] = b;
294 					} else if(bitsPerPixel == 4) {
295 						img.data[offset++] = (b&0xf0) >> 4;
296 						x++;
297 						if(offset == img.data.length)
298 							break;
299 						img.data[offset++] = (b&0x0f);
300 					} else if(bitsPerPixel == 2) {
301 						img.data[offset++] = (b & 0b11000000) >> 6;
302 						x++;
303 						if(offset == img.data.length)
304 							break;
305 						img.data[offset++] = (b & 0b00110000) >> 4;
306 						x++;
307 						if(offset == img.data.length)
308 							break;
309 						img.data[offset++] = (b & 0b00001100) >> 2;
310 						x++;
311 						if(offset == img.data.length)
312 							break;
313 						img.data[offset++] = (b & 0b00000011) >> 0;
314 					} else if(bitsPerPixel == 1) {
315 						foreach(lol; 0 .. 8) {
316 							img.data[offset++] = (b & (1 << lol)) >> (7 - lol);
317 							x++;
318 							if(offset == img.data.length)
319 								break;
320 						}
321 						x--; // we do this once too many times in the loop
322 					} else assert(0);
323 					// I don't think these happen in the wild but I could be wrong, my bmp knowledge is somewhat outdated
324 				}
325 				if (rdheight > 0) offsetStart += width * bytesPerPixel;
326 			}
327 		}
328 
329 		return img;
330 	} else {
331 		if (compression != 0) throw new Exception("invalid bitmap compression");
332 		// true color image
333 		auto img = new TrueColorImage(width, height);
334 
335 		// no palette, so straight into the data
336 		int offsetStart = width * height * 4;
337 		int bytesPerPixel = 4;
338 		for(int y = height; y > 0; y--) {
339 			offsetStart -= width * bytesPerPixel;
340 			int offset = offsetStart;
341 			int b = 0;
342 			foreach(x; 0 .. width) {
343 				if(compression == 3) {
344 					ubyte[8] buffer;
345 					assert(bitsPerPixel / 8 < 8);
346 					foreach(lol; 0 .. bitsPerPixel / 8) {
347 						if(lol >= buffer.length)
348 							throw new Exception("wtf");
349 						buffer[lol] = read1();
350 						b++;
351 					}
352 
353 					ulong data = *(cast(ulong*) buffer.ptr);
354 
355 					auto blue = data & blueMask;
356 					auto green = data & greenMask;
357 					auto red = data & redMask;
358 					auto alpha = data & alphaMask;
359 
360 					if(blueMask)
361 						blue = blue * 255 / blueMask;
362 					if(greenMask)
363 						green = green * 255 / greenMask;
364 					if(redMask)
365 						red = red * 255 / redMask;
366 					if(alphaMask)
367 						alpha = alpha * 255 / alphaMask;
368 					else
369 						alpha = 255;
370 
371 					img.imageData.bytes[offset + 2] = cast(ubyte) blue;
372 					img.imageData.bytes[offset + 1] = cast(ubyte) green;
373 					img.imageData.bytes[offset + 0] = cast(ubyte) red;
374 					img.imageData.bytes[offset + 3] = cast(ubyte) alpha;
375 				} else {
376 					assert(compression == 0);
377 
378 					if(bitsPerPixel == 24 || bitsPerPixel == 32) {
379 						img.imageData.bytes[offset + 2] = read1(); // b
380 						img.imageData.bytes[offset + 1] = read1(); // g
381 						img.imageData.bytes[offset + 0] = read1(); // r
382 						if(bitsPerPixel == 32) {
383 							img.imageData.bytes[offset + 3] = read1(); // a
384 							b++;
385 						} else {
386 							img.imageData.bytes[offset + 3] = 255; // a
387 						}
388 						b += 3;
389 					} else {
390 						assert(bitsPerPixel == 16);
391 						// these are stored xrrrrrgggggbbbbb
392 						ushort d = read1();
393 						d |= cast(ushort)read1() << 8;
394 							// we expect 8 bit numbers but these only give 5 bits of info,
395 							// therefore we shift left 3 to get the right stuff.
396 						img.imageData.bytes[offset + 0] = (d & 0b0111110000000000) >> (10-3);
397 						img.imageData.bytes[offset + 1] = (d & 0b0000001111100000) >> (5-3);
398 						img.imageData.bytes[offset + 2] = (d & 0b0000000000011111) << 3;
399 						img.imageData.bytes[offset + 3] = 255; // r
400 						b += 2;
401 					}
402 				}
403 
404 				offset += bytesPerPixel;
405 			}
406 
407 			int w = b%4;
408 			if(w)
409 			for(int a = 0; a < 4-w; a++)
410 				read1(); // pad until divisible by four
411 		}
412 
413 
414 		return img;
415 	}
416 
417 	assert(0);
418 }
419 
420 /// Writes the `img` out to `filename`, in .bmp format. Writes [TrueColorImage] out
421 /// as a 24 bmp and [IndexedImage] out as an 8 bit bmp. Drops transparency information.
422 void writeBmp(MemoryImage img, string filename) {
423 	import core.stdc.stdio;
424 	FILE* fp = fopen((filename ~ "\0").ptr, "wb".ptr);
425 	if(fp is null)
426 		throw new Exception("can't open save file");
427 	scope(exit) fclose(fp);
428 
429 	int written;
430 	void my_fwrite(ubyte b) {
431 		written++;
432 		fputc(b, fp);
433 	}
434 
435 	writeBmpIndirect(img, &my_fwrite, true);
436 }
437 
438 /+
439 void main() {
440 	import arsd.simpledisplay;
441 	//import std.file;
442 	//auto img = readBmp(cast(ubyte[]) std.file.read("/home/me/test2.bmp"));
443 	auto img = readBmp("/home/me/test2.bmp");
444 	import std.stdio;
445 	writeln((cast(Object)img).toString());
446 	displayImage(Image.fromMemoryImage(img));
447 	//img.writeBmp("/home/me/test2.bmp");
448 }
449 +/
450 
451 void writeBmpIndirect(MemoryImage img, scope void delegate(ubyte) fwrite, bool prependFileHeader) {
452 
453 	void write4(uint what){
454 		fwrite(what & 0xff);
455 		fwrite((what >> 8) & 0xff);
456 		fwrite((what >> 16) & 0xff);
457 		fwrite((what >> 24) & 0xff);
458 	}
459 	void write2(ushort what){
460 		fwrite(what & 0xff);
461 		fwrite(what >> 8);
462 	}
463 	void write1(ubyte what) { fwrite(what); }
464 
465 	int width = img.width;
466 	int height = img.height;
467 	ushort bitsPerPixel;
468 
469 	ubyte[] data;
470 	Color[] palette;
471 
472 	// FIXME we should be able to write RGBA bitmaps too, though it seems like not many
473 	// programs correctly read them!
474 
475 	if(auto tci = cast(TrueColorImage) img) {
476 		bitsPerPixel = 24;
477 		data = tci.imageData.bytes;
478 		// we could also realistically do 16 but meh
479 	} else if(auto pi = cast(IndexedImage) img) {
480 		// FIXME: implement other bpps for more efficiency
481 		/*
482 		if(pi.palette.length == 2)
483 			bitsPerPixel = 1;
484 		else if(pi.palette.length <= 16)
485 			bitsPerPixel = 4;
486 		else
487 		*/
488 			bitsPerPixel = 8;
489 		data = pi.data;
490 		palette = pi.palette;
491 	} else throw new Exception("I can't save this image type " ~ img.classinfo.name);
492 
493 	ushort offsetToBits;
494 	if(bitsPerPixel == 8)
495 		offsetToBits = 1078;
496 	else if (bitsPerPixel == 24 || bitsPerPixel == 16)
497 		offsetToBits = 54;
498 	else
499 		offsetToBits = cast(ushort)(54 * (1 << bitsPerPixel)); // room for the palette...
500 
501 	uint fileSize = offsetToBits;
502 	if(bitsPerPixel == 8) {
503 		fileSize += height * (width + width%4);
504 	} else if(bitsPerPixel == 24)
505 		fileSize += height * ((width * 3) + (!((width*3)%4) ? 0 : 4-((width*3)%4)));
506 	else assert(0, "not implemented"); // FIXME
507 
508 	if(prependFileHeader) {
509 		write1('B');
510 		write1('M');
511 
512 		write4(fileSize); // size of file in bytes
513 		write2(0); 	// reserved
514 		write2(0); 	// reserved
515 		write4(offsetToBits); // offset to the bitmap data
516 	}
517 
518 	write4(40); // size of BITMAPINFOHEADER
519 
520 	write4(width); // width
521 	write4(height); // height
522 
523 	write2(1); // planes
524 	write2(bitsPerPixel); // bpp
525 	write4(0); // compression
526 	write4(0); // size of uncompressed
527 	write4(0); // x pels per meter
528 	write4(0); // y pels per meter
529 	write4(0); // colors used
530 	write4(0); // colors important
531 
532 	// And here we write the palette
533 	if(bitsPerPixel <= 8)
534 		foreach(c; palette[0..(1 << bitsPerPixel)]){
535 			write1(c.b);
536 			write1(c.g);
537 			write1(c.r);
538 			write1(0);
539 		}
540 
541 	// And finally the data
542 
543 	int bytesPerPixel;
544 	if(bitsPerPixel == 8)
545 		bytesPerPixel = 1;
546 	else if(bitsPerPixel == 24)
547 		bytesPerPixel = 4;
548 	else assert(0, "not implemented"); // FIXME
549 
550 	int offsetStart = cast(int) data.length;
551 	for(int y = height; y > 0; y--) {
552 		offsetStart -= width * bytesPerPixel;
553 		int offset = offsetStart;
554 		int b = 0;
555 		foreach(x; 0 .. width) {
556 			if(bitsPerPixel == 8) {
557 				write1(data[offset]);
558 				b++;
559 			} else if(bitsPerPixel == 24) {
560 				write1(data[offset + 2]); // blue
561 				write1(data[offset + 1]); // green
562 				write1(data[offset + 0]); // red
563 				b += 3;
564 			} else assert(0); // FIXME
565 			offset += bytesPerPixel;
566 		}
567 
568 		int w = b%4;
569 		if(w)
570 		for(int a = 0; a < 4-w; a++)
571 			write1(0); // pad until divisible by four
572 	}
573 }
Suggestion Box / Bug Report