374 lines
12 KiB
C++

#include "Bitmap.h"
#include <cstdlib>
#include <cstring>
// ============================================================================
// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations
// ============================================================================
// Note: For cover images, dithering is done in JpegToBmpConverter.cpp
// This file handles BMP reading - use simple quantization to avoid double-dithering
constexpr bool USE_ATKINSON = true; // Use Atkinson dithering instead of Floyd-Steinberg
// ============================================================================
Bitmap::~Bitmap() {
delete[] errorCurRow;
delete[] errorNextRow;
delete atkinsonDitherer;
delete fsDitherer;
}
uint16_t Bitmap::readLE16(FsFile& f) {
const int c0 = f.read();
const int c1 = f.read();
const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0);
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
return static_cast<uint16_t>(b0) | (static_cast<uint16_t>(b1) << 8);
}
uint32_t Bitmap::readLE32(FsFile& f) {
const int c0 = f.read();
const int c1 = f.read();
const int c2 = f.read();
const int c3 = f.read();
const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0);
const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1);
const auto b2 = static_cast<uint8_t>(c2 < 0 ? 0 : c2);
const auto b3 = static_cast<uint8_t>(c3 < 0 ? 0 : c3);
return static_cast<uint32_t>(b0) | (static_cast<uint32_t>(b1) << 8) | (static_cast<uint32_t>(b2) << 16) |
(static_cast<uint32_t>(b3) << 24);
}
const char* Bitmap::errorToString(BmpReaderError err) {
switch (err) {
case BmpReaderError::Ok:
return "Ok";
case BmpReaderError::FileInvalid:
return "FileInvalid";
case BmpReaderError::SeekStartFailed:
return "SeekStartFailed";
case BmpReaderError::NotBMP:
return "NotBMP (missing 'BM')";
case BmpReaderError::DIBTooSmall:
return "DIBTooSmall (<40 bytes)";
case BmpReaderError::BadPlanes:
return "BadPlanes (!= 1)";
case BmpReaderError::UnsupportedBpp:
return "UnsupportedBpp (expected 1, 2, 8, 24, or 32)";
case BmpReaderError::UnsupportedCompression:
return "UnsupportedCompression (expected BI_RGB or BI_BITFIELDS for 32bpp)";
case BmpReaderError::BadDimensions:
return "BadDimensions";
case BmpReaderError::ImageTooLarge:
return "ImageTooLarge (max 2048x3072)";
case BmpReaderError::PaletteTooLarge:
return "PaletteTooLarge";
case BmpReaderError::SeekPixelDataFailed:
return "SeekPixelDataFailed";
case BmpReaderError::BufferTooSmall:
return "BufferTooSmall";
case BmpReaderError::OomRowBuffer:
return "OomRowBuffer";
case BmpReaderError::ShortReadRow:
return "ShortReadRow";
}
return "Unknown";
}
BmpReaderError Bitmap::parseHeaders() {
if (!file) return BmpReaderError::FileInvalid;
// Store file size for cache validation
fileSize = file.size();
if (!file.seek(0)) return BmpReaderError::SeekStartFailed;
// --- BMP FILE HEADER ---
const uint16_t bfType = readLE16(file);
if (bfType != 0x4D42) return BmpReaderError::NotBMP;
file.seekCur(8);
bfOffBits = readLE32(file);
// --- DIB HEADER ---
const uint32_t biSize = readLE32(file);
if (biSize < 40) return BmpReaderError::DIBTooSmall;
width = static_cast<int32_t>(readLE32(file));
const auto rawHeight = static_cast<int32_t>(readLE32(file));
topDown = rawHeight < 0;
height = topDown ? -rawHeight : rawHeight;
const uint16_t planes = readLE16(file);
bpp = readLE16(file);
const uint32_t comp = readLE32(file);
const bool validBpp = bpp == 1 || bpp == 2 || bpp == 8 || bpp == 24 || bpp == 32;
if (planes != 1) return BmpReaderError::BadPlanes;
if (!validBpp) return BmpReaderError::UnsupportedBpp;
// Allow BI_RGB (0) for all, and BI_BITFIELDS (3) for 32bpp which is common for BGRA masks.
if (!(comp == 0 || (bpp == 32 && comp == 3))) return BmpReaderError::UnsupportedCompression;
file.seekCur(12); // biSizeImage, biXPelsPerMeter, biYPelsPerMeter
const uint32_t colorsUsed = readLE32(file);
if (colorsUsed > 256u) return BmpReaderError::PaletteTooLarge;
file.seekCur(4); // biClrImportant
if (width <= 0 || height <= 0) return BmpReaderError::BadDimensions;
// Safety limits to prevent memory issues on ESP32
constexpr int MAX_IMAGE_WIDTH = 2048;
constexpr int MAX_IMAGE_HEIGHT = 3072;
if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT) {
return BmpReaderError::ImageTooLarge;
}
// Pre-calculate Row Bytes to avoid doing this every row
rowBytes = (width * bpp + 31) / 32 * 4;
for (int i = 0; i < 256; i++) paletteLum[i] = static_cast<uint8_t>(i);
if (colorsUsed > 0) {
for (uint32_t i = 0; i < colorsUsed; i++) {
uint8_t rgb[4];
file.read(rgb, 4); // Read B, G, R, Reserved in one go
paletteLum[i] = (77u * rgb[2] + 150u * rgb[1] + 29u * rgb[0]) >> 8;
}
}
if (!file.seek(bfOffBits)) {
return BmpReaderError::SeekPixelDataFailed;
}
// Create ditherer if enabled (only for 2-bit output)
// Use OUTPUT dimensions for dithering (after prescaling)
if (bpp > 2 && dithering) {
if (USE_ATKINSON) {
atkinsonDitherer = new AtkinsonDitherer(width);
} else {
fsDitherer = new FloydSteinbergDitherer(width);
}
}
return BmpReaderError::Ok;
}
// packed 2bpp output, 0 = black, 1 = dark gray, 2 = light gray, 3 = white
BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const {
// Note: rowBuffer should be pre-allocated by the caller to size 'rowBytes'
if (file.read(rowBuffer, rowBytes) != rowBytes) return BmpReaderError::ShortReadRow;
prevRowY += 1;
uint8_t* outPtr = data;
uint8_t currentOutByte = 0;
int bitShift = 6;
int currentX = 0;
// Helper lambda to pack 2bpp color into the output stream
auto packPixel = [&](const uint8_t lum) {
uint8_t color;
if (atkinsonDitherer) {
color = atkinsonDitherer->processPixel(adjustPixel(lum), currentX);
} else if (fsDitherer) {
color = fsDitherer->processPixel(adjustPixel(lum), currentX);
} else {
if (bpp > 2) {
// Simple quantization or noise dithering
color = quantize(adjustPixel(lum), currentX, prevRowY);
} else {
// do not quantize 2bpp image
color = static_cast<uint8_t>(lum >> 6);
}
}
currentOutByte |= (color << bitShift);
if (bitShift == 0) {
*outPtr++ = currentOutByte;
currentOutByte = 0;
bitShift = 6;
} else {
bitShift -= 2;
}
currentX++;
};
uint8_t lum;
switch (bpp) {
case 32: {
const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) {
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum);
p += 4;
}
break;
}
case 24: {
const uint8_t* p = rowBuffer;
for (int x = 0; x < width; x++) {
lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
packPixel(lum);
p += 3;
}
break;
}
case 8: {
for (int x = 0; x < width; x++) {
packPixel(paletteLum[rowBuffer[x]]);
}
break;
}
case 2: {
for (int x = 0; x < width; x++) {
lum = paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
packPixel(lum);
}
break;
}
case 1: {
for (int x = 0; x < width; x++) {
// Get palette index (0 or 1) from bit at position x
const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0;
// Use palette lookup for proper black/white mapping
lum = paletteLum[palIndex];
packPixel(lum);
}
break;
}
default:
return BmpReaderError::UnsupportedBpp;
}
if (atkinsonDitherer)
atkinsonDitherer->nextRow();
else if (fsDitherer)
fsDitherer->nextRow();
// Flush remaining bits if width is not a multiple of 4
if (bitShift != 6) *outPtr = currentOutByte;
return BmpReaderError::Ok;
}
BmpReaderError Bitmap::rewindToData() const {
if (!file.seek(bfOffBits)) {
return BmpReaderError::SeekPixelDataFailed;
}
// Reset dithering when rewinding
if (fsDitherer) fsDitherer->reset();
if (atkinsonDitherer) atkinsonDitherer->reset();
return BmpReaderError::Ok;
}
EdgeLuminance Bitmap::detectEdgeLuminance(int depth) const {
// Detect average luminance for each edge of the image.
// Samples 'depth' pixels from each edge for more stable averages.
// Returns per-edge luminance values (0-255).
EdgeLuminance result = {128, 128, 128, 128}; // Default to neutral gray
if (width <= 0 || height <= 0) return result;
if (depth < 1) depth = 1;
if (depth > width / 2) depth = width / 2;
if (depth > height / 2) depth = height / 2;
auto* rowBuffer = static_cast<uint8_t*>(malloc(rowBytes));
if (!rowBuffer) return result;
// Accumulators for each edge
uint32_t topSum = 0, bottomSum = 0, leftSum = 0, rightSum = 0;
int topCount = 0, bottomCount = 0, leftCount = 0, rightCount = 0;
// Helper lambda to get luminance from a pixel at position x in rowBuffer
auto getLuminance = [&](int x) -> uint8_t {
switch (bpp) {
case 32: {
const uint8_t* p = rowBuffer + x * 4;
return (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
}
case 24: {
const uint8_t* p = rowBuffer + x * 3;
return (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8;
}
case 8:
return paletteLum[rowBuffer[x]];
case 2:
return paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03];
case 1: {
const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0;
return paletteLum[palIndex];
}
default:
return 128; // Neutral if unsupported
}
};
// Helper to seek to a specific image row (accounting for top-down vs bottom-up)
auto seekToRow = [&](int imageRow) -> bool {
// In bottom-up BMP (topDown=false), row 0 in file is the bottom row of image
// In top-down BMP (topDown=true), row 0 in file is the top row of image
int fileRow = topDown ? imageRow : (height - 1 - imageRow);
return file.seek(bfOffBits + static_cast<uint32_t>(fileRow) * rowBytes);
};
// Sample top rows (image rows 0 to depth-1) - all pixels
for (int row = 0; row < depth && row < height; row++) {
if (seekToRow(row) && file.read(rowBuffer, rowBytes) == rowBytes) {
for (int x = 0; x < width; x++) {
topSum += getLuminance(x);
topCount++;
}
}
}
// Sample bottom rows (image rows height-depth to height-1) - all pixels
for (int row = height - depth; row < height; row++) {
if (row >= depth && row >= 0) { // Avoid overlap with top rows
if (seekToRow(row) && file.read(rowBuffer, rowBytes) == rowBytes) {
for (int x = 0; x < width; x++) {
bottomSum += getLuminance(x);
bottomCount++;
}
}
}
}
// Sample left and right edges from all rows
for (int y = 0; y < height; y++) {
if (seekToRow(y) && file.read(rowBuffer, rowBytes) == rowBytes) {
// Left edge (first 'depth' pixels)
for (int x = 0; x < depth && x < width; x++) {
leftSum += getLuminance(x);
leftCount++;
}
// Right edge (last 'depth' pixels)
for (int x = width - depth; x < width; x++) {
if (x >= depth) { // Avoid overlap with left edge
rightSum += getLuminance(x);
rightCount++;
}
}
}
}
free(rowBuffer);
// Calculate averages
if (topCount > 0) result.top = static_cast<uint8_t>(topSum / topCount);
if (bottomCount > 0) result.bottom = static_cast<uint8_t>(bottomSum / bottomCount);
if (leftCount > 0) result.left = static_cast<uint8_t>(leftSum / leftCount);
if (rightCount > 0) result.right = static_cast<uint8_t>(rightSum / rightCount);
// Rewind file position for subsequent drawing
rewindToData();
return result;
}