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 }