1 // Copyright 2008-present Contributors to the OpenImageIO project.
2 // SPDX-License-Identifier: BSD-3-Clause
3 // https://github.com/OpenImageIO/oiio/blob/master/LICENSE.md
4 
5 #include <cstdio>
6 
7 #include <OpenImageIO/filesystem.h>
8 #include <OpenImageIO/imageio.h>
9 
10 #include "bmp_pvt.h"
11 
12 OIIO_PLUGIN_NAMESPACE_BEGIN
13 
14 using namespace bmp_pvt;
15 
16 
17 class BmpInput final : public ImageInput {
18 public:
BmpInput()19     BmpInput() { init(); }
~BmpInput()20     virtual ~BmpInput() { close(); }
format_name(void) const21     virtual const char* format_name(void) const override { return "bmp"; }
22     virtual bool valid_file(const std::string& filename) const override;
23     virtual bool open(const std::string& name, ImageSpec& newspec) override;
24     virtual bool open(const std::string& name, ImageSpec& newspec,
25                       const ImageSpec& config) override;
26     virtual bool close(void) override;
27     virtual bool read_native_scanline(int subimage, int miplevel, int y, int z,
28                                       void* data) override;
29 
30 private:
31     int64_t m_padded_scanline_size;
32     int m_pad_size;
33     FILE* m_fd;
34     bmp_pvt::BmpFileHeader m_bmp_header;
35     bmp_pvt::DibInformationHeader m_dib_header;
36     std::string m_filename;
37     std::vector<bmp_pvt::color_table> m_colortable;
38     std::vector<unsigned char> fscanline;       // temp space: read from file
39     std::vector<unsigned char> m_uncompressed;  // uncompressed palette image
40     bool m_allgray;
41 
init(void)42     void init(void)
43     {
44         m_padded_scanline_size = 0;
45         m_pad_size             = 0;
46         m_fd                   = NULL;
47         m_filename.clear();
48         m_colortable.clear();
49         m_allgray = false;
50         fscanline.shrink_to_fit();
51         m_uncompressed.shrink_to_fit();
52     }
53 
54     bool read_color_table();
55     bool color_table_is_all_gray();
56     bool read_rle_image();
57 };
58 
59 
60 
61 // Obligatory material to make this a recognizeable imageio plugin
62 OIIO_PLUGIN_EXPORTS_BEGIN
63 
64 OIIO_EXPORT int bmp_imageio_version = OIIO_PLUGIN_VERSION;
65 
66 OIIO_EXPORT const char*
bmp_imageio_library_version()67 bmp_imageio_library_version()
68 {
69     return nullptr;
70 }
71 
72 OIIO_EXPORT ImageInput*
bmp_input_imageio_create()73 bmp_input_imageio_create()
74 {
75     return new BmpInput;
76 }
77 
78 OIIO_EXPORT const char* bmp_input_extensions[] = { "bmp", "dib", nullptr };
79 
80 OIIO_PLUGIN_EXPORTS_END
81 
82 
83 bool
valid_file(const std::string & filename) const84 BmpInput::valid_file(const std::string& filename) const
85 {
86     FILE* fd = Filesystem::fopen(filename, "rb");
87     if (!fd)
88         return false;
89     bmp_pvt::BmpFileHeader bmp_header;
90     bool ok = bmp_header.read_header(fd) && bmp_header.isBmp();
91     fclose(fd);
92     return ok;
93 }
94 
95 
96 
97 bool
open(const std::string & name,ImageSpec & newspec)98 BmpInput::open(const std::string& name, ImageSpec& newspec)
99 {
100     ImageSpec emptyconfig;
101     return open(name, newspec, emptyconfig);
102 }
103 
104 
105 
106 bool
open(const std::string & name,ImageSpec & newspec,const ImageSpec & config)107 BmpInput::open(const std::string& name, ImageSpec& newspec,
108                const ImageSpec& config)
109 {
110     // saving 'name' for later use
111     m_filename = name;
112 
113     // BMP cannot be 1-channel, but config hint "bmp:monochrome_detect" is a
114     // hint to try to detect when all palette entries are gray and pretend
115     // that it's a 1-channel image to allow the calling app to save memory
116     // and time. It does this by default, but setting the hint to 0 turns
117     // this behavior off.
118     bool monodetect = config["bmp:monochrome_detect"].get<int>(1);
119 
120     m_fd = Filesystem::fopen(m_filename, "rb");
121     if (!m_fd) {
122         errorf("Could not open file \"%s\"", name);
123         return false;
124     }
125 
126     // we read header of the file that we think is BMP file
127     if (!m_bmp_header.read_header(m_fd)) {
128         errorf("\"%s\": wrong bmp header size", m_filename);
129         close();
130         return false;
131     }
132     if (!m_bmp_header.isBmp()) {
133         errorf("\"%s\" is not a BMP file, magic number doesn't match",
134                m_filename);
135         close();
136         return false;
137     }
138     if (!m_dib_header.read_header(m_fd)) {
139         errorf("\"%s\": wrong bitmap header size", m_filename);
140         close();
141         return false;
142     }
143 
144     const int nchannels = (m_dib_header.bpp == 32) ? 4 : 3;
145     const int height    = (m_dib_header.height >= 0) ? m_dib_header.height
146                                                      : -m_dib_header.height;
147     m_spec = ImageSpec(m_dib_header.width, height, nchannels, TypeDesc::UINT8);
148     if (m_dib_header.hres > 0 && m_dib_header.vres > 0) {
149         m_spec.attribute("XResolution", (int)m_dib_header.hres);
150         m_spec.attribute("YResolution", (int)m_dib_header.vres);
151         m_spec.attribute("ResolutionUnit", "m");
152     }
153 
154     // computing size of one scanline - this is the size of one scanline that
155     // is stored in the file, not in the memory
156     int swidth = 0;
157     switch (m_dib_header.bpp) {
158     case 32:
159     case 24:
160         m_padded_scanline_size = ((m_spec.width * m_spec.nchannels) + 3) & ~3;
161         break;
162     case 16:
163         m_padded_scanline_size = ((m_spec.width << 1) + 3) & ~3;
164         m_spec.attribute("oiio:BitsPerSample", 4);
165         break;
166     case 8:
167         m_padded_scanline_size = (m_spec.width + 3) & ~3;
168         if (!read_color_table())
169             return false;
170         m_allgray = monodetect && color_table_is_all_gray();
171         if (m_allgray) {
172             m_spec.nchannels = 1;  // make it look like a 1-channel image
173             m_spec.default_channel_names();
174         }
175         break;
176     case 4:
177         swidth                 = (m_spec.width + 1) / 2;
178         m_padded_scanline_size = (swidth + 3) & ~3;
179         if (!read_color_table())
180             return false;
181         break;
182     case 1:
183         swidth                 = (m_spec.width + 7) / 8;
184         m_padded_scanline_size = (swidth + 3) & ~3;
185         if (!read_color_table())
186             return false;
187         break;
188     }
189     if (m_dib_header.bpp <= 16)
190         m_spec.attribute("bmp:bitsperpixel", m_dib_header.bpp);
191     switch (m_dib_header.size) {
192     case OS2_V1: m_spec.attribute("bmp:version", 1); break;
193     case WINDOWS_V3: m_spec.attribute("bmp:version", 3); break;
194     case WINDOWS_V4: m_spec.attribute("bmp:version", 4); break;
195     case WINDOWS_V5: m_spec.attribute("bmp:version", 5); break;
196     }
197 
198     // Bite the bullet and uncompress now, for simplicity
199     if (m_dib_header.compression == RLE4_COMPRESSION
200         || m_dib_header.compression == RLE8_COMPRESSION) {
201         if (!read_rle_image()) {
202             errorfmt("BMP error reading rle-compressed image");
203             close();
204             return false;
205         }
206     }
207 
208     if (m_spec.width < 1 || m_spec.height < 1 || m_spec.nchannels < 1
209         || m_spec.image_bytes() < 1) {
210         errorfmt("Invalid image size {} x {} ({} chans, {})", m_spec.width,
211                  m_spec.height, m_spec.nchannels, m_spec.format);
212         return false;
213     }
214 
215     newspec = m_spec;
216     return true;
217 }
218 
219 
220 
221 bool
read_rle_image()222 BmpInput::read_rle_image()
223 {
224     int rletype = m_dib_header.compression == RLE4_COMPRESSION ? 4 : 8;
225     m_spec.attribute("bmp:compression", rletype == 4 ? "rle4" : "rle8");
226     m_uncompressed.clear();
227     m_uncompressed.resize(m_spec.height * m_spec.width);
228     // Note: the clear+resize zeroes out the buffer
229     bool err = false;
230     int y = 0, x = 0;
231     while (!err && !feof(m_fd)) {
232         unsigned char rle_pair[2];
233         if (fread(rle_pair, 1, 2, m_fd) != 2) {
234             err = true;
235             break;
236         }
237         int npixels = rle_pair[0];
238         int value   = rle_pair[1];
239         if (npixels == 0 && value == 0) {
240             // [0,0] is end of line marker
241             x = 0;
242             ++y;
243         } else if (npixels == 0 && value == 1) {
244             // [0,1] is end of bitmap marker
245             break;
246         } else if (npixels == 0 && value == 2) {
247             // [0,2] is a "delta" -- two more bytes reposition the
248             // current pixel position that we're reading.
249             unsigned char offset[2];
250             err |= (fread(offset, 1, 2, m_fd) != 2);
251             x += offset[0];
252             y += offset[1];
253         } else if (npixels == 0) {
254             // [0,n>2] is an "absolute" run of pixel data.
255             // n is the number of pixel indices that follow, but note
256             // that it pads to word size.
257             int npixels = value;
258             int nbytes  = (rletype == 4)
259                               ? round_to_multiple((npixels + 1) / 2, 2)
260                               : round_to_multiple(npixels, 2);
261             unsigned char absolute[256];
262             err |= (fread(absolute, 1, nbytes, m_fd) != size_t(nbytes));
263             for (int i = 0; i < npixels; ++i, ++x) {
264                 if (rletype == 4)
265                     value = (i & 1) ? (absolute[i / 2] & 0x0f)
266                                     : (absolute[i / 2] >> 4);
267                 else
268                     value = absolute[i];
269                 if (x < m_spec.width)
270                     m_uncompressed[y * m_spec.width + x] = value;
271             }
272         } else {
273             // [n>0,p] is a run of n pixels.
274             for (int i = 0; i < npixels; ++i, ++x) {
275                 int v;
276                 if (rletype == 4)
277                     v = (i & 1) ? (value & 0x0f) : (value >> 4);
278                 else
279                     v = value;
280                 if (x < m_spec.width)
281                     m_uncompressed[y * m_spec.width + x] = v;
282             }
283         }
284     }
285     return !err;
286 }
287 
288 
289 
290 bool
read_native_scanline(int subimage,int miplevel,int y,int,void * data)291 BmpInput::read_native_scanline(int subimage, int miplevel, int y, int /*z*/,
292                                void* data)
293 {
294     lock_guard lock(m_mutex);
295     if (!seek_subimage(subimage, miplevel))
296         return false;
297 
298     if (y < 0 || y > m_spec.height)
299         return false;
300 
301     size_t scanline_bytes = m_spec.scanline_bytes();
302     uint8_t* mscanline    = (uint8_t*)data;
303     if (m_dib_header.compression == RLE4_COMPRESSION
304         || m_dib_header.compression == RLE8_COMPRESSION) {
305         for (int x = 0; x < m_spec.width; ++x) {
306             int p = m_uncompressed[(m_spec.height - 1 - y) * m_spec.width + x];
307             mscanline[3 * x]     = m_colortable[p].r;
308             mscanline[3 * x + 1] = m_colortable[p].g;
309             mscanline[3 * x + 2] = m_colortable[p].b;
310         }
311         return true;
312     }
313 
314     // if the height is positive scanlines are stored bottom-up
315     if (m_dib_header.height >= 0)
316         y = m_spec.height - y - 1;
317     const int64_t scanline_off = y * m_padded_scanline_size;
318 
319     fscanline.resize(m_padded_scanline_size);
320     Filesystem::fseek(m_fd, m_bmp_header.offset + scanline_off, SEEK_SET);
321     size_t n = fread(fscanline.data(), 1, m_padded_scanline_size, m_fd);
322     if (n != (size_t)m_padded_scanline_size) {
323         if (feof(m_fd))
324             errorf("Hit end of file unexpectedly");
325         else
326             errorf("read error");
327         return false;  // Read failed
328     }
329 
330     // in each case we process only first m_spec.scanline_bytes () bytes
331     // as only they contain information about pixels. The rest are just
332     // because scanline size have to be 32-bit boundary
333     if (m_dib_header.bpp == 24 || m_dib_header.bpp == 32) {
334         for (unsigned int i = 0; i < m_spec.scanline_bytes();
335              i += m_spec.nchannels)
336             std::swap(fscanline[i], fscanline[i + 2]);
337         memcpy(data, fscanline.data(), m_spec.scanline_bytes());
338         return true;
339     }
340 
341     if (m_dib_header.bpp == 16) {
342         const uint16_t RED   = 0x7C00;
343         const uint16_t GREEN = 0x03E0;
344         const uint16_t BLUE  = 0x001F;
345         for (unsigned int i = 0, j = 0; j < scanline_bytes; i += 2, j += 3) {
346             uint16_t pixel   = (uint16_t) * (&fscanline[i]);
347             mscanline[j]     = (uint8_t)((pixel & RED) >> 8);
348             mscanline[j + 1] = (uint8_t)((pixel & GREEN) >> 4);
349             mscanline[j + 2] = (uint8_t)(pixel & BLUE);
350         }
351     }
352     if (m_dib_header.bpp == 8) {
353         if (m_allgray) {
354             // Keep it as 1-channel image because all colors are gray
355             for (unsigned int i = 0; i < scanline_bytes; ++i) {
356                 mscanline[i] = m_colortable[fscanline[i]].r;
357             }
358         } else {
359             // Expand palette image into 3-channel RGB (existing code)
360             for (unsigned int i = 0, j = 0; j < scanline_bytes; ++i, j += 3) {
361                 mscanline[j]     = m_colortable[fscanline[i]].r;
362                 mscanline[j + 1] = m_colortable[fscanline[i]].g;
363                 mscanline[j + 2] = m_colortable[fscanline[i]].b;
364             }
365         }
366     }
367     if (m_dib_header.bpp == 4) {
368         for (unsigned int i = 0, j = 0; j < scanline_bytes; ++i, j += 6) {
369             uint8_t mask     = 0xF0;
370             mscanline[j]     = m_colortable[(fscanline[i] & mask) >> 4].r;
371             mscanline[j + 1] = m_colortable[(fscanline[i] & mask) >> 4].g;
372             mscanline[j + 2] = m_colortable[(fscanline[i] & mask) >> 4].b;
373             if (j + 3 >= scanline_bytes)
374                 break;
375             mask             = 0x0F;
376             mscanline[j + 3] = m_colortable[fscanline[i] & mask].r;
377             mscanline[j + 4] = m_colortable[fscanline[i] & mask].g;
378             mscanline[j + 5] = m_colortable[fscanline[i] & mask].b;
379         }
380     }
381     if (m_dib_header.bpp == 1) {
382         for (int64_t i = 0, k = 0; i < m_padded_scanline_size; ++i) {
383             for (int j = 7; j >= 0; --j, k += 3) {
384                 if (size_t(k + 2) >= scanline_bytes)
385                     break;
386                 int index = 0;
387                 if (fscanline[i] & (1 << j))
388                     index = 1;
389                 mscanline[k]     = m_colortable[index].r;
390                 mscanline[k + 1] = m_colortable[index].g;
391                 mscanline[k + 2] = m_colortable[index].b;
392             }
393         }
394     }
395     return true;
396 }
397 
398 
399 
close(void)400 bool inline BmpInput::close(void)
401 {
402     if (m_fd) {
403         fclose(m_fd);
404         m_fd = NULL;
405     }
406     init();
407     return true;
408 }
409 
410 
411 
412 bool
read_color_table(void)413 BmpInput::read_color_table(void)
414 {
415     // size of color table is defined  by m_dib_header.cpalete
416     // if this field is 0 - color table has max colors:
417     // pow(2, m_dib_header.cpalete) otherwise color table have
418     // m_dib_header.cpalete entries
419     if (m_dib_header.cpalete < 0
420         || m_dib_header.cpalete > (1 << m_dib_header.bpp)) {
421         errorf("Possible corrupted header, invalid palette size");
422         return false;
423     }
424     const int32_t colors = (m_dib_header.cpalete) ? m_dib_header.cpalete
425                                                   : 1 << m_dib_header.bpp;
426     size_t entry_size    = 4;
427     // if the file is OS V2 bitmap color table entry has only 3 bytes, not four
428     if (m_dib_header.size == OS2_V1)
429         entry_size = 3;
430     m_colortable.resize(colors);
431     for (int i = 0; i < colors; i++) {
432         size_t n = fread(&m_colortable[i], 1, entry_size, m_fd);
433         if (n != entry_size) {
434             if (feof(m_fd))
435                 errorfmt(
436                     "Hit end of file unexpectedly while reading color table on color {}/{} (read {}, expected {})",
437                     i, colors, n, entry_size);
438             else
439                 errorf("read error while reading color table");
440             return false;  // Read failed
441         }
442     }
443     return true;  // ok
444 }
445 
446 bool
color_table_is_all_gray(void)447 BmpInput::color_table_is_all_gray(void)
448 {
449     size_t ncolors = m_colortable.size();
450     for (size_t i = 0; i < ncolors; i++) {
451         color_table& color = m_colortable[i];
452         if (color.b != color.g || color.g != color.r)
453             return false;
454     }
455     return true;
456 }
457 
458 OIIO_PLUGIN_NAMESPACE_END
459