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