1 // Copyright 2019 Tero Hänninen. All rights reserved.
2 // SPDX-License-Identifier: BSD-2-Clause
3 module imagefmt.bmp;
4 
5 import imagefmt;
6 
7 @nogc nothrow package:
8 
9 struct BMPHeader {
10     int w;              // can be negative
11     int h;              // can be negative
12     int planes;         // only checked, not otherwise used...
13     int bitspp;
14     uint dataoff;
15     uint alphamask;     // alpha; from dibv3
16     uint rmask;         // red
17     uint gmask;         // green
18     uint bmask;         // blue
19     uint compress;
20     uint palettelen;
21     uint dibsize;
22     ubyte dibv;         // dib header version
23 }
24 
25 int abs(int x) { return x >= 0 ? x : -x; }
26 
27 bool detect_bmp(Reader* rc)
28 {
29     bool result;
30     if (read_u8(rc) != 'B' || read_u8(rc) != 'M') {
31         result = false;
32     } else {
33         skip(rc, 12);
34         const uint ds = read_u32le(rc);
35         result = ((ds == 12 || ds == 40 || ds == 52 ||
36                    ds == 56 || ds == 108 || ds == 124) && !rc.fail);
37     }
38     reset2start(rc);
39     return result;
40 }
41 
42 IFInfo read_bmp_info(Reader* rc)
43 {
44     BMPHeader head;
45     IFInfo info;
46     info.e = read_bmp_header(rc, head);
47     if (info.e) return info;
48     info.w = abs(head.w);
49     info.h = abs(head.h);
50     info.c = (head.dibv >= 3 && head.alphamask != 0 && head.bitspp == 32) ? 4 : 3;
51     return info;
52 }
53 
54 ubyte read_bmp_header(Reader* rc, out BMPHeader head)
55 {
56     ubyte b = read_u8(rc);
57     ubyte m = read_u8(rc);
58 
59     skip(rc, 8);    // filesize (4) + reserved bytes
60     head.dataoff = read_u32le(rc);
61     head.dibsize = read_u32le(rc);
62 
63     if (rc.fail)
64         return ERROR.stream;
65 
66     if (b != 'B' || m != 'M')
67         return ERROR.data;
68 
69     switch (head.dibsize) {
70         case 12: head.dibv = 0; break;
71         case 40: head.dibv = 1; break;
72         case 52: head.dibv = 2; break;
73         case 56: head.dibv = 3; break;
74         case 108: head.dibv = 4; break;
75         case 124: head.dibv = 5; break;
76         default: return ERROR.unsupp;
77     }
78 
79     if (head.dibsize <= 12) {
80         head.w      = read_u16le(rc);
81         head.h      = read_u16le(rc);
82         head.planes = read_u16le(rc);
83         head.bitspp = read_u16le(rc);
84     } else {
85         head.w      = cast(int) read_u32le(rc);
86         head.h      = cast(int) read_u32le(rc);
87         head.planes = read_u16le(rc);
88         head.bitspp = read_u16le(rc);
89     }
90 
91     if (head.dibsize >= 40) {
92         head.compress = read_u32le(rc);
93         skip(rc, 4 * 3); // image data size + pixels per meter x & y
94         head.palettelen = read_u32le(rc);
95         skip(rc, 4);    // important color count
96     }
97 
98     if (head.dibsize >= 52) {
99         head.rmask = read_u32le(rc);
100         head.gmask = read_u32le(rc);
101         head.bmask = read_u32le(rc);
102     }
103 
104     if (head.dibsize >= 56)
105         head.alphamask = read_u32le(rc);
106 
107     if (head.dibsize >= 108)
108         skip(rc, 4 + 36 + 4*3); // color space type + endpoints + rgb-gamma
109 
110     if (head.dibsize >= 124)
111         skip(rc, 8);    // icc profile data + size
112 
113     if (rc.fail)
114         return ERROR.stream;
115 
116     if (head.w == int.min || head.h == int.min)
117         return ERROR.data;  // make abs simple
118 
119     return 0;
120 }
121 
122 enum CMP_RGB  = 0;
123 enum CMP_BITS = 3;
124 
125 ubyte read_bmp(Reader* rc, out IFImage image, in int reqchans, in int reqbpc)
126 {
127     if (cast(uint) reqchans > 4)
128         return ERROR.arg;
129     const ubyte tbpc = cast(ubyte) (reqbpc ? reqbpc : 8);
130     if (tbpc != 8 && tbpc != 16)
131         return ERROR.unsupp;
132     BMPHeader head;
133     if (ubyte e = read_bmp_header(rc, head))
134         return e;
135     if (head.w < 1 || head.h == 0)
136         return ERROR.dim;
137     if (head.dataoff < (14 + head.dibsize) || head.dataoff > 0xffffff)
138         return ERROR.data;    // that upper limit is arbitrary --^
139     if (head.planes != 1)
140         return ERROR.unsupp;
141 
142     int bytes_pp    = 1;
143     bool paletted   = true;
144     int palettelen  = 256;
145     bool rgb_masked = false;
146     int pe_bytes_pp = 3;
147 
148     if (head.dibv >= 1) {
149         if (head.palettelen > 256)
150             return ERROR.dim;
151         if (head.bitspp <= 8 && (head.palettelen == 0 || head.compress != CMP_RGB))
152             return ERROR.unsupp;
153         if (head.compress != CMP_RGB && head.compress != CMP_BITS)
154             return ERROR.unsupp;
155 
156         switch (head.bitspp) {
157             case 8  : bytes_pp = 1; paletted = true; break;
158             case 24 : bytes_pp = 3; paletted = false; break;
159             case 32 : bytes_pp = 4; paletted = false; break;
160             default: return ERROR.unsupp;
161         }
162 
163         palettelen = head.palettelen;
164         rgb_masked = head.compress == CMP_BITS;
165         pe_bytes_pp = 4;
166     }
167 
168     int redi = 2;
169     int grei = 1;
170     int blui = 0;
171 
172     if (rgb_masked) {
173         if (head.dibv < 2)
174             return ERROR.data;
175         if (mask2idx(head.rmask, redi)
176          || mask2idx(head.gmask, grei)
177          || mask2idx(head.bmask, blui))
178             return ERROR.unsupp;
179     }
180 
181     bool alphamasked = false;
182     int alphai = 0;
183 
184     if (bytes_pp == 4 && head.dibv >= 3 && head.alphamask != 0) {
185         alphamasked = true;
186         if (mask2idx(head.alphamask, alphai))
187             return ERROR.unsupp;
188     }
189 
190     const int tchans = reqchans > 0 ? reqchans
191                                     : alphamasked ? CHANS.rgba
192                                                   : CHANS.rgb;
193 
194     // note: this does not directly match cinfile, see alphamasked
195     const int sfmt = paletted && pe_bytes_pp == 3 ? CHANS.bgr
196                                                   : CHANS.bgra;
197 
198     auto convert = cast(conv8) getconv(sfmt, tchans, 8);
199 
200     const int slinesz = head.w * bytes_pp;    // without padding
201     const int srcpad  = 3 - ((slinesz-1) % 4);
202     const int tlinesz = head.w * tchans;
203     const int tstride = head.h < 0 ? tlinesz : -tlinesz;
204     const int height  = abs(head.h);
205     const int ti_start  = head.h < 0 ? 0 : (head.h-1) * tlinesz;
206     const uint ti_limit = height * tlinesz;
207 
208     ubyte e;
209 
210     if (cast(ulong) head.w * height * tchans > MAXIMUM_IMAGE_SIZE)
211         return ERROR.bigimg;
212 
213     ubyte[] result       = new_buffer(head.w * height * tchans, e);
214     if (e) return e;
215     ubyte[] sline        = null;
216     ubyte[] xline        = null;  // intermediate buffer
217     ubyte[] palette      = null;
218     ubyte[] workbuf      = new_buffer(slinesz + srcpad + head.w * 4, e);
219     if (e) goto failure;
220     sline                = workbuf[0 .. slinesz + srcpad];
221     xline                = workbuf[sline.length .. sline.length + head.w * 4];
222 
223     if (paletted) {
224         palette = new_buffer(palettelen * pe_bytes_pp, e);
225         if (e) goto failure;
226         read_block(rc, palette[0..$]);
227     }
228 
229     skipto(rc, head.dataoff);
230 
231     if (rc.fail) {
232         e = ERROR.stream;
233         goto failure;
234     }
235 
236     if (!paletted) {
237         for (int ti = ti_start; cast(uint) ti < ti_limit; ti += tstride) {
238             read_block(rc, sline[0..$]);
239             for (size_t si, di;   si < slinesz;   si+=bytes_pp, di+=4) {
240                 xline[di + 0] = sline[si + blui];
241                 xline[di + 1] = sline[si + grei];
242                 xline[di + 2] = sline[si + redi];
243                 xline[di + 3] = alphamasked ? sline[si + alphai]
244                                             : 255;
245             }
246             convert(xline[0..$], result[ti .. ti + tlinesz]);
247         }
248     } else {
249         const int ps = pe_bytes_pp;
250         for (int ti = ti_start; cast(uint) ti < ti_limit; ti += tstride) {
251             read_block(rc, sline[0..$]);
252             int di = 0;
253             foreach (idx; sline[0 .. slinesz]) {
254                 if (idx > palettelen) {
255                     e = ERROR.data;
256                     goto failure;
257                 }
258                 const int i = idx * ps;
259                 xline[di + 0] = palette[i + 0];
260                 xline[di + 1] = palette[i + 1];
261                 xline[di + 2] = palette[i + 2];
262                 if (ps == 4)
263                     xline[di + 3] = 255;
264                 di += ps;
265             }
266             convert(xline[0..$], result[ti .. ti + tlinesz]);
267         }
268     }
269 
270     if (rc.fail) goto failure;
271 finish:
272     _free(workbuf.ptr);
273     _free(palette.ptr);
274     image.w = head.w;
275     image.h = abs(head.h);
276     image.c = cast(ubyte) tchans;
277     image.cinfile = head.dibv >= 3 && head.alphamask != 0 && head.bitspp == 32
278                   ? 4 : 3;
279     image.bpc = tbpc;
280     if (tbpc == 8) {
281         image.buf8 = result;
282     } else if (result) {
283         image.buf16 = bpc8to16(result);
284         if (!image.buf16.ptr && !e)
285             e = ERROR.oom;
286     }
287     return e;
288 failure:
289     _free(result.ptr);
290     result = null;
291     goto finish;
292 }
293 
294 bool mask2idx(in uint mask, out int index)
295 {
296     switch (mask) {
297         case 0xff00_0000: index = 3; return false;
298         case 0x00ff_0000: index = 2; return false;
299         case 0x0000_ff00: index = 1; return false;
300         case 0x0000_00ff: index = 0; return false;
301         default: return true;
302     }
303 }
304 
305 // Note: will only write RGB and RGBA images.
306 ubyte write_bmp(Writer* wc, int w, int h, in ubyte[] buf, int reqchans)
307 {
308     if (w < 1 || h < 1 || w > 0x7fff || h > 0x7fff)
309         return ERROR.dim;
310     const int schans = cast(int) (buf.length / w / h);
311     if (schans < 1 || schans > 4 || schans * w * h != buf.length)
312         return ERROR.dim;
313     if (reqchans != 0 && reqchans != 3 && reqchans != 4)
314         return ERROR.unsupp;
315 
316     const int tchans = reqchans ? reqchans
317                                 : schans == 1 || schans == 3 ? 3 : 4;
318 
319     const uint dibsize = 108;
320     const uint tlinesz = cast(size_t) (w * tchans);
321     const uint pad = 3 - ((tlinesz-1) % 4);
322     const uint idat_offset = 14 + dibsize;       // bmp file header + dib header
323     const size_t filesize = idat_offset + cast(size_t) h * (tlinesz + pad);
324     if (filesize > 0xffff_ffff)
325         return ERROR.bigimg;
326     const ubyte[64] zeros = 0;
327 
328     write_u8(wc, 'B');
329     write_u8(wc, 'M');
330     write_u32le(wc, cast(uint) filesize);
331     write_u32le(wc, 0);     // reserved
332     write_u32le(wc, idat_offset);
333     write_u32le(wc, dibsize);
334     write_u32le(wc, w);
335     write_u32le(wc, h);     // positive -> bottom-up
336     write_u16le(wc, 1);     // planes
337     write_u16le(wc, cast(ushort) (tchans * 8));   // bitspp
338     write_u32le(wc, tchans == 3 ? CMP_RGB : CMP_BITS);
339     write_block(wc, zeros[0..20]);      // rest of dibv1
340     if (tchans == 3) {
341         write_block(wc, zeros[0..16]);  // dibv2 and dibv3
342     } else {
343         static immutable ubyte[16] masks = [
344             0, 0, 0xff, 0,
345             0, 0xff, 0, 0,
346             0xff, 0, 0, 0,
347             0, 0, 0, 0xff
348         ];
349         write_block(wc, masks[0..$]);
350     }
351     write_u8(wc, 'B');
352     write_u8(wc, 'G');
353     write_u8(wc, 'R');
354     write_u8(wc, 's');
355     write_block(wc, zeros[0..48]);
356 
357     if (wc.fail)
358         return ERROR.stream;
359 
360     auto convert =
361         cast(conv8) getconv(schans, tchans == 3 ? CHANS.bgr : CHANS.bgra, 8);
362 
363     const size_t slinesz = cast(size_t) w * schans;
364     size_t si = cast(size_t) h * slinesz;
365 
366     ubyte e;
367     ubyte[] tline = new_buffer(tlinesz + pad, e);
368 
369     if (e)
370         goto finish;
371 
372     foreach (_; 0..h) {
373         si -= slinesz;
374         convert(buf[si .. si + slinesz], tline[0..tlinesz]);
375         write_block(wc, tline[0..$]);
376     }
377 
378     if (wc.fail)
379         e = ERROR.stream;
380 
381 finish:
382     _free(tline.ptr);
383     return e;
384 }