1 module imageformats.bmp;
2 
3 import std.bitmanip : littleEndianToNative, nativeToLittleEndian;
4 import std.stdio    : File, SEEK_SET;
5 import std.math     : abs;
6 import std.typecons : scoped;
7 import imageformats;
8 
9 private:
10 
11 immutable bmp_header = ['B', 'M'];
12 
13 /// Reads a BMP image. req_chans defines the format of returned image
14 /// (you can use ColFmt here).
15 public IFImage read_bmp(in char[] filename, long req_chans = 0) {
16     auto reader = scoped!FileReader(filename);
17     return read_bmp(reader, req_chans);
18 }
19 
20 /// Reads an image from a buffer containing a BMP image. req_chans defines the
21 /// format of returned image (you can use ColFmt here).
22 public IFImage read_bmp_from_mem(in ubyte[] source, long req_chans = 0) {
23     auto reader = scoped!MemReader(source);
24     return read_bmp(reader, req_chans);
25 }
26 
27 /// Returns the header of a BMP file.
28 public BMP_Header read_bmp_header(in char[] filename) {
29     auto reader = scoped!FileReader(filename);
30     return read_bmp_header(reader);
31 }
32 
33 /// Reads the image header from a buffer containing a BMP image.
34 public BMP_Header read_bmp_header_from_mem(in ubyte[] source) {
35     auto reader = scoped!MemReader(source);
36     return read_bmp_header(reader);
37 }
38 
39 /// Header of a BMP file.
40 public struct BMP_Header {
41     uint file_size;
42     uint pixel_data_offset;
43 
44     uint dib_size;
45     int width;
46     int height;
47     ushort planes;
48     int bits_pp;
49     uint dib_version;
50     DibV1 dib_v1;
51     DibV2 dib_v2;
52     uint dib_v3_alpha_mask;
53     DibV4 dib_v4;
54     DibV5 dib_v5;
55 }
56 
57 /// Part of BMP header, not always present.
58 public struct DibV1 {
59     uint compression;
60     uint idat_size;
61     uint pixels_per_meter_x;
62     uint pixels_per_meter_y;
63     uint palette_length;
64     uint important_color_count;
65 }
66 
67 /// Part of BMP header, not always present.
68 public struct DibV2 {
69     uint red_mask;
70     uint green_mask;
71     uint blue_mask;
72 }
73 
74 /// Part of BMP header, not always present.
75 public struct DibV4 {
76     uint color_space_type;
77     ubyte[36] color_space_endpoints;
78     uint gamma_red;
79     uint gamma_green;
80     uint gamma_blue;
81 }
82 
83 /// Part of BMP header, not always present.
84 public struct DibV5 {
85     uint icc_profile_data;
86     uint icc_profile_size;
87 }
88 
89 /// Returns width, height and color format information via w, h and chans.
90 public void read_bmp_info(in char[] filename, out int w, out int h, out int chans) {
91     auto reader = scoped!FileReader(filename);
92     return read_bmp_info(reader, w, h, chans);
93 }
94 
95 /// Returns width, height and color format information via w, h and chans.
96 public void read_bmp_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) {
97     auto reader = scoped!MemReader(source);
98     return read_bmp_info(reader, w, h, chans);
99 }
100 
101 /// Writes a BMP image into a file.
102 public void write_bmp(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0)
103 {
104     auto writer = scoped!FileWriter(file);
105     write_bmp(writer, w, h, data, tgt_chans);
106 }
107 
108 /// Writes a BMP image into a buffer.
109 public ubyte[] write_bmp_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) {
110     auto writer = scoped!MemWriter();
111     write_bmp(writer, w, h, data, tgt_chans);
112     return writer.result;
113 }
114 
115 // Detects whether a BMP image is readable from stream.
116 package bool detect_bmp(Reader stream) {
117     try {
118         ubyte[18] tmp = void;  // bmp header + size of dib header
119         stream.readExact(tmp, tmp.length);
120         size_t ds = littleEndianToNative!uint(tmp[14..18]);
121         return (tmp[0..2] == bmp_header
122             && (ds == 12 || ds == 40 || ds == 52 || ds == 56 || ds == 108 || ds == 124));
123     } catch (Throwable) {
124         return false;
125     } finally {
126         stream.seek(0, SEEK_SET);
127     }
128 }
129 
130 BMP_Header read_bmp_header(Reader stream) {
131     ubyte[18] tmp = void;  // bmp header + size of dib header
132     stream.readExact(tmp[], tmp.length);
133 
134     if (tmp[0..2] != bmp_header)
135         throw new ImageIOException("corrupt header");
136 
137     uint dib_size = littleEndianToNative!uint(tmp[14..18]);
138     uint dib_version;
139     switch (dib_size) {
140         case 12: dib_version = 0; break;
141         case 40: dib_version = 1; break;
142         case 52: dib_version = 2; break;
143         case 56: dib_version = 3; break;
144         case 108: dib_version = 4; break;
145         case 124: dib_version = 5; break;
146         default: throw new ImageIOException("unsupported dib version");
147     }
148     auto dib_header = new ubyte[dib_size-4];
149     stream.readExact(dib_header[], dib_header.length);
150 
151     DibV1 dib_v1;
152     DibV2 dib_v2;
153     uint dib_v3_alpha_mask;
154     DibV4 dib_v4;
155     DibV5 dib_v5;
156 
157     if (1 <= dib_version) {
158         DibV1 v1 = {
159             compression           : littleEndianToNative!uint(dib_header[12..16]),
160             idat_size             : littleEndianToNative!uint(dib_header[16..20]),
161             pixels_per_meter_x    : littleEndianToNative!uint(dib_header[20..24]),
162             pixels_per_meter_y    : littleEndianToNative!uint(dib_header[24..28]),
163             palette_length        : littleEndianToNative!uint(dib_header[28..32]),
164             important_color_count : littleEndianToNative!uint(dib_header[32..36]),
165         };
166         dib_v1 = v1;
167     }
168 
169     if (2 <= dib_version) {
170         DibV2 v2 = {
171             red_mask              : littleEndianToNative!uint(dib_header[36..40]),
172             green_mask            : littleEndianToNative!uint(dib_header[40..44]),
173             blue_mask             : littleEndianToNative!uint(dib_header[44..48]),
174         };
175         dib_v2 = v2;
176     }
177 
178     if (3 <= dib_version) {
179         dib_v3_alpha_mask = littleEndianToNative!uint(dib_header[48..52]);
180     }
181 
182     if (4 <= dib_version) {
183         DibV4 v4 = {
184             color_space_type      : littleEndianToNative!uint(dib_header[52..56]),
185             color_space_endpoints : dib_header[56..92],
186             gamma_red             : littleEndianToNative!uint(dib_header[92..96]),
187             gamma_green           : littleEndianToNative!uint(dib_header[96..100]),
188             gamma_blue            : littleEndianToNative!uint(dib_header[100..104]),
189         };
190         dib_v4 = v4;
191     }
192 
193     if (5 <= dib_version) {
194         DibV5 v5 = {
195             icc_profile_data      : littleEndianToNative!uint(dib_header[108..112]),
196             icc_profile_size      : littleEndianToNative!uint(dib_header[112..116]),
197         };
198         dib_v5 = v5;
199     }
200 
201     int width, height; ushort planes; int bits_pp;
202     if (0 == dib_version) {
203         width = littleEndianToNative!ushort(dib_header[0..2]);
204         height = littleEndianToNative!ushort(dib_header[2..4]);
205         planes = littleEndianToNative!ushort(dib_header[4..6]);
206         bits_pp = littleEndianToNative!ushort(dib_header[6..8]);
207     } else {
208         width = littleEndianToNative!int(dib_header[0..4]);
209         height = littleEndianToNative!int(dib_header[4..8]);
210         planes = littleEndianToNative!ushort(dib_header[8..10]);
211         bits_pp = littleEndianToNative!ushort(dib_header[10..12]);
212     }
213 
214     BMP_Header header = {
215         file_size             : littleEndianToNative!uint(tmp[2..6]),
216         pixel_data_offset     : littleEndianToNative!uint(tmp[10..14]),
217         width                 : width,
218         height                : height,
219         planes                : planes,
220         bits_pp               : bits_pp,
221         dib_version           : dib_version,
222         dib_v1                : dib_v1,
223         dib_v2                : dib_v2,
224         dib_v3_alpha_mask     : dib_v3_alpha_mask,
225         dib_v4                : dib_v4,
226         dib_v5                : dib_v5,
227     };
228     return header;
229 }
230 
231 enum CMP_RGB  = 0;
232 enum CMP_BITS = 3;
233 
234 package IFImage read_bmp(Reader stream, long req_chans = 0) {
235     if (req_chans < 0 || 4 < req_chans)
236         throw new ImageIOException("unknown color format");
237 
238     BMP_Header hdr = read_bmp_header(stream);
239 
240     if (hdr.width < 1 || hdr.height == 0)
241         throw new ImageIOException("invalid dimensions");
242     if (hdr.pixel_data_offset < (14 + hdr.dib_size)
243      || hdr.pixel_data_offset > 0xffffff /* arbitrary */) {
244         throw new ImageIOException("invalid pixel data offset");
245     }
246     if (hdr.planes != 1)
247         throw new ImageIOException("not supported");
248 
249     auto bytes_pp       = 1;
250     bool paletted       = true;
251     size_t palette_length = 256;
252     bool rgb_masked     = false;
253     auto pe_bytes_pp    = 3;
254 
255     if (1 <= hdr.dib_version) {
256         if (256 < hdr.dib_v1.palette_length)
257             throw new ImageIOException("ivnalid palette length");
258         if (hdr.bits_pp <= 8 &&
259            (hdr.dib_v1.palette_length == 0 || hdr.dib_v1.compression != CMP_RGB))
260              throw new ImageIOException("unsupported format");
261         if (hdr.dib_v1.compression != CMP_RGB && hdr.dib_v1.compression != CMP_BITS)
262              throw new ImageIOException("unsupported compression");
263 
264         switch (hdr.bits_pp) {
265             case 8  : bytes_pp = 1; paletted = true; break;
266             case 24 : bytes_pp = 3; paletted = false; break;
267             case 32 : bytes_pp = 4; paletted = false; break;
268             default: throw new ImageIOException("not supported");
269         }
270 
271         palette_length = hdr.dib_v1.palette_length;
272         rgb_masked = hdr.dib_v1.compression == CMP_BITS;
273         pe_bytes_pp = 4;
274     }
275 
276     static size_t mask_to_idx(in uint mask) {
277         switch (mask) {
278             case 0xff00_0000: return 3;
279             case 0x00ff_0000: return 2;
280             case 0x0000_ff00: return 1;
281             case 0x0000_00ff: return 0;
282             default: throw new ImageIOException("unsupported mask");
283         }
284     }
285 
286     size_t redi = 2;
287     size_t greeni = 1;
288     size_t bluei = 0;
289     if (rgb_masked) {
290         if (hdr.dib_version < 2)
291             throw new ImageIOException("invalid format");
292         redi = mask_to_idx(hdr.dib_v2.red_mask);
293         greeni = mask_to_idx(hdr.dib_v2.green_mask);
294         bluei = mask_to_idx(hdr.dib_v2.blue_mask);
295     }
296 
297     bool alpha_masked = false;
298     size_t alphai = 0;
299     if (bytes_pp == 4 && 3 <= hdr.dib_version && hdr.dib_v3_alpha_mask != 0) {
300         alpha_masked = true;
301         alphai = mask_to_idx(hdr.dib_v3_alpha_mask);
302     }
303 
304     ubyte[] depaletted_line = null;
305     ubyte[] palette = null;
306     if (paletted) {
307         depaletted_line = new ubyte[hdr.width * pe_bytes_pp];
308         palette = new ubyte[palette_length * pe_bytes_pp];
309         stream.readExact(palette[], palette.length);
310     }
311 
312     stream.seek(hdr.pixel_data_offset, SEEK_SET);
313 
314     const tgt_chans = (0 < req_chans) ? req_chans
315                                       : (alpha_masked) ? _ColFmt.RGBA
316                                                        : _ColFmt.RGB;
317 
318     const src_fmt = (!paletted || pe_bytes_pp == 4) ? _ColFmt.BGRA : _ColFmt.BGR;
319     const LineConv!ubyte convert = get_converter!ubyte(src_fmt, tgt_chans);
320 
321     const size_t src_linesize = hdr.width * bytes_pp;  // without padding
322     const size_t src_pad = 3 - ((src_linesize-1) % 4);
323     const ptrdiff_t tgt_linesize = (hdr.width * cast(int) tgt_chans);
324 
325     const ptrdiff_t tgt_stride = (hdr.height < 0) ? tgt_linesize : -tgt_linesize;
326     ptrdiff_t ti               = (hdr.height < 0) ? 0 : (hdr.height-1) * tgt_linesize;
327 
328     auto src_line      = new ubyte[src_linesize + src_pad];
329     auto bgra_line_buf = (paletted) ? null : new ubyte[hdr.width * 4];
330     auto result        = new ubyte[hdr.width * abs(hdr.height) * cast(int) tgt_chans];
331 
332     foreach (_; 0 .. abs(hdr.height)) {
333         stream.readExact(src_line[], src_line.length);
334 
335         if (paletted) {
336             const size_t ps = pe_bytes_pp;
337             size_t di = 0;
338             foreach (idx; src_line[0..src_linesize]) {
339                 if (idx > palette_length)
340                     throw new ImageIOException("invalid palette index");
341                 size_t i = idx * ps;
342                 depaletted_line[di .. di+ps] = palette[i .. i+ps];
343                 if (ps == 4) {
344                     depaletted_line[di+3] = 255;
345                 }
346                 di += ps;
347             }
348             convert(depaletted_line[], result[ti .. ti + tgt_linesize]);
349         } else {
350             for (size_t si, di;   si < src_linesize;   si+=bytes_pp, di+=4) {
351                 bgra_line_buf[di + 0] = src_line[si + bluei];
352                 bgra_line_buf[di + 1] = src_line[si + greeni];
353                 bgra_line_buf[di + 2] = src_line[si + redi];
354                 bgra_line_buf[di + 3] = (alpha_masked) ? src_line[si + alphai]
355                                                        : 255;
356             }
357             convert(bgra_line_buf[], result[ti .. ti + tgt_linesize]);
358         }
359 
360         ti += tgt_stride;
361     }
362 
363     IFImage ret = {
364         w      : hdr.width,
365         h      : abs(hdr.height),
366         c      : cast(ColFmt) tgt_chans,
367         pixels : result,
368     };
369     return ret;
370 }
371 
372 package void read_bmp_info(Reader stream, out int w, out int h, out int chans) {
373     BMP_Header hdr = read_bmp_header(stream);
374     w = abs(hdr.width);
375     h = abs(hdr.height);
376     chans = (hdr.dib_version >= 3 && hdr.dib_v3_alpha_mask != 0 && hdr.bits_pp == 32)
377           ? ColFmt.RGBA
378           : ColFmt.RGB;
379 }
380 
381 // ----------------------------------------------------------------------
382 // BMP encoder
383 
384 // Writes RGB or RGBA data.
385 void write_bmp(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) {
386     if (w < 1 || h < 1 || 0x7fff < w || 0x7fff < h)
387         throw new ImageIOException("invalid dimensions");
388     size_t src_chans = data.length / cast(size_t) w / cast(size_t) h;
389     if (src_chans < 1 || 4 < src_chans)
390         throw new ImageIOException("invalid channel count");
391     if (tgt_chans != 0 && tgt_chans != 3 && tgt_chans != 4)
392         throw new ImageIOException("unsupported format for writing");
393     if (src_chans * w * h != data.length)
394         throw new ImageIOException("mismatching dimensions and length");
395 
396     if (tgt_chans == 0)
397         tgt_chans = (src_chans == 1 || src_chans == 3) ? 3 : 4;
398 
399     const dib_size = 108;
400     const size_t tgt_linesize = cast(size_t) (w * tgt_chans);
401     const size_t pad = 3 - ((tgt_linesize-1) & 3);
402     const size_t idat_offset = 14 + dib_size;       // bmp file header + dib header
403     const size_t filesize = idat_offset + cast(size_t) h * (tgt_linesize + pad);
404     if (filesize > 0xffff_ffff) {
405         throw new ImageIOException("image too large");
406     }
407 
408     ubyte[14+dib_size] hdr;
409     hdr[0] = 0x42;
410     hdr[1] = 0x4d;
411     hdr[2..6] = nativeToLittleEndian(cast(uint) filesize);
412     hdr[6..10] = 0;                                                // reserved
413     hdr[10..14] = nativeToLittleEndian(cast(uint) idat_offset);    // offset of pixel data
414     hdr[14..18] = nativeToLittleEndian(cast(uint) dib_size);       // dib header size
415     hdr[18..22] = nativeToLittleEndian(cast(int) w);
416     hdr[22..26] = nativeToLittleEndian(cast(int) h);            // positive -> bottom-up
417     hdr[26..28] = nativeToLittleEndian(cast(ushort) 1);         // planes
418     hdr[28..30] = nativeToLittleEndian(cast(ushort) (tgt_chans * 8)); // bits per pixel
419     hdr[30..34] = nativeToLittleEndian((tgt_chans == 3) ? CMP_RGB : CMP_BITS);
420     hdr[34..54] = 0;                                          // rest of dib v1
421     if (tgt_chans == 3) {
422         hdr[54..70] = 0;    // dib v2 and v3
423     } else {
424         static immutable ubyte[16] b = [
425             0, 0, 0xff, 0,
426             0, 0xff, 0, 0,
427             0xff, 0, 0, 0,
428             0, 0, 0, 0xff
429         ];
430         hdr[54..70] = b;
431     }
432     static immutable ubyte[4] BGRs = ['B', 'G', 'R', 's'];
433     hdr[70..74] = BGRs;
434     hdr[74..122] = 0;
435     stream.rawWrite(hdr);
436 
437     const LineConv!ubyte convert =
438         get_converter!ubyte(src_chans, (tgt_chans == 3) ? _ColFmt.BGR
439                                                         : _ColFmt.BGRA);
440 
441     auto tgt_line = new ubyte[tgt_linesize + pad];
442     const size_t src_linesize = cast(size_t) w * src_chans;
443     size_t si = cast(size_t) h * src_linesize;
444 
445     foreach (_; 0..h) {
446         si -= src_linesize;
447         convert(data[si .. si + src_linesize], tgt_line[0..tgt_linesize]);
448         stream.rawWrite(tgt_line);
449     }
450 
451     stream.flush();
452 }