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