1 /**
2 BMP support.
3 
4 Copyright: Copyright Guillaume Piolat 2022
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module gamut.plugins.bmp;
8 
9 nothrow @nogc @safe:
10 
11 import core.stdc.stdlib: malloc, free, realloc;
12 import gamut.types;
13 import gamut.io;
14 import gamut.plugin;
15 import gamut.image;
16 import gamut.internals.errors;
17 import gamut.internals.types;
18 
19 
20 version(decodeBMP) import gamut.codecs.stbdec;
21 version(encodeBMP) import gamut.codecs.bmpenc;
22 
23 ImageFormatPlugin makeBMPPlugin()
24 {
25     ImageFormatPlugin p;
26     p.format = "BMP";
27     p.extensionList = "bmp,dib";
28     p.mimeTypes = "image/bmp";
29     version(decodeBMP)
30         p.loadProc = &loadBMP;
31     else
32         p.loadProc = null;
33     version(encodeBMP)
34         p.saveProc = &saveBMP;
35     else
36         p.saveProc = null;
37     p.detectProc = &detectBMP;
38     return p;
39 }
40 
41 // FUTURE: Note: detection API should report I/O errors other than yes/no for the test, 
42 // since stream might be fatally errored.
43 // Returning a ternary would be extra-nice.
44 
45 bool detectBMP(IOStream *io, IOHandle handle) @trusted
46 {
47     // save I/O cursor
48     c_long offset = io.tell(handle);
49     if (offset == -1) // IO error
50         return false;
51 
52     uint ds;
53     bool err;    
54     ubyte b = io.read_ubyte(handle, &err); if (err) return false; // IO error
55     if (b != 'B') goto no_match;
56 
57     b = io.read_ubyte(handle, &err); if (err) return false; // IO error
58     if (b != 'M') goto no_match;
59     
60     if (!io.skipBytes(handle, 12))
61         return false; // IO error
62 
63     ds = io.read_uint_LE(handle, &err); if (err) return false; // IO error
64 
65     if (ds == 12 || ds == 40 || ds == 52 || ds == 56 || ds == 108 || ds == 124)
66         goto match;
67     else
68         goto no_match;
69 
70 match:
71     // restore I/O cursor
72     if (!io.seekAbsolute(handle, offset))
73         return false; // IO error
74     return true;
75 
76 no_match:
77     // restore I/O cursor
78     if (!io.seekAbsolute(handle, offset))
79         return false; // IO error
80 
81     return false;
82 }
83 
84 version(decodeBMP)
85 void loadBMP(ref Image image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
86 {
87     // prepare STB callbacks
88     IOAndHandle ioh;
89     stbi_io_callbacks stb_callback;
90     initSTBCallbacks(io, handle, &ioh, &stb_callback);
91        
92     int requestedComp = computeRequestedImageComponents(flags);
93     if (requestedComp == 0) // error
94     {
95         image.error(kStrInvalidFlags);
96         return;
97     }
98     if (requestedComp == -1)
99         requestedComp = 0; // auto
100 
101     ubyte* decoded;
102     int width, height, components;
103 
104     float ppmX = -1;
105     float ppmY = -1;
106     float pixelRatio = -1;
107 
108     // PERF: let stb_image return a flipped bitmap, so that to save some time on load.
109 
110     decoded = stbi_load_from_callbacks(&stb_callback, &ioh, &width, &height, &components, requestedComp,
111                                        &ppmX, &ppmY, &pixelRatio);
112 
113     if (requestedComp != 0)
114         components = requestedComp;
115 
116     if (decoded is null)
117     {
118         image.error(kStrImageDecodingFailed);
119         return;
120     }
121 
122     if (!imageIsValidSize(1, width, height))
123     {
124         image.error(kStrImageTooLarge);
125         free(decoded);
126         return;
127     }
128 
129     image._allocArea = decoded; // Note: coupling, works because stb and gamut both use malloc/free
130     image._width = width;
131     image._height = height;
132     image._data = decoded; 
133     image._pitch = width * components;
134     image._pixelAspectRatio = (pixelRatio == -1) ? GAMUT_UNKNOWN_ASPECT_RATIO : pixelRatio;
135     image._resolutionY = (ppmY == -1) ? GAMUT_UNKNOWN_RESOLUTION : convertInchesToMeters(ppmY);
136     image._layoutConstraints = LAYOUT_DEFAULT; // STB decoder follows no particular constraints (TODO?)
137     image._layerCount = 1;
138     image._layerOffset = 0;
139 
140     if (components == 1)
141     {
142         image._type = PixelType.l8;
143     }
144     else if (components == 2)
145     {
146         image._type = PixelType.la8;
147     }
148     else if (components == 3)
149     {
150         image._type = PixelType.rgb8;
151     }
152     else if (components == 4)
153     {
154         image._type = PixelType.rgba8;
155     }
156     else
157         assert(false);
158 
159     PixelType targetType = applyLoadFlags(image._type, flags);
160 
161     // Convert to target type and constraints
162     image.convertTo(targetType, cast(LayoutConstraints) flags);
163 }
164 
165 version(encodeBMP)
166 bool saveBMP(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
167 {
168     if (page != 0)
169         return false;    
170 
171     int components;
172 
173     // For now, can save RGB and RGBA 8-bit images.
174     switch (image._type)
175     {
176         case PixelType.rgb8:
177             components = 3; break;
178         case PixelType.rgba8:
179             components = 4; 
180             break;
181         default:
182             return false;
183     }
184 
185     int width = image._width;
186     int height = image._height;
187     int pitch = image._pitch;
188     if (width < 1 || height < 1 || width > 32767 || height > 32767)
189         return false; // Can't be saved as BMP
190 
191     bool success = write_bmp(image, io, handle, width, height, components);
192 
193     return success;
194 }
195