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