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 if(lookForFileHeader) { 116 require1('B'); 117 require1('M'); 118 119 auto fileSize = read4(); // size of file in bytes 120 require2(0); // reserved 121 require2(0); // reserved 122 123 auto offsetToBits = read4(); 124 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("pixel data offset: 0x%08x\n", cast(uint)offsetToBits); } 125 } 126 127 auto sizeOfBitmapInfoHeader = read4(); 128 if (sizeOfBitmapInfoHeader < 12) throw new Exception("invalid bitmap header size"); 129 130 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size of bitmap info header: %d\n", cast(uint)sizeOfBitmapInfoHeader); } 131 132 int width, height, rdheight; 133 134 if (sizeOfBitmapInfoHeader == 12) { 135 width = read2(); 136 rdheight = cast(short)read2(); 137 } else { 138 if (sizeOfBitmapInfoHeader < 16) throw new Exception("invalid bitmap header size"); 139 sizeOfBitmapInfoHeader -= 4; // hack! 140 width = readLONG(); 141 rdheight = cast(int)readLONG(); 142 } 143 144 height = (rdheight < 0 ? -rdheight : rdheight); 145 146 if(hasAndMask) { 147 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("has and mask so height slashed %d\n", height / 2); } 148 height = height / 2; 149 } 150 151 rdheight = (rdheight < 0 ? 1 : -1); // so we can use it as delta (note the inverted sign) 152 153 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("size: %dx%d\n", cast(int)width, cast(int) height); } 154 if (width < 1 || height < 1) throw new Exception("invalid bitmap dimensions"); 155 156 require2(1); // planes 157 158 auto bitsPerPixel = read2(); 159 switch (bitsPerPixel) { 160 case 1: case 2: case 4: case 8: case 16: case 24: case 32: break; 161 default: throw new Exception("invalid bitmap depth"); 162 } 163 164 /* 165 0 = BI_RGB 166 1 = BI_RLE8 RLE 8-bit/pixel Can be used only with 8-bit/pixel bitmaps 167 2 = BI_RLE4 RLE 4-bit/pixel Can be used only with 4-bit/pixel bitmaps 168 3 = BI_BITFIELDS 169 */ 170 uint compression = 0; 171 uint sizeOfUncompressedData = 0; 172 uint xPixelsPerMeter = 0; 173 uint yPixelsPerMeter = 0; 174 uint colorsUsed = 0; 175 uint colorsImportant = 0; 176 177 sizeOfBitmapInfoHeader -= 12; 178 if (sizeOfBitmapInfoHeader > 0) { 179 if (sizeOfBitmapInfoHeader < 6*4) throw new Exception("invalid bitmap header size"); 180 sizeOfBitmapInfoHeader -= 6*4; 181 compression = read4(); 182 sizeOfUncompressedData = read4(); 183 xPixelsPerMeter = readLONG(); 184 yPixelsPerMeter = readLONG(); 185 colorsUsed = read4(); 186 colorsImportant = read4(); 187 } 188 189 if (compression > 3) throw new Exception("invalid bitmap compression"); 190 if (compression == 1 && bitsPerPixel != 8) throw new Exception("invalid bitmap compression"); 191 if (compression == 2 && bitsPerPixel != 4) throw new Exception("invalid bitmap compression"); 192 193 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("compression: %u; bpp: %u\n", compression, cast(uint)bitsPerPixel); } 194 195 uint redMask; 196 uint greenMask; 197 uint blueMask; 198 uint alphaMask; 199 if (compression == 3) { 200 if (sizeOfBitmapInfoHeader < 4*4) throw new Exception("invalid bitmap compression"); 201 sizeOfBitmapInfoHeader -= 4*4; 202 redMask = read4(); 203 greenMask = read4(); 204 blueMask = read4(); 205 alphaMask = read4(); 206 } 207 // FIXME: we could probably handle RLE4 as well 208 209 // I don't know about the rest of the header, so I'm just skipping it. 210 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("header bytes left: %u\n", cast(uint)sizeOfBitmapInfoHeader); } 211 foreach (skip; 0..sizeOfBitmapInfoHeader) read1(); 212 213 headerRead = true; 214 215 216 217 // the dg returns the change in offset 218 void processAndMask(scope int delegate(int x, int y, bool transparent) apply) { 219 try { 220 // the and mask is always 1bpp and i want to translate it into transparent pixels 221 222 for(int y = (height - 1); y >= 0; y--) { 223 //version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" reading and mask %d\n", y); } 224 int read; 225 for(int x = 0; x < width; x++) { 226 const b = read1(); 227 //import std.stdio; writefln("%02x", b); 228 read++; 229 foreach_reverse(lol; 0 .. 8) { 230 bool transparent = !!((b & (1 << lol))); 231 version(arsd_debug_bitmap_loader) { import std.stdio; write(transparent ? "o":"x"); } 232 apply(x, y, transparent); 233 234 x++; 235 if(x >= width) 236 break; 237 } 238 x--; // we do this once too many times in the loop 239 } 240 while(read % 4) { 241 read1(); 242 read++; 243 } 244 version(arsd_debug_bitmap_loader) {import std.stdio; writeln(""); } 245 } 246 247 /+ 248 this the algorithm btw 249 keep.imageData.bytes[] &= tci.imageData.bytes[andOffset .. $]; 250 keep.imageData.bytes[] ^= tci.imageData.bytes[0 .. andOffset]; 251 +/ 252 } catch(Exception e) { 253 // discard; the and mask is optional in practice since using all 0's 254 // gives a result and some files in the wild deliberately truncate the 255 // file (though they aren't supposed to....) expecting readers to do this. 256 version(arsd_debug_bitmap_loader) { import std.stdio; writeln(e); } 257 } 258 } 259 260 261 262 if(bitsPerPixel <= 8) { 263 // indexed image 264 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("colorsUsed=%u; colorsImportant=%u\n", colorsUsed, colorsImportant); } 265 if (colorsUsed == 0 || colorsUsed > (1 << bitsPerPixel)) colorsUsed = (1 << bitsPerPixel); 266 auto img = new IndexedImage(width, height); 267 img.palette.reserve(1 << bitsPerPixel); 268 269 foreach(idx; 0 .. /*(1 << bitsPerPixel)*/colorsUsed) { 270 auto b = read1(); 271 auto g = read1(); 272 auto r = read1(); 273 auto reserved = read1(); 274 275 img.palette ~= Color(r, g, b); 276 } 277 while (img.palette.length < (1 << bitsPerPixel)) img.palette ~= Color.transparent; 278 279 // and the data 280 int bytesPerPixel = 1; 281 auto offsetStart = (rdheight > 0 ? 0 : width * height * bytesPerPixel); 282 int bytesRead = 0; 283 284 if (compression == 1) { 285 // this is complicated 286 assert(bitsPerPixel == 8); // always 287 int x = 0, y = (rdheight > 0 ? 0 : height-1); 288 void setpix (int v) { 289 if (x >= 0 && y >= 0 && x < width && y < height) img.data.ptr[y*width+x] = v&0xff; 290 ++x; 291 } 292 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf("width=%d; height=%d; rdheight=%d\n", width, height, rdheight); } 293 for (;;) { 294 ubyte codelen = read1(); 295 ubyte codecode = read1(); 296 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); } 297 bytesRead += 2; 298 if (codelen == 0) { 299 // special code 300 if (codecode == 0) { 301 // end of line 302 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" EOL\n"); } 303 while (x < width) setpix(1); 304 x = 0; 305 y += rdheight; 306 if (y < 0 || y >= height) break; // ooops 307 } else if (codecode == 1) { 308 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" EOB\n"); } 309 // end of bitmap 310 break; 311 } else if (codecode == 2) { 312 // delta 313 int xofs = read1(); 314 int yofs = read1(); 315 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" deltax=%d; deltay=%d\n", xofs, yofs); } 316 bytesRead += 2; 317 x += xofs; 318 y += yofs*rdheight; 319 if (y < 0 || y >= height) break; // ooops 320 } else { 321 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" LITERAL: %u\n", cast(uint)codecode); } 322 // literal copy 323 while (codecode-- > 0) { 324 setpix(read1()); 325 ++bytesRead; 326 } 327 version(arsd_debug_bitmap_loader) if (bytesRead%2) { import core.stdc.stdio; printf(" LITERAL SKIP\n"); } 328 if (bytesRead%2) { read1(); ++bytesRead; } 329 assert(bytesRead%2 == 0); 330 } 331 } else { 332 while (codelen-- > 0) setpix(codecode); 333 } 334 } 335 } else if (compression == 2) { 336 throw new Exception("4RLE for bitmaps aren't supported yet"); 337 } else { 338 for(int y = height; y > 0; y--) { 339 if (rdheight < 0) offsetStart -= width * bytesPerPixel; 340 int offset = offsetStart; 341 while (bytesRead%4 != 0) { 342 read1(); 343 ++bytesRead; 344 } 345 bytesRead = 0; 346 347 for(int x = 0; x < width; x++) { 348 auto b = read1(); 349 ++bytesRead; 350 if(bitsPerPixel == 8) { 351 img.data[offset++] = b; 352 } else if(bitsPerPixel == 4) { 353 img.data[offset++] = (b&0xf0) >> 4; 354 x++; 355 if(offset == img.data.length) 356 break; 357 img.data[offset++] = (b&0x0f); 358 } else if(bitsPerPixel == 2) { 359 img.data[offset++] = (b & 0b11000000) >> 6; 360 x++; 361 if(offset == img.data.length) 362 break; 363 img.data[offset++] = (b & 0b00110000) >> 4; 364 x++; 365 if(offset == img.data.length) 366 break; 367 img.data[offset++] = (b & 0b00001100) >> 2; 368 x++; 369 if(offset == img.data.length) 370 break; 371 img.data[offset++] = (b & 0b00000011) >> 0; 372 } else if(bitsPerPixel == 1) { 373 foreach_reverse(lol; 0 .. 8) { 374 bool value = !!((b & (1 << lol))); 375 img.data[offset++] = value ? 1 : 0; 376 x++; 377 if(offset == img.data.length) 378 break; 379 } 380 x--; // we do this once too many times in the loop 381 } else assert(0); 382 // I don't think these happen in the wild but I could be wrong, my bmp knowledge is somewhat outdated 383 } 384 if (rdheight > 0) offsetStart += width * bytesPerPixel; 385 } 386 } 387 388 if(hasAndMask) { 389 auto tp = img.palette.length; 390 if(tp < 256) { 391 // easy, there's room, just add an entry. 392 img.palette ~= Color.transparent; 393 img.hasAlpha = true; 394 } else { 395 // not enough room, gotta try to find something unused to overwrite... 396 // FIXME: could prolly use more caution here 397 auto selection = 39; 398 399 img.palette[selection] = Color.transparent; 400 img.hasAlpha = true; 401 tp = selection; 402 } 403 404 if(tp < 256) { 405 processAndMask(delegate int(int x, int y, bool transparent) { 406 auto existing = img.data[y * img.width + x]; 407 408 if(img.palette[existing] == Color.black && transparent) { 409 // import std.stdio; write("O"); 410 img.data[y * img.width + x] = cast(ubyte) tp; 411 } else { 412 // import std.stdio; write("X"); 413 } 414 415 return 1; 416 }); 417 } else { 418 //import std.stdio; writeln("no room in palette for transparency alas"); 419 } 420 } 421 422 return img; 423 } else { 424 if (compression != 0) throw new Exception("invalid bitmap compression"); 425 // true color image 426 auto img = new TrueColorImage(width, height); 427 428 // no palette, so straight into the data 429 int offsetStart = width * height * 4; 430 int bytesPerPixel = 4; 431 for(int y = height; y > 0; y--) { 432 version(arsd_debug_bitmap_loader) { import core.stdc.stdio; printf(" true color image: %d\n", y); } 433 offsetStart -= width * bytesPerPixel; 434 int offset = offsetStart; 435 int b = 0; 436 foreach(x; 0 .. width) { 437 if(compression == 3) { 438 ubyte[8] buffer; 439 assert(bitsPerPixel / 8 < 8); 440 foreach(lol; 0 .. bitsPerPixel / 8) { 441 if(lol >= buffer.length) 442 throw new Exception("wtf"); 443 buffer[lol] = read1(); 444 b++; 445 } 446 447 ulong data = *(cast(ulong*) buffer.ptr); 448 449 auto blue = data & blueMask; 450 auto green = data & greenMask; 451 auto red = data & redMask; 452 auto alpha = data & alphaMask; 453 454 if(blueMask) 455 blue = blue * 255 / blueMask; 456 if(greenMask) 457 green = green * 255 / greenMask; 458 if(redMask) 459 red = red * 255 / redMask; 460 if(alphaMask) 461 alpha = alpha * 255 / alphaMask; 462 else 463 alpha = 255; 464 465 img.imageData.bytes[offset + 2] = cast(ubyte) blue; 466 img.imageData.bytes[offset + 1] = cast(ubyte) green; 467 img.imageData.bytes[offset + 0] = cast(ubyte) red; 468 img.imageData.bytes[offset + 3] = cast(ubyte) alpha; 469 } else { 470 assert(compression == 0); 471 472 if(bitsPerPixel == 24 || bitsPerPixel == 32) { 473 img.imageData.bytes[offset + 2] = read1(); // b 474 img.imageData.bytes[offset + 1] = read1(); // g 475 img.imageData.bytes[offset + 0] = read1(); // r 476 if(bitsPerPixel == 32) { 477 img.imageData.bytes[offset + 3] = read1(); // a 478 b++; 479 } else { 480 img.imageData.bytes[offset + 3] = 255; // a 481 } 482 b += 3; 483 } else { 484 assert(bitsPerPixel == 16); 485 // these are stored xrrrrrgggggbbbbb 486 ushort d = read1(); 487 d |= cast(ushort)read1() << 8; 488 // we expect 8 bit numbers but these only give 5 bits of info, 489 // therefore we shift left 3 to get the right stuff. 490 img.imageData.bytes[offset + 0] = (d & 0b0111110000000000) >> (10-3); 491 img.imageData.bytes[offset + 1] = (d & 0b0000001111100000) >> (5-3); 492 img.imageData.bytes[offset + 2] = (d & 0b0000000000011111) << 3; 493 img.imageData.bytes[offset + 3] = 255; // r 494 b += 2; 495 } 496 } 497 498 offset += bytesPerPixel; 499 } 500 501 int w = b%4; 502 if(w) 503 for(int a = 0; a < 4-w; a++) 504 read1(); // pad until divisible by four 505 } 506 507 if(hasAndMask) { 508 processAndMask(delegate int(int x, int y, bool transparent) { 509 int offset = (y * img.width + x) * 4; 510 auto existing = img.imageData.bytes[offset + 3]; 511 // only use the and mask if the alpha channel appears unused 512 if(transparent && existing == 255) 513 img.imageData.bytes[offset + 3] = 0; 514 //import std.stdio; write(transparent ? "o":"x"); 515 516 return 4; 517 }); 518 } 519 520 521 return img; 522 } 523 524 assert(0); 525 } 526 527 /// Writes the `img` out to `filename`, in .bmp format. Writes [TrueColorImage] out 528 /// as a 24 bmp and [IndexedImage] out as an 8 bit bmp. Drops transparency information. 529 void writeBmp(MemoryImage img, string filename) { 530 import core.stdc.stdio; 531 FILE* fp = fopen((filename ~ "\0").ptr, "wb".ptr); 532 if(fp is null) 533 throw new Exception("can't open save file"); 534 scope(exit) fclose(fp); 535 536 int written; 537 void my_fwrite(ubyte b) { 538 written++; 539 fputc(b, fp); 540 } 541 542 writeBmpIndirect(img, &my_fwrite, true); 543 } 544 545 /+ 546 void main() { 547 import arsd.simpledisplay; 548 //import std.file; 549 //auto img = readBmp(cast(ubyte[]) std.file.read("/home/me/test2.bmp")); 550 auto img = readBmp("/home/me/test2.bmp"); 551 import std.stdio; 552 writeln((cast(Object)img).toString()); 553 displayImage(Image.fromMemoryImage(img)); 554 //img.writeBmp("/home/me/test2.bmp"); 555 } 556 +/ 557 558 /++ 559 Writes a bitmap file to a delegate, byte by byte, with data from the given image. 560 561 If `prependFileHeader` is `true`, it will add the bitmap file header too. 562 +/ 563 void writeBmpIndirect(MemoryImage img, scope void delegate(ubyte) fwrite, bool prependFileHeader) { 564 565 void write4(uint what){ 566 fwrite(what & 0xff); 567 fwrite((what >> 8) & 0xff); 568 fwrite((what >> 16) & 0xff); 569 fwrite((what >> 24) & 0xff); 570 } 571 void write2(ushort what){ 572 fwrite(what & 0xff); 573 fwrite(what >> 8); 574 } 575 void write1(ubyte what) { fwrite(what); } 576 577 int width = img.width; 578 int height = img.height; 579 ushort bitsPerPixel; 580 581 ubyte[] data; 582 Color[] palette; 583 584 // FIXME we should be able to write RGBA bitmaps too, though it seems like not many 585 // programs correctly read them! 586 587 if(auto tci = cast(TrueColorImage) img) { 588 bitsPerPixel = 24; 589 data = tci.imageData.bytes; 590 // we could also realistically do 16 but meh 591 } else if(auto pi = cast(IndexedImage) img) { 592 // FIXME: implement other bpps for more efficiency 593 /* 594 if(pi.palette.length == 2) 595 bitsPerPixel = 1; 596 else if(pi.palette.length <= 16) 597 bitsPerPixel = 4; 598 else 599 */ 600 bitsPerPixel = 8; 601 data = pi.data; 602 palette = pi.palette; 603 } else throw new Exception("I can't save this image type " ~ img.classinfo.name); 604 605 ushort offsetToBits; 606 if(bitsPerPixel == 8) 607 offsetToBits = 1078; 608 else if (bitsPerPixel == 24 || bitsPerPixel == 16) 609 offsetToBits = 54; 610 else 611 offsetToBits = cast(ushort)(54 * (1 << bitsPerPixel)); // room for the palette... 612 613 uint fileSize = offsetToBits; 614 if(bitsPerPixel == 8) { 615 fileSize += height * (width + width%4); 616 } else if(bitsPerPixel == 24) 617 fileSize += height * ((width * 3) + (!((width*3)%4) ? 0 : 4-((width*3)%4))); 618 else assert(0, "not implemented"); // FIXME 619 620 if(prependFileHeader) { 621 write1('B'); 622 write1('M'); 623 624 write4(fileSize); // size of file in bytes 625 write2(0); // reserved 626 write2(0); // reserved 627 write4(offsetToBits); // offset to the bitmap data 628 } 629 630 write4(40); // size of BITMAPINFOHEADER 631 632 write4(width); // width 633 write4(height); // height 634 635 write2(1); // planes 636 write2(bitsPerPixel); // bpp 637 write4(0); // compression 638 write4(0); // size of uncompressed 639 write4(0); // x pels per meter 640 write4(0); // y pels per meter 641 write4(0); // colors used 642 write4(0); // colors important 643 644 // And here we write the palette 645 if(bitsPerPixel <= 8) 646 foreach(c; palette[0..(1 << bitsPerPixel)]){ 647 write1(c.b); 648 write1(c.g); 649 write1(c.r); 650 write1(0); 651 } 652 653 // And finally the data 654 655 int bytesPerPixel; 656 if(bitsPerPixel == 8) 657 bytesPerPixel = 1; 658 else if(bitsPerPixel == 24) 659 bytesPerPixel = 4; 660 else assert(0, "not implemented"); // FIXME 661 662 int offsetStart = cast(int) data.length; 663 for(int y = height; y > 0; y--) { 664 offsetStart -= width * bytesPerPixel; 665 int offset = offsetStart; 666 int b = 0; 667 foreach(x; 0 .. width) { 668 if(bitsPerPixel == 8) { 669 write1(data[offset]); 670 b++; 671 } else if(bitsPerPixel == 24) { 672 write1(data[offset + 2]); // blue 673 write1(data[offset + 1]); // green 674 write1(data[offset + 0]); // red 675 b += 3; 676 } else assert(0); // FIXME 677 offset += bytesPerPixel; 678 } 679 680 int w = b%4; 681 if(w) 682 for(int a = 0; a < 4-w; a++) 683 write1(0); // pad until divisible by four 684 } 685 }