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