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 }