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 }