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