1 /* 2 Copyright (c) 2014-2021 Timur Gafarov, Roman Chistokhodov 3 4 Boost Software License - Version 1.0 - August 17th, 2003 5 6 Permission is hereby granted, free of charge, to any person or organization 7 obtaining a copy of the software and accompanying documentation covered by 8 this license (the "Software") to use, reproduce, display, distribute, 9 execute, and transmit the Software, and to prepare derivative works of the 10 Software, and to permit third-parties to whom the Software is furnished to 11 do so, all subject to the following: 12 13 The copyright notices in the Software and this entire statement, including 14 the above license grant, this restriction and the following disclaimer, 15 must be included in all copies of the Software, in whole or in part, and 16 all derivative works of the Software, unless such copies or derivative 17 works are solely in the form of machine-executable object code generated by 18 a source language processor. 19 20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 23 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 24 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 25 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 DEALINGS IN THE SOFTWARE. 27 */ 28 29 /** 30 * Decode and encode BMP images 31 * 32 * Copyright: Timur Gafarov, Roman Chistokhodov 2014-2021. 33 * License: $(LINK2 boost.org/LICENSE_1_0.txt, Boost License 1.0). 34 * Authors: Timur Gafarov, Roman Chistokhodov 35 */ 36 module dlib.image.io.bmp; 37 38 import std.stdio; 39 import dlib.core.stream; 40 import dlib.core.memory; 41 import dlib.core.compound; 42 import dlib.image.image; 43 import dlib.image.color; 44 import dlib.image.io; 45 import dlib.image.io.utils; 46 import dlib.filesystem.local; 47 48 // uncomment this to see debug messages: 49 //version = BMPDebug; 50 51 static const ubyte[2] BMPMagic = ['B', 'M']; 52 53 struct BMPFileHeader 54 { 55 ubyte[2] type; // magic number "BM" 56 uint size; // file size 57 ushort reserved1; 58 ushort reserved2; 59 uint offset; // offset to image data 60 } 61 62 struct BMPInfoHeader 63 { 64 uint size; // size of bitmap info header 65 int width; // image width 66 int height; // image height 67 ushort planes; // must be equal to 1 68 ushort bitsPerPixel; // bits per pixel 69 uint compression; // compression type 70 uint imageSize; // size of pixel data 71 int xPixelsPerMeter; // pixels per meter on x-axis 72 int yPixelsPerMeter; // pixels per meter on y-axis 73 uint colorsUsed; // number of used colors 74 uint colorsImportant; // number of important colors 75 } 76 77 struct BMPCoreHeader 78 { 79 uint size; // size of bitmap core header 80 ushort width; // image with 81 ushort height; // image height 82 ushort planes; // must be equal to 1 83 ushort bitsPerPixel; // bits per pixel 84 } 85 86 struct BMPCoreInfo 87 { 88 BMPCoreHeader header; 89 ubyte[3] colors; 90 } 91 92 enum BMPOSType 93 { 94 Win, 95 OS2 96 } 97 98 // BMP compression type constants 99 enum BMPCompressionType 100 { 101 RGB = 0, 102 RLE8 = 1, 103 RLE4 = 2, 104 BitFields = 3 105 } 106 107 // RLE byte type constants 108 enum RLE 109 { 110 Command = 0, 111 EndOfLine = 0, 112 EndOfBitmap = 1, 113 Delta = 2 114 } 115 116 enum BMPInfoSize 117 { 118 OLD = 12, 119 WIN = 40, 120 OS2 = 64, 121 WIN4 = 108, 122 WIN5 = 124, 123 } 124 125 class BMPLoadException: ImageLoadException 126 { 127 this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable next = null) 128 { 129 super(msg, file, line, next); 130 } 131 } 132 133 private ubyte calculateShift(uint mask) nothrow pure 134 { 135 ubyte result = 0; 136 while (mask && !(mask & 1)) { 137 result++; 138 mask >>= 1; 139 } 140 return result; 141 } 142 143 unittest 144 { 145 assert(calculateShift(0xff) == 0); 146 assert(calculateShift(0xff00) == 8); 147 assert(calculateShift(0xff0000) == 16); 148 assert(calculateShift(0xff000000) == 24); 149 } 150 151 private ubyte applyMask(uint value, uint mask, ubyte shift, ubyte scale) nothrow pure 152 { 153 return cast(ubyte) (((value & mask) >> shift) * scale); 154 } 155 156 private ubyte calculateScale(uint mask, ubyte shift) nothrow pure 157 { 158 return cast(ubyte) (256 / calculateDivisor(mask, shift)); 159 } 160 161 private uint calculateDivisor(uint mask, ubyte shift) nothrow pure 162 { 163 return (mask >> shift) + 1; 164 } 165 166 private bool checkIndex(uint index, const(ubyte)[] colormap) nothrow pure { 167 return index + 2 < colormap.length; 168 } 169 170 /** 171 * Load BMP from file using local FileSystem. 172 * Causes GC allocation 173 */ 174 SuperImage loadBMP(string filename) 175 { 176 InputStream input = openForInput(filename); 177 178 try 179 { 180 return loadBMP(input); 181 } 182 catch (BMPLoadException ex) 183 { 184 throw new Exception("'" ~ filename ~ "' :" ~ ex.msg, ex.file, ex.line, ex.next); 185 } 186 finally 187 { 188 input.close(); 189 } 190 } 191 192 /** 193 * Load BMP from stream using default image factory. 194 * Causes GC allocation 195 */ 196 SuperImage loadBMP(InputStream istrm) 197 { 198 Compound!(SuperImage, string) res = 199 loadBMP(istrm, defaultImageFactory); 200 if (res[0] is null) 201 throw new BMPLoadException(res[1]); 202 else 203 return res[0]; 204 } 205 206 /** 207 * Load BMP from stream using specified image factory. 208 * GC-free 209 */ 210 Compound!(SuperImage, string) loadBMP( 211 InputStream istrm, 212 SuperImageFactory imgFac) 213 { 214 SuperImage img = null; 215 216 BMPFileHeader bmpfh; 217 BMPInfoHeader bmpih; 218 BMPCoreHeader bmpch; 219 220 BMPOSType osType; 221 222 uint compression; 223 uint bitsPerPixel; 224 225 uint redMask, greenMask, blueMask, alphaMask; 226 227 ubyte[] colormap; 228 int colormapSize; 229 230 Compound!(SuperImage, string) error(string errorMsg) 231 { 232 if (img) 233 { 234 img.free(); 235 img = null; 236 } 237 if (colormap.length) 238 Delete(colormap); 239 return compound(img, errorMsg); 240 } 241 242 bmpfh = readStruct!BMPFileHeader(istrm); 243 244 auto bmphPos = istrm.position; 245 246 version(BMPDebug) 247 { 248 writefln("bmpfh.type = %s", cast(char[])bmpfh.type); 249 writefln("bmpfh.size = %s", bmpfh.size); 250 writefln("bmpfh.reserved1 = %s", bmpfh.reserved1); 251 writefln("bmpfh.reserved2 = %s", bmpfh.reserved2); 252 writefln("bmpfh.offset = %s", bmpfh.offset); 253 writeln("-------------------"); 254 } 255 256 if (bmpfh.type != BMPMagic) 257 return error("loadBMP error: input data is not BMP"); 258 259 uint numChannels = 3; 260 uint width, height; 261 262 bmpih = readStruct!BMPInfoHeader(istrm); 263 264 version(BMPDebug) 265 { 266 writefln("bmpih.size = %s", bmpih.size); 267 writefln("bmpih.width = %s", bmpih.width); 268 writefln("bmpih.height = %s", bmpih.height); 269 writefln("bmpih.planes = %s", bmpih.planes); 270 writefln("bmpih.bitsPerPixel = %s", bmpih.bitsPerPixel); 271 writefln("bmpih.compression = %s", bmpih.compression); 272 writefln("bmpih.imageSize = %s", bmpih.imageSize); 273 writefln("bmpih.xPixelsPerMeter = %s", bmpih.xPixelsPerMeter); 274 writefln("bmpih.yPixelsPerMeter = %s", bmpih.yPixelsPerMeter); 275 writefln("bmpih.colorsUsed = %s", bmpih.colorsUsed); 276 writefln("bmpih.colorsImportant = %s", bmpih.colorsImportant); 277 writeln("-------------------"); 278 } 279 280 if (bmpih.compression > 3) 281 { 282 /* 283 * This is an OS/2 bitmap file, we don't use 284 * bitmap info header but bitmap core header instead 285 */ 286 287 // We must go back to read bitmap core header 288 istrm.position = bmphPos; 289 bmpch = readStruct!BMPCoreHeader(istrm); 290 291 osType = BMPOSType.OS2; 292 compression = BMPCompressionType.RGB; 293 bitsPerPixel = bmpch.bitsPerPixel; 294 295 width = bmpch.width; 296 height = bmpch.height; 297 } 298 else 299 { 300 // Windows style 301 osType = BMPOSType.Win; 302 compression = bmpih.compression; 303 bitsPerPixel = bmpih.bitsPerPixel; 304 305 width = bmpih.width; 306 height = bmpih.height; 307 } 308 309 version(BMPDebug) 310 { 311 writefln("osType = %s", [BMPOSType.OS2: "OS/2", BMPOSType.Win: "Windows"][osType]); 312 writefln("width = %s", width); 313 writefln("height = %s", height); 314 writefln("bitsPerPixel = %s", bitsPerPixel); 315 writefln("compression = %s", compression); 316 writeln("-------------------"); 317 } 318 319 if (bmpih.size >= BMPInfoSize.WIN4 || (compression == BMPCompressionType.BitFields && (bitsPerPixel == 16 || bitsPerPixel == 32))) { 320 bool ok = true; 321 ok = ok && istrm.readLE(&redMask); 322 ok = ok && istrm.readLE(&greenMask); 323 ok = ok && istrm.readLE(&blueMask); 324 325 version(BMPDebug) { 326 writeln("File has bitfields masks"); 327 writefln("redMask = %#x", redMask); 328 writefln("greenMask = %#x", greenMask); 329 writefln("blueMask = %#x", blueMask); 330 writeln("-------------------"); 331 } 332 333 if (ok && bmpih.size >= BMPInfoSize.WIN4) { 334 version(BMPDebug) { 335 writeln("File is at least version 4"); 336 } 337 338 int CSType; 339 int[9] coords; 340 int gammaRed; 341 int gammaGreen; 342 int gammaBlue; 343 344 ok = ok && istrm.readLE(&alphaMask); 345 ok = ok && istrm.readLE(&CSType); 346 istrm.fillArray(coords); 347 ok = ok && istrm.readLE(&gammaRed); 348 ok = ok && istrm.readLE(&gammaGreen); 349 ok = ok && istrm.readLE(&gammaBlue); 350 351 if (ok && bmpih.size >= BMPInfoSize.WIN5) { 352 version(BMPDebug) { 353 writeln("File is at least version 5"); 354 } 355 356 int intent; 357 int profileData; 358 int profileSize; 359 int reserved; 360 361 ok = ok && istrm.readLE(&intent); 362 ok = ok && istrm.readLE(&profileData); 363 ok = ok && istrm.readLE(&profileSize); 364 ok = ok && istrm.readLE(&reserved); 365 } 366 } 367 if (!ok) { 368 return error("loadBMP error: failed to read data of size specified in bmp info structure"); 369 } 370 } 371 372 if (compression != BMPCompressionType.RGB && compression != BMPCompressionType.BitFields && compression != BMPCompressionType.RLE8) { 373 return error("loadBMP error: unsupported compression type (RLE4 is not supported yet)"); 374 } 375 376 if (bitsPerPixel != 4 && bitsPerPixel != 8 && bitsPerPixel != 16 && bitsPerPixel != 24 && bitsPerPixel != 32) { 377 return error("loadBMP error: unsupported color depth"); 378 } 379 380 uint numberOfColors; 381 ubyte colormapEntrySize = (osType == BMPOSType.OS2)? 3 : 4; 382 383 ubyte blueShift, greenShift, redShift, alphaShift; 384 ubyte blueScale = 1, greenScale = 1, redScale = 1, alphaScale; 385 386 if (bitsPerPixel == 8 || bitsPerPixel == 4) 387 { 388 numberOfColors = bmpih.colorsUsed ? bmpih.colorsUsed : (1 << bitsPerPixel); 389 if (numberOfColors == 0 || numberOfColors > 256) { 390 return error("loadBMP error: strange number of used colors"); 391 } 392 } else if (compression == BMPCompressionType.BitFields && (bitsPerPixel == 16 || bitsPerPixel == 32)) { 393 redShift = calculateShift(redMask); 394 greenShift = calculateShift(greenMask); 395 blueShift = calculateShift(blueMask); 396 alphaShift = calculateShift(alphaMask); 397 398 version(BMPDebug) { 399 writefln("redShift = %#x", redShift); 400 writefln("greenShift = %#x", greenShift); 401 writefln("blueShift = %#x", blueShift); 402 writefln("alphaShift = %#x", alphaShift); 403 } 404 405 //scales are used to get equivalent weights for every color channel fit in byte 406 407 if (calculateDivisor(redMask, redShift) == 0 || calculateDivisor(greenMask, greenShift) == 0 408 || calculateDivisor(blueMask, blueShift) == 0 || calculateDivisor(alphaMask, alphaShift) == 0 409 ) { 410 return error("loadBMP error: division by zero when calculating scale"); 411 } 412 413 redScale = calculateScale(redMask, redShift); 414 greenScale = calculateScale(greenMask, greenShift); 415 blueScale = calculateScale(blueMask, blueShift); 416 alphaScale = calculateScale(alphaMask, alphaShift); 417 418 version(BMPDebug) { 419 writefln("redScale = %#x", redScale); 420 writefln("greenScale = %#x", greenScale); 421 writefln("blueScale = %#x", blueScale); 422 writefln("alphaScale = %#x", alphaScale); 423 } 424 425 } else if (compression == BMPCompressionType.RGB && (bitsPerPixel == 24 || bitsPerPixel == 32)) { 426 blueMask = 0x000000ff; 427 greenMask = 0x0000ff00; 428 redMask = 0x00ff0000; 429 blueShift = 0; 430 greenShift = 8; 431 redShift = 16; 432 } else if (compression == BMPCompressionType.RGB && bitsPerPixel == 16) { 433 blueMask = 0x001f; 434 greenMask = 0x03e0; 435 redMask = 0x7c00; 436 blueShift = 0; 437 greenShift = 2; 438 redShift = 7; 439 440 blueScale = 8; 441 } else { 442 return error("loadBMP error: unknown compression type / color depth combination"); 443 } 444 445 // Look for palette data if present 446 if (numberOfColors) { 447 colormapSize = numberOfColors * colormapEntrySize; 448 colormap = New!(ubyte[])(colormapSize); 449 istrm.fillArray(colormap); 450 } 451 452 // Go to begining of pixel data 453 istrm.position = bmpfh.offset; 454 455 const bool transparent = alphaMask != 0 && compression == BMPCompressionType.BitFields; 456 457 // Create image 458 img = imgFac.createImage(width, height, transparent ? 4 : 3, 8); 459 460 enum wrongIndexError = "wrong index for colormap"; 461 462 if (bitsPerPixel == 4 && compression == BMPCompressionType.RGB) { 463 foreach(y; 0..img.height) 464 { 465 //4 bits per pixel, so width/2 iterations 466 foreach(x; 0..img.width/2) 467 { 468 ubyte[1] buf; 469 istrm.fillArray(buf); 470 const uint first = (buf[0] >> 4)*colormapEntrySize; 471 const uint second = (buf[0] & 0x0f)*colormapEntrySize; 472 473 if (!checkIndex(first, colormap) || !checkIndex(second, colormap)) { 474 return error(wrongIndexError); 475 } 476 img[x*2, img.height-y-1] = Color4f(ColorRGBA(colormap[first+2], colormap[first+1], colormap[first])); 477 img[x*2 + 1, img.height-y-1] = Color4f(ColorRGBA(colormap[second+2], colormap[second+1], colormap[second])); 478 } 479 //for odd widths 480 if (img.width & 1) { 481 ubyte[1] buf; 482 istrm.fillArray(buf); 483 const uint index = (buf[0] >> 4)*colormapEntrySize; 484 if (!checkIndex(index, colormap)) { 485 return error(wrongIndexError); 486 } 487 img[img.width-1, img.height-y-1] = Color4f(ColorRGBA(colormap[index+2], colormap[index+1], colormap[index])); 488 } 489 } 490 } else if (bitsPerPixel == 8 && compression == BMPCompressionType.RGB) { 491 foreach(y; 0..img.height) 492 { 493 foreach(x; 0..img.width) 494 { 495 ubyte[1] buf; 496 istrm.fillArray(buf); 497 const uint index = buf[0]*colormapEntrySize; 498 if (!checkIndex(index, colormap)) { 499 return error(wrongIndexError); 500 } 501 img[x, img.height-y-1] = Color4f(ColorRGBA(colormap[index+2], colormap[index+1], colormap[index])); 502 } 503 } 504 } else if (bitsPerPixel == 8 && compression == BMPCompressionType.RLE8) { 505 int x, y; 506 507 while(y < img.height) { 508 ubyte value; 509 if (!istrm.readLE(&value)) { 510 break; 511 } 512 if (value == 0) { 513 if (!istrm.readLE(&value) || value == 1) { 514 break; 515 } else { 516 if (value == 0) { 517 x = 0; 518 y++; 519 } else if (value == 2) { 520 version(BMPDebug) { 521 writeln("in delta"); 522 } 523 524 ubyte xdelta, ydelta; 525 istrm.readLE(&xdelta); 526 istrm.readLE(&ydelta); 527 x += xdelta; 528 y += ydelta; 529 } else { 530 version(BMPDebug) { 531 writeln("in absolute mode"); 532 } 533 foreach(i; 0..value) { 534 ubyte j; 535 istrm.readLE(&j); 536 const uint index = j*colormapEntrySize; 537 if (!checkIndex(index, colormap)) { 538 return error(wrongIndexError); 539 } 540 img[x++, img.height-y-1] = Color4f(ColorRGBA(colormap[index+2], colormap[index+1], colormap[index])); 541 } 542 if (value & 1) { 543 ubyte padding; 544 istrm.readLE(&padding); 545 } 546 } 547 } 548 } else { 549 ubyte j; 550 istrm.readLE(&j); 551 const uint index = j*colormapEntrySize; 552 if (!checkIndex(index, colormap)) { 553 return error(wrongIndexError); 554 } 555 foreach(i; 0..value) { 556 img[x++, img.height-y-1] = Color4f(ColorRGBA(colormap[index+2], colormap[index+1], colormap[index])); 557 } 558 } 559 } 560 } else if (bitsPerPixel == 16 || bitsPerPixel == 24 || bitsPerPixel == 32) { 561 const bytesPerPixel = bitsPerPixel / 8; 562 const bytesPerRow = ((bitsPerPixel*width+31)/32)*4; //round to multiple of 4 563 const bytesPerLine = bytesPerPixel * width; 564 const padding = bytesPerRow - bytesPerLine; 565 566 if (bitsPerPixel == 24) { 567 foreach(y; 0..img.height) 568 { 569 foreach(x; 0..img.width) 570 { 571 ubyte[3] bgr; 572 istrm.fillArray(bgr); 573 img[x, img.height-y-1] = Color4f(ColorRGBA(bgr[2], bgr[1], bgr[0])); 574 } 575 576 istrm.seek(padding); 577 } 578 } else if (bitsPerPixel == 16) { 579 foreach(y; 0..img.height) 580 { 581 foreach(x; 0..img.width) 582 { 583 ushort bgr; 584 istrm.readLE(&bgr); 585 const uint p = bgr; 586 const ubyte r = applyMask(p, redMask, redShift, redScale); 587 const ubyte g = applyMask(p, greenMask, greenShift, greenScale); 588 const ubyte b = applyMask(p, blueMask, blueShift, blueScale); 589 590 img[x, img.height-y-1] = Color4f(ColorRGBA(r,g,b)); 591 } 592 593 istrm.seek(padding); 594 } 595 } else if (bitsPerPixel == 32) { 596 foreach(y; 0..img.height) 597 { 598 foreach(x; 0..img.width) 599 { 600 uint p; 601 istrm.readLE(&p); 602 603 const ubyte r = applyMask(p, redMask, redShift, redScale); 604 const ubyte g = applyMask(p, greenMask, greenShift, greenScale); 605 const ubyte b = applyMask(p, blueMask, blueShift, blueScale); 606 607 img[x, img.height-y-1] = Color4f(ColorRGBA(r, g, b, transparent ? applyMask(p, alphaMask, alphaShift, alphaScale) : 0xff)); 608 } 609 610 istrm.seek(padding); 611 } 612 } 613 } else { 614 return error("loadBMP error: unknown or unsupported compression type / color depth combination"); 615 } 616 617 if (colormap.length) 618 Delete(colormap); 619 620 return compound(img, ""); 621 } 622 623 /// 624 unittest 625 { 626 import dlib.core.stream; 627 import std.stdio; 628 629 SuperImage img; 630 631 //32 bit with bitfield masks 632 ubyte[] bmpData32 = [ 633 66, 77, 72, 1, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 56, 0, 0, 0, 8, 0, 0, 0, 8, 0, 634 0, 0, 1, 0, 32, 0, 3, 0, 0, 0, 2, 1, 0, 0, 18, 11, 0, 0, 18, 11, 0, 0, 0, 0, 0, 635 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, 636 255, 255, 0, 245, 235, 224, 0, 229, 199, 154, 0, 248, 227, 185, 0, 255, 229, 637 181, 0, 236, 203, 152, 0, 244, 234, 223, 0, 255, 255, 255, 0, 244, 234, 224, 0, 638 202, 139, 76, 0, 242, 199, 126, 0, 202, 217, 187, 0, 117, 190, 218, 0, 167, 639 177, 160, 0, 209, 140, 72, 0, 243, 231, 221, 0, 196, 149, 107, 0, 166, 97, 16, 640 0, 208, 143, 34, 0, 161, 188, 160, 0, 59, 207, 255, 0, 52, 168, 228, 0, 182, 641 115, 42, 0, 196, 144, 97, 0, 196, 151, 116, 0, 192, 136, 51, 0, 226, 169, 71, 642 0, 231, 202, 160, 0, 170, 199, 178, 0, 101, 178, 172, 0, 176, 156, 116, 0, 201, 643 153, 112, 0, 204, 162, 127, 0, 185, 156, 134, 0, 136, 155, 170, 0, 153, 201, 644 201, 0, 161, 211, 186, 0, 69, 179, 136, 0, 123, 151, 103, 0, 210, 164, 133, 0, 645 215, 183, 153, 0, 201, 174, 166, 0, 34, 94, 208, 0, 29, 132, 228, 0, 125, 188, 646 190, 0, 112, 178, 134, 0, 120, 144, 104, 0, 213, 181, 154, 0, 246, 240, 233, 0, 647 221, 193, 168, 0, 167, 168, 213, 0, 127, 147, 220, 0, 220, 224, 236, 0, 255, 648 239, 232, 0, 220, 191, 169, 0, 245, 238, 230, 0, 255, 255, 255, 0, 247, 240, 649 233, 0, 235, 213, 186, 0, 252, 237, 216, 0, 245, 231, 217, 0, 231, 212, 193, 0, 650 246, 239, 230, 0, 255, 255, 255, 0, 0 651 ]; 652 auto bmpStream32 = new ArrayStream(bmpData32); 653 img = loadBMP(bmpStream32); 654 assert(img[2,2].convert(8) == Color4(208, 94, 34, 255)); 655 assert(img[5,2].convert(8) == Color4(134, 178, 112, 255)); 656 assert(img[2,5].convert(8) == Color4(34, 143, 208, 255)); 657 assert(img[5,5].convert(8) == Color4(228, 168, 52, 255)); 658 659 //32 bit with transparency 660 ubyte[] bmpData32_alpha = [ 661 66, 77, 122, 1, 0, 0, 0, 0, 0, 0, 122, 0, 0, 0, 108, 0, 0, 0, 8, 0, 0, 0, 8, 662 0, 0, 0, 1, 0, 32, 0, 3, 0, 0, 0, 0, 1, 0, 0, 109, 11, 0, 0, 109, 11, 0, 0, 0, 663 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 255, 0, 0, 255, 0, 0, 0, 0, 0, 0, 255, 1, 664 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 665 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 666 249, 173, 0, 206, 142, 67, 52, 231, 194, 127, 176, 248, 229, 183, 239, 249, 667 229, 182, 240, 235, 196, 126, 179, 207, 143, 68, 55, 255, 255, 238, 0, 168, 86, 668 17, 50, 198, 128, 57, 207, 230, 188, 116, 255, 200, 211, 179, 255, 131, 192, 669 209, 255, 164, 172, 153, 255, 199, 131, 60, 211, 171, 88, 18, 55, 162, 88, 23, 670 171, 171, 103, 22, 255, 203, 147, 48, 255, 165, 188, 159, 255, 78, 202, 251, 671 255, 71, 168, 211, 255, 174, 119, 55, 255, 166, 89, 21, 179, 190, 142, 100, 672 233, 191, 137, 58, 255, 215, 167, 81, 255, 216, 198, 158, 255, 162, 198, 181, 673 255, 106, 177, 169, 255, 171, 153, 111, 255, 193, 142, 98, 239, 198, 155, 120, 674 232, 184, 154, 130, 255, 140, 155, 166, 255, 149, 194, 197, 255, 153, 206, 185, 675 255, 84, 180, 141, 255, 129, 151, 106, 255, 198, 154, 121, 238, 196, 152, 117, 676 168, 191, 166, 157, 255, 59, 109, 202, 255, 51, 140, 222, 255, 127, 188, 190, 677 255, 119, 179, 141, 255, 129, 146, 106, 255, 188, 149, 117, 175, 193, 147, 111, 678 47, 213, 186, 167, 203, 164, 163, 201, 255, 138, 156, 216, 255, 212, 216, 226, 679 255, 237, 225, 212, 255, 213, 189, 168, 207, 190, 148, 112, 52, 255, 255, 255, 680 0, 212, 179, 149, 47, 236, 218, 201, 169, 247, 237, 227, 233, 246, 237, 229, 681 234, 233, 217, 203, 172, 214, 182, 154, 50, 255, 255, 255, 0 682 ]; 683 auto bmpStream32_alpha = new ArrayStream(bmpData32_alpha); 684 img = loadBMP(bmpStream32_alpha); 685 assert(img[1,1].convert(8) == Color4(167, 186, 213, 203)); 686 assert(img[1,6].convert(8) == Color4(57, 128, 198, 207)); 687 assert(img[2,2].convert(8) == Color4(202, 109, 59, 255)); 688 assert(img[5,5].convert(8) == Color4(211, 168, 71, 255)); 689 690 //24 bit 691 ubyte[] bmpData24 = [ 692 66, 77, 248, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, 40, 0, 0, 0, 8, 0, 0, 0, 8, 0, 693 0, 0, 1, 0, 24, 0, 0, 0, 0, 0, 194, 0, 0, 0, 18, 11, 0, 0, 18, 11, 0, 0, 0, 0, 694 0, 0, 0, 0, 0, 0, 255, 255, 255, 245, 235, 224, 229, 199, 154, 248, 227, 185, 695 255, 229, 181, 236, 203, 152, 244, 234, 223, 255, 255, 255, 244, 234, 224, 202, 696 139, 76, 242, 199, 126, 202, 217, 187, 117, 190, 218, 167, 177, 160, 209, 140, 697 72, 243, 231, 221, 196, 149, 107, 166, 97, 16, 208, 143, 34, 161, 188, 160, 59, 698 207, 255, 52, 168, 228, 182, 115, 42, 196, 144, 97, 196, 151, 116, 192, 136, 699 51, 226, 169, 71, 231, 202, 160, 170, 199, 178, 101, 178, 172, 176, 156, 116, 700 201, 153, 112, 204, 162, 127, 185, 156, 134, 136, 155, 170, 153, 201, 201, 161, 701 211, 186, 69, 179, 136, 123, 151, 103, 210, 164, 133, 215, 183, 153, 201, 174, 702 166, 34, 94, 208, 29, 132, 228, 125, 188, 190, 112, 178, 134, 120, 144, 104, 703 213, 181, 154, 246, 240, 233, 221, 193, 168, 167, 168, 213, 127, 147, 220, 220, 704 224, 236, 255, 239, 232, 220, 191, 169, 245, 238, 230, 255, 255, 255, 247, 240, 705 233, 235, 213, 186, 252, 237, 216, 245, 231, 217, 231, 212, 193, 246, 239, 230, 706 255, 255, 255, 0, 0 707 ]; 708 auto bmpStream24 = new ArrayStream(bmpData24); 709 img = loadBMP(bmpStream24); 710 assert(img[2,2].convert(8) == Color4(208, 94, 34, 255)); 711 assert(img[5,5].convert(8) == Color4(228, 168, 52, 255)); 712 713 //16 bit X1 R5 G5 B5 714 ubyte[] bmpData16_1_5_5_5 = [ 715 66, 77, 184, 0, 0, 0, 0, 0, 0, 0, 54, 0, 0, 0, 40, 0, 0, 0, 8, 0, 0, 0, 8, 0, 716 0, 0, 1, 0, 16, 0, 0, 0, 0, 0, 130, 0, 0, 0, 18, 11, 0, 0, 18, 11, 0, 0, 0, 0, 717 0, 0, 0, 0, 0, 0, 255, 127, 190, 111, 28, 79, 158, 91, 159, 91, 61, 75, 158, 718 111, 255, 127, 158, 111, 57, 38, 29, 63, 89, 95, 238, 110, 212, 78, 57, 38, 719 158, 111, 88, 54, 148, 9, 57, 18, 244, 78, 39, 127, 134, 114, 214, 21, 88, 50, 720 88, 58, 55, 26, 187, 38, 60, 79, 21, 91, 204, 86, 117, 58, 120, 58, 153, 62, 721 118, 66, 113, 86, 19, 99, 84, 95, 200, 70, 79, 54, 154, 66, 218, 78, 184, 82, 722 100, 101, 4, 114, 239, 94, 206, 66, 79, 54, 218, 78, 190, 115, 251, 82, 148, 723 106, 79, 110, 123, 119, 191, 115, 251, 86, 190, 115, 255, 127, 190, 115, 93, 724 95, 191, 107, 158, 107, 92, 95, 190, 115, 255, 127, 0, 0 725 ]; 726 auto bmpStream16_1_5_5_5 = new ArrayStream(bmpData16_1_5_5_5); 727 img = loadBMP(bmpStream16_1_5_5_5); 728 729 /*TODO: pixel comparisons 730 * GIMP shows slightly different pixel values on the same images. 731 */ 732 733 //16 bit X4 R4 G4 B4 734 ubyte[] bmpData16_4_4_4_4 = [ 735 66, 77, 200, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 56, 0, 0, 0, 8, 0, 0, 0, 8, 0, 736 0, 0, 1, 0, 16, 0, 3, 0, 0, 0, 130, 0, 0, 0, 18, 11, 0, 0, 18, 11, 0, 0, 0, 0, 737 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 240, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 255, 15, 738 238, 13, 205, 9, 223, 11, 223, 11, 206, 9, 238, 13, 255, 15, 238, 13, 140, 4, 739 206, 7, 220, 11, 183, 13, 170, 9, 140, 4, 238, 13, 156, 6, 106, 1, 140, 2, 185, 740 9, 195, 15, 163, 13, 123, 2, 140, 6, 156, 7, 139, 3, 173, 4, 206, 9, 202, 10, 741 166, 10, 154, 7, 156, 7, 172, 7, 155, 8, 152, 10, 201, 12, 201, 11, 180, 8, 742 151, 6, 172, 8, 189, 9, 172, 10, 98, 12, 130, 13, 183, 11, 167, 8, 135, 6, 189, 743 9, 238, 14, 189, 10, 170, 13, 151, 13, 221, 14, 239, 14, 189, 10, 238, 14, 255, 744 15, 239, 14, 222, 11, 239, 13, 238, 13, 206, 11, 238, 14, 255, 15, 0, 0 745 ]; 746 auto bmpStream16_4_4_4_4 = new ArrayStream(bmpData16_4_4_4_4); 747 img = loadBMP(bmpStream16_4_4_4_4); 748 749 /*TODO: pixel comparisons 750 * GIMP shows slightly different pixel values on the same images. 751 */ 752 753 //16 bit R5 G6 B5 754 ubyte[] bmpData16_5_6_5 = [ 755 66, 77, 200, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, 56, 0, 0, 0, 8, 0, 0, 0, 8, 0, 756 0, 0, 1, 0, 16, 0, 3, 0, 0, 0, 130, 0, 0, 0, 18, 11, 0, 0, 18, 11, 0, 0, 0, 0, 757 0, 0, 0, 0, 0, 0, 0, 248, 0, 0, 224, 7, 0, 0, 31, 0, 0, 0, 0, 0, 0, 0, 255, 758 255, 94, 223, 60, 158, 30, 183, 63, 183, 93, 150, 94, 223, 255, 255, 94, 223, 759 89, 76, 61, 126, 217, 190, 238, 221, 148, 157, 121, 76, 62, 223, 184, 108, 20, 760 19, 121, 36, 212, 157, 103, 254, 70, 229, 150, 43, 152, 100, 184, 116, 87, 52, 761 91, 77, 92, 158, 53, 182, 140, 173, 245, 116, 216, 116, 25, 125, 246, 132, 209, 762 172, 83, 198, 148, 190, 136, 141, 175, 108, 58, 133, 186, 157, 120, 165, 228, 763 202, 36, 228, 207, 189, 142, 133, 143, 108, 186, 157, 126, 231, 27, 166, 84, 764 213, 143, 220, 251, 238, 127, 231, 251, 173, 126, 231, 255, 255, 126, 231, 189, 765 190, 127, 215, 62, 215, 156, 190, 126, 231, 255, 255, 0, 0 766 ]; 767 auto bmpStream16_5_6_5 = new ArrayStream(bmpData16_5_6_5); 768 img = loadBMP(bmpStream16_5_6_5); 769 770 /*TODO: pixel comparisons 771 * GIMP shows slightly different pixel values on the same images. 772 */ 773 774 //4 bit 775 ubyte[] bmpData4 = [ 776 66, 77, 150, 0, 0, 0, 0, 0, 0, 0, 118, 0, 0, 0, 40, 0, 0, 0, 8, 0, 0, 0, 8, 0, 777 0, 0, 1, 0, 4, 0, 0, 0, 0, 0, 32, 0, 0, 0, 196, 14, 0, 0, 196, 14, 0, 0, 0, 0, 778 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 128, 0, 0, 0, 128, 128, 0, 128, 779 0, 0, 0, 128, 0, 128, 0, 128, 128, 0, 0, 128, 128, 128, 0, 192, 192, 192, 0, 0, 780 0, 255, 0, 0, 255, 0, 0, 0, 255, 255, 0, 255, 0, 0, 0, 255, 0, 255, 0, 255, 781 255, 0, 0, 255, 255, 255, 0, 255, 136, 136, 255, 246, 136, 136, 111, 116, 103, 782 187, 103, 118, 104, 131, 119, 119, 120, 131, 119, 136, 147, 135, 40, 248, 135, 783 255, 143, 255, 255, 255, 255 784 ]; 785 auto bmpStream4 = new ArrayStream(bmpData4); 786 img = loadBMP(bmpStream4); 787 assert(img[2,2].convert(8) == Color4(255,0,0,255)); 788 assert(img[1,1].convert(8) == Color4(192,192,192,255)); 789 assert(img[6,2].convert(8) == Color4(0,128,0,255)); 790 } 791 792 /** 793 * Save BMP to file using local FileSystem. 794 * Causes GC allocation 795 */ 796 void saveBMP(SuperImage img, string filename) 797 { 798 OutputStream output = openForOutput(filename); 799 Compound!(bool, string) res = 800 saveBMP(img, output); 801 output.close(); 802 803 if (!res[0]) 804 throw new BMPLoadException(res[1]); 805 } 806 807 /** 808 * Save BMP to stream. 809 * GC-free 810 */ 811 Compound!(bool, string) saveBMP(SuperImage img, OutputStream output) 812 { 813 Compound!(bool, string) error(string errorMsg) 814 { 815 return compound(false, errorMsg); 816 } 817 818 uint bytesPerRow = (img.width * 24 + 31) / 32 * 4; 819 uint dataOffset = 12 + BMPInfoSize.WIN; 820 uint fileSize = dataOffset + img.height * bytesPerRow; 821 822 output.writeArray(BMPMagic); 823 output.writeLE(fileSize); 824 output.writeLE(cast(ushort)0); 825 output.writeLE(cast(ushort)0); 826 output.writeLE(dataOffset); 827 828 output.writeLE(BMPInfoSize.WIN); 829 output.writeLE(img.width); 830 output.writeLE(img.height); 831 output.writeLE(cast(ushort)1); 832 output.writeLE(cast(ushort)24); 833 output.writeLE(BMPCompressionType.RGB); 834 output.writeLE(bytesPerRow * img.height); 835 output.writeLE(2834); 836 output.writeLE(2834); 837 output.writeLE(0); 838 output.writeLE(0); 839 840 foreach_reverse(y; 0..img.height) { 841 foreach(x; 0..img.width) { 842 ubyte[3] rgb; 843 ColorRGBA color = img[x, y].convert(8); 844 rgb[0] = cast(ubyte)color[2]; 845 rgb[1] = cast(ubyte)color[1]; 846 rgb[2] = cast(ubyte)color[0]; 847 output.writeArray(rgb); 848 } 849 //padding 850 for(uint i=0; i<(bytesPerRow-img.width*3); ++i) { 851 output.writeLE(cast(ubyte)0); 852 } 853 } 854 855 return compound(true, ""); 856 }