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 }