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 }