191 lines
8.3 KiB
C++
191 lines
8.3 KiB
C++
// SPDX-FileCopyrightText: 2023 Phillip Burgess for Adafruit Industries
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// BMP-reading code. Much of this is adapted from the Adafruit_ImageReader
|
|
// library, which is extremely LCD-display-centric and turned out to be
|
|
// neither workable nor easily extensible into what was needed here. That's
|
|
// okay. For now, I just pulled in minimal parts of that code, with changes
|
|
// as needed for this project (and dropping some portability considerations).
|
|
|
|
#include <SdFat.h>
|
|
|
|
// Barebones query pixel height of BMP image file. NOT universally portable;
|
|
// does some rude straight-to-var little-endian reads.
|
|
// Param: Pointer to FAT volume.
|
|
// Param: Absolute filename.
|
|
// Param: Pointer to int32_t for height result.
|
|
// Returns: true on success, false on ANY error, does not distinguish
|
|
// (file not found, no BMP signature, etc.).
|
|
bool bmpHeight(FatVolume *fs, char *filename, int32_t *height) {
|
|
File32 file;
|
|
bool status = false; // Assume error until success
|
|
if ((file = fs->open(filename, FILE_READ))) {
|
|
uint16_t sig;
|
|
file.read(&sig, sizeof sig); // Little-endian straight to var
|
|
if (sig == 0x4D42) { // BMP signature?
|
|
file.seekCur(20); // Skip file size, width, etc.
|
|
file.read(height, sizeof(int32_t)); // Little-endian straight to var
|
|
if (*height < 0) *height *= -1; // Handle top-to-bottom variant
|
|
status = true; // YAY
|
|
}
|
|
file.close();
|
|
}
|
|
return status;
|
|
}
|
|
|
|
// Barebones BMP read into RAM. ALL images regardless of BMP format are
|
|
// converted as needed into a DotStar-ready 24-bit-per-pixel format. Again
|
|
// this is NOT universally portable; rude little-endian reads.
|
|
// Param: Pointer to FAT volume.
|
|
// Param: Absolute filename.
|
|
// Param: Pointer to uint8_t for storing result (destination buffer
|
|
// is assumed allocated and cleared, not performed here).
|
|
// Param: Max width to clip or pad to (DotStar strip length).
|
|
// Param: Red, green, blue byte offsets (0-2) in dest buffer.
|
|
// Param: Brightness (0.0 to 255.0).
|
|
// Param: Gamma (1.0 = linear, 2.6 = typical LED curve). Gamma is a lossy
|
|
// operation; might prefer to pass 1.0 and do adjustment on source
|
|
// image instead w/dithering, or perhaps future poi code could do
|
|
// this on-the-fly. But for now, on load.
|
|
// Returns: true on success, false on ANY error, does not distinguish
|
|
// (file not found, no BMP signature, etc.).
|
|
bool loadBMP(FatVolume *fs, char *filename, uint8_t *dest,
|
|
const uint16_t dest_width, const uint8_t rOffset,
|
|
const uint8_t gOffset, const uint8_t bOffset,
|
|
const float brightness, const float gamma) {
|
|
File32 file;
|
|
bool status = false; // Assume error until success
|
|
if ((file = fs->open(filename, FILE_READ))) {
|
|
uint16_t sig;
|
|
file.read(&sig, sizeof sig); // Little-endian straight to var
|
|
if (sig == 0x4D42) { // BMP signature?
|
|
uint32_t offset; // Start of image data
|
|
uint32_t header_size; // Indicates BMP version
|
|
int32_t bmp_width, bmp_height; // BMP width & height in pixels
|
|
boolean flip = true; // BMP is stored bottom-to-top
|
|
uint32_t compression = 0; // BMP compression mode
|
|
uint32_t colors = 0; // Number of colors in palette
|
|
uint16_t planes;
|
|
uint16_t depth;
|
|
|
|
file.seekCur(8); // Skip file size, creator bytes
|
|
file.read(&offset , sizeof offset);
|
|
file.read(&header_size, sizeof header_size); // DIB header...
|
|
file.read(&bmp_width , sizeof bmp_width);
|
|
file.read(&bmp_height , sizeof bmp_height);
|
|
// If bmpHeight is negative, image is in top-down order.
|
|
// This is not canon but has been observed in the wild.
|
|
if (bmp_height < 0) {
|
|
bmp_height *= -1;
|
|
flip = false;
|
|
}
|
|
file.read(&planes, sizeof planes);
|
|
file.read(&depth , sizeof depth);
|
|
// Compression mode is present in later BMP versions (default = none)
|
|
if (header_size > 12) {
|
|
file.read(&compression, sizeof compression);
|
|
file.seekCur(12); // Skip raw bitmap data size, etc.
|
|
file.read(&colors, sizeof colors); // # of colors in palette; 0 = 2^depth
|
|
file.seekCur(4); // Skip # of colors used
|
|
// File position should now be at start of palette (if present)
|
|
}
|
|
|
|
if ((planes == 1) && (compression == 0)) { // Only uncompressed is handled
|
|
uint8_t palette[3][256]; // Rude but code's easier than malloc check
|
|
uint8_t b; // Byte-holding var used in 1-8 bit modes
|
|
|
|
if (depth < 16) { // Lower depths include a color palette
|
|
if (!colors) colors = 1 << depth;
|
|
for (uint16_t i = 0; i < colors; i++) {
|
|
uint32_t rgb;
|
|
file.read(&rgb, sizeof rgb);
|
|
palette[rOffset][i] = (uint8_t)(pow((float)((rgb >> 16) & 0xFF) / 255.0, gamma) * brightness + 0.5);
|
|
palette[gOffset][i] = (uint8_t)(pow((float)((rgb >> 8) & 0xFF) / 255.0, gamma) * brightness + 0.5);
|
|
palette[bOffset][i] = (uint8_t)(pow((float)( rgb & 0xFF) / 255.0, gamma) * brightness + 0.5);
|
|
}
|
|
} else {
|
|
// But HEY, as long as we have that palette array taking up space
|
|
// on the heap...use it to pre-compute a brightness/gamma table,
|
|
// saves a TON of floating-point math on every pixel later.
|
|
for (uint16_t i = 0; i < 256; i++) {
|
|
palette[0][i] = (uint8_t)(pow((float)i / 255.0, gamma) * brightness + 0.5);
|
|
}
|
|
}
|
|
|
|
// BMP rows are padded (if needed) to 4-byte boundary,
|
|
// width loaded is cropped if needed to DotStar strand length.
|
|
uint32_t row_size = ((depth * bmp_width + 31) / 32) * 4;
|
|
int load_width = min(bmp_width, dest_width);
|
|
|
|
for (int row = 0; row < bmp_height; row++) { // For each scanline...
|
|
|
|
file.seekSet(offset + row_size * (flip ? bmp_height - 1 - row : row));
|
|
uint8_t *d2 = dest + row * dest_width * 3;
|
|
|
|
switch (depth) {
|
|
case 32:
|
|
for (int col = 0; col < load_width; col++, d2 += 3) {
|
|
uint32_t rgba;
|
|
file.read(&rgba, sizeof rgba);
|
|
d2[rOffset] = palette[0][(rgba >> 16) & 0xFF]; // palette[0] is
|
|
d2[gOffset] = palette[0][(rgba >> 8) & 0xFF]; // brightness/gamma
|
|
d2[bOffset] = palette[0][ rgba & 0xFF]; // adjustment table
|
|
}
|
|
break;
|
|
case 24:
|
|
for (int col = 0; col < load_width; col++, d2 += 3) {
|
|
d2[bOffset] = palette[0][file.read()]; // palette[0] is
|
|
d2[gOffset] = palette[0][file.read()]; // brightness/gamma
|
|
d2[rOffset] = palette[0][file.read()]; // adjustment table
|
|
}
|
|
break;
|
|
case 16:
|
|
// Not currently supported but might be nice to have.
|
|
// Will require dissecting DIB header bitfields.
|
|
break;
|
|
case 8:
|
|
for (int col = 0; col < load_width; col++, d2 += 3) {
|
|
b = file.read();
|
|
d2[0] = palette[0][b];
|
|
d2[1] = palette[1][b];
|
|
d2[2] = palette[2][b];
|
|
}
|
|
break;
|
|
case 4:
|
|
for (int col = 0; col < load_width; col++, d2 += 3) {
|
|
uint8_t n; // 4-bit pixel value
|
|
if (!(col & 1)) {
|
|
b = file.read();
|
|
n = b >> 4;
|
|
} else {
|
|
n = b & 0xF;
|
|
}
|
|
d2[0] = palette[0][n];
|
|
d2[1] = palette[1][n];
|
|
d2[2] = palette[2][n];
|
|
}
|
|
break;
|
|
// A 2-bit BMP mode exists but is SUPER ESOTERIC (apparently
|
|
// a Windows CE thing), not supported here (nor in Photoshop).
|
|
case 2:
|
|
break;
|
|
case 1:
|
|
for (int col = 0; col < load_width; col++, d2 += 3) {
|
|
if (!(col & 7)) b = file.read();
|
|
uint8_t n = (b >> (7 - (col & 7))) & 1;
|
|
d2[0] = palette[0][n];
|
|
d2[1] = palette[1][n];
|
|
d2[2] = palette[2][n];
|
|
}
|
|
break;
|
|
} // end switch
|
|
|
|
} // end row
|
|
status = true; // YAY
|
|
} // end planes/compression check
|
|
} // end signature
|
|
file.close();
|
|
} // end file open
|
|
return status;
|
|
}
|