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 }