1 module daffodil.bmp; 2 3 public { 4 import daffodil.bmp.meta; 5 6 static { 7 import headers = daffodil.bmp.headers; 8 } 9 } 10 11 import std.math; 12 import std.traits; 13 import std.bitmanip; 14 import std.typecons; 15 import std.algorithm; 16 import core.bitop; 17 18 import daffodil; 19 import daffodil.util.data; 20 import daffodil.util.range; 21 import daffodil.util.errors; 22 import daffodil.util.headers; 23 24 import daffodil.bmp.headers; 25 26 /// Register this file format with the common api 27 static this() { 28 registerFormat(Format( 29 "BMP", 30 &check!DataRange, 31 &loadMeta!DataRange, 32 (d, m) => loadImage!DataRange(d, cast(BmpMetaData)m).imageRangeObject, 33 (d, i, m) => saveImage!(OutputRange!ubyte, RandomAccessImageRange!(real[]))(d, i, cast(BmpMetaData)m), 34 [".bmp", ".dib"], 35 typeid(BmpMetaData), 36 )); 37 } 38 39 // Magic Number "BM" 40 const ubyte[] MAGIC_NUMBER = [0x42, 0x4D]; 41 42 /** 43 * Documentation 44 */ 45 bool check(R)(R data) if (isInputRange!R && 46 is(ElementType!R == ubyte)) { 47 auto taken = data.take(2).array; 48 enforce!UnexpectedEndOfData(taken.length == 2); 49 // Make sure data starts with the magic number 50 return taken == MAGIC_NUMBER; 51 } 52 /// Ditto 53 bool check(T)(T loadeable) if (isLoadeable!T) { 54 return check(dataLoad(loadeable)); 55 } 56 57 @("BMP file format check") 58 unittest { 59 assert( check(cast(ubyte[])[0x42, 0x4D, 0x32, 0x7D, 0xFA, 0x9E])); 60 assert(!check(cast(ubyte[])[0x43, 0x4D, 0x32, 0x7D, 0xFA, 0x9E])); 61 assert(!check(cast(ubyte[])[0x42, 0x4C, 0x32, 0x7D, 0xFA, 0x9E])); 62 } 63 64 /** 65 * Documentation 66 */ 67 auto load(V = real, T : DataRange)(T data, BmpMetaData meta = null) { 68 enforce!InvalidImageType(check(data), "Data does not contain a bmp image."); 69 70 if (meta is null) meta = loadMeta(data); 71 return new Image!V(loadImage(data, meta), meta); 72 } 73 /// Ditto 74 auto load(V = real, T)(T loadeable) if (isLoadeable!T) { 75 return load!V(dataLoad(loadeable)); 76 } 77 78 // The default rgba masks for common formats 79 private enum DEFAULT_MASKS = [ 80 16 : tuple(0x0F_00_00_00u, 0x00_F0_00_00u, 0x00_0F_00_00u, 0xF0_00_00_00u), 81 24 : tuple(0x00_00_FF_00u, 0x00_FF_00_00u, 0xFF_00_00_00u, 0x00_00_00_00u), 82 32 : tuple(0x00_FF_00_00u, 0x00_00_FF_00u, 0x00_00_00_FFu, 0xFF_00_00_00u), 83 ]; 84 85 /** 86 * Documentation 87 */ 88 BmpMetaData loadMeta(R)(R data) if (isInputRange!R && 89 is(ElementType!R == ubyte)) { 90 auto bmpHeader = parseHeader!BmpHeader(data); 91 auto dibVersion = cast(DibVersion)parseHeader!uint(data); 92 93 // Parse dib header according to version, but store in most complex version 94 DibHeader!() dibHeader; 95 foreach (ver; EnumMembers!DibVersion) { 96 if (dibVersion == ver) { 97 dibHeader = cast(DibHeader!())parseHeader!(DibHeader!ver)(data); 98 } 99 } 100 101 // Validation 102 alias checkValid = enforce!(InvalidHeader, bool); 103 104 checkValid(dibVersion > DibVersion.CORE || dibHeader.bitCount < 16, 105 "Old BMP image header does not support bpp > 8"); 106 checkValid(dibHeader.planes == 1, "BMP image can only have one color plane."); 107 checkValid(dibHeader.compression == CompressionMethod.RGB || dibHeader.dataSize > 0, 108 "Invalid data size for compression method"); 109 checkValid(dibHeader.width > 0, "BMP image width must be positive"); 110 checkValid(dibHeader.height != 0, "BMP image height must not be 0"); 111 112 checkValid(dibHeader.dataSize == bmpHeader.size - bmpHeader.contentOffset, 113 "BMP header's image size does not match DIB header's"); 114 115 // Calculate raster data sizes 116 uint rowSize = (dibHeader.bitCount * dibHeader.width + 31)/32 * 4; 117 uint columnSize = rowSize * dibHeader.height; 118 checkValid(dibHeader.dataSize == columnSize, 119 "BMP data size does not match image dimensions"); 120 121 // Compressions methods are not yet supported 122 enforce!NotSupported(dibHeader.compression == CompressionMethod.RGB || 123 dibHeader.compression == CompressionMethod.BITFIELDS); 124 125 // V5 has a ICC color profile, not yet supported 126 enforce!NotSupported(dibVersion != DibVersion.V5); 127 128 // Color tables are not yet supported 129 enforce!NotSupported(dibHeader.colorsUsed == 0); 130 enforce!NotSupported(dibHeader.bitCount > 8); 131 132 // Use special color mask for special compression method 133 if (dibHeader.compression == CompressionMethod.BITFIELDS) { 134 auto mask = parseHeader!(DibColorMask!false)(data); 135 dibHeader.redMask = mask.redMask; 136 dibHeader.greenMask = mask.greenMask; 137 dibHeader.blueMask = mask.blueMask; 138 } 139 // Default RGB masks, as early versions didn't have them 140 else if (dibVersion <= DibVersion.INFO) { 141 checkValid((dibHeader.bitCount in DEFAULT_MASKS) != null, 142 "BMP header uses non-standard bpp without color masks"); 143 auto mask = DEFAULT_MASKS[dibHeader.bitCount]; 144 dibHeader.redMask = mask[0]; 145 dibHeader.greenMask = mask[1]; 146 dibHeader.blueMask = mask[2]; 147 dibHeader.alphaMask = mask[3]; 148 } 149 150 uint[] masks = [dibHeader.redMask, dibHeader.greenMask, dibHeader.blueMask]; 151 if (dibHeader.alphaMask != 0) { 152 masks ~= dibHeader.alphaMask; 153 } 154 155 // Validate color masks 156 foreach (mask; masks) { 157 checkValid(mask != 0, "Color mask is 0"); 158 } 159 160 return new BmpMetaData(dibHeader.width, abs(dibHeader.height), 161 bmpHeader, dibVersion, dibHeader); 162 } 163 /// Ditto 164 auto loadMeta(T)(T loadeable) if (isLoadeable!T) { 165 return loadMeta(dataLoad(loadeable)); 166 } 167 168 /** 169 * Documentation 170 */ 171 auto loadImage(R)(R data, BmpMetaData meta) if (isInputRange!R && 172 is(ElementType!R == ubyte)) { 173 enforce!ImageException(meta !is null, "Cannot load bmp Image without bmp Meta Data"); 174 175 auto dib = meta.dibHeader; 176 uint[] masks = [dib.redMask, dib.greenMask, dib.blueMask]; 177 if (dib.alphaMask != 0) { 178 masks ~= dib.alphaMask; 179 } 180 181 return maskedRasterLoad(data, masks, dib.bitCount, 182 dib.width, -dib.height, RGB, 4); 183 } 184 /// Ditto 185 auto loadImage(T)(T loadeable, BmpMetaData meta) if (isLoadeable!T) { 186 return loadImage(dataLoad(loadeable), meta); 187 } 188 189 /** 190 * Documentation 191 */ 192 void save(V = real, R)(Image!V image, R output) if (isOutputRange!(R, ubyte)) { 193 saveImage(output, image.range, cast(BmpMetaData)image.meta); 194 } 195 /// Ditto 196 void save(V = real, T)(Image!V image, T saveable) if (isSaveable!T) { 197 save(image, dataSave(saveable)); 198 } 199 200 /** 201 * Documentation 202 */ 203 void saveImage(R, I)(R output, I image, BmpMetaData meta) if (isOutputRange!(R, ubyte) && 204 isRandomAccessImageRange!I) { 205 if (meta is null) { 206 // TODO: Default 207 assert(false); 208 } 209 210 put(output, MAGIC_NUMBER[]); 211 // TODO: Validation 212 writeHeader(meta.bmpHeader, output); 213 writeHeader(meta.dibVersion, output); 214 215 foreach (ver; EnumMembers!DibVersion) { 216 if (meta.dibVersion == ver) { 217 writeHeader(cast(DibHeader!ver)meta.dibHeader, output); 218 } 219 } 220 221 auto dib = meta.dibHeader; 222 uint[] masks = [dib.redMask, dib.greenMask, dib.blueMask]; 223 if (dib.alphaMask != 0) { 224 masks ~= dib.alphaMask; 225 } 226 227 maskedRasterSave(image, output, masks, dib.bitCount, dib.width, -dib.height, 4); 228 } 229 /// Ditto 230 void saveImage(T, I)(T saveable, I image, BmpMetaData meta) if (isSaveable!T && 231 isRandomAccessImageRange!I) { 232 return saveImage(dataSave(saveable), image, meta); 233 }