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