A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at records-reader 295 lines 9.8 kB view raw
1#include "Bitmap.h" 2 3#include <cstdlib> 4#include <cstring> 5 6// ============================================================================ 7// IMAGE PROCESSING OPTIONS 8// ============================================================================ 9// Dithering is applied when converting high-color BMPs to the display's native 10// 2-bit (4-level) grayscale. Images whose palette entries all map to native 11// gray levels (0, 85, 170, 255 ±21) are mapped directly without dithering. 12// For cover images, dithering is done in JpegToBmpConverter.cpp instead. 13constexpr bool USE_ATKINSON = true; // Use Atkinson dithering instead of Floyd-Steinberg 14// ============================================================================ 15 16Bitmap::~Bitmap() { 17 delete[] errorCurRow; 18 delete[] errorNextRow; 19 20 delete atkinsonDitherer; 21 delete fsDitherer; 22} 23 24uint16_t Bitmap::readLE16(FsFile& f) { 25 const int c0 = f.read(); 26 const int c1 = f.read(); 27 const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0); 28 const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1); 29 return static_cast<uint16_t>(b0) | (static_cast<uint16_t>(b1) << 8); 30} 31 32uint32_t Bitmap::readLE32(FsFile& f) { 33 const int c0 = f.read(); 34 const int c1 = f.read(); 35 const int c2 = f.read(); 36 const int c3 = f.read(); 37 38 const auto b0 = static_cast<uint8_t>(c0 < 0 ? 0 : c0); 39 const auto b1 = static_cast<uint8_t>(c1 < 0 ? 0 : c1); 40 const auto b2 = static_cast<uint8_t>(c2 < 0 ? 0 : c2); 41 const auto b3 = static_cast<uint8_t>(c3 < 0 ? 0 : c3); 42 43 return static_cast<uint32_t>(b0) | (static_cast<uint32_t>(b1) << 8) | (static_cast<uint32_t>(b2) << 16) | 44 (static_cast<uint32_t>(b3) << 24); 45} 46 47const char* Bitmap::errorToString(BmpReaderError err) { 48 switch (err) { 49 case BmpReaderError::Ok: 50 return "Ok"; 51 case BmpReaderError::FileInvalid: 52 return "FileInvalid"; 53 case BmpReaderError::SeekStartFailed: 54 return "SeekStartFailed"; 55 case BmpReaderError::NotBMP: 56 return "NotBMP (missing 'BM')"; 57 case BmpReaderError::DIBTooSmall: 58 return "DIBTooSmall (<40 bytes)"; 59 case BmpReaderError::BadPlanes: 60 return "BadPlanes (!= 1)"; 61 case BmpReaderError::UnsupportedBpp: 62 return "UnsupportedBpp (expected 1, 2, 4, 8, 24, or 32)"; 63 case BmpReaderError::UnsupportedCompression: 64 return "UnsupportedCompression (expected BI_RGB or BI_BITFIELDS for 32bpp)"; 65 case BmpReaderError::BadDimensions: 66 return "BadDimensions"; 67 case BmpReaderError::ImageTooLarge: 68 return "ImageTooLarge (max 2048x3072)"; 69 case BmpReaderError::PaletteTooLarge: 70 return "PaletteTooLarge"; 71 72 case BmpReaderError::SeekPixelDataFailed: 73 return "SeekPixelDataFailed"; 74 case BmpReaderError::BufferTooSmall: 75 return "BufferTooSmall"; 76 77 case BmpReaderError::OomRowBuffer: 78 return "OomRowBuffer"; 79 case BmpReaderError::ShortReadRow: 80 return "ShortReadRow"; 81 } 82 return "Unknown"; 83} 84 85BmpReaderError Bitmap::parseHeaders() { 86 if (!file) return BmpReaderError::FileInvalid; 87 if (!file.seek(0)) return BmpReaderError::SeekStartFailed; 88 89 // --- BMP FILE HEADER --- 90 const uint16_t bfType = readLE16(file); 91 if (bfType != 0x4D42) return BmpReaderError::NotBMP; 92 93 file.seekCur(8); 94 bfOffBits = readLE32(file); 95 96 // --- DIB HEADER --- 97 const uint32_t biSize = readLE32(file); 98 if (biSize < 40) return BmpReaderError::DIBTooSmall; 99 100 width = static_cast<int32_t>(readLE32(file)); 101 const auto rawHeight = static_cast<int32_t>(readLE32(file)); 102 topDown = rawHeight < 0; 103 height = topDown ? -rawHeight : rawHeight; 104 105 const uint16_t planes = readLE16(file); 106 bpp = readLE16(file); 107 const uint32_t comp = readLE32(file); 108 const bool validBpp = bpp == 1 || bpp == 2 || bpp == 4 || bpp == 8 || bpp == 24 || bpp == 32; 109 110 if (planes != 1) return BmpReaderError::BadPlanes; 111 if (!validBpp) return BmpReaderError::UnsupportedBpp; 112 // Allow BI_RGB (0) for all, and BI_BITFIELDS (3) for 32bpp which is common for BGRA masks. 113 if (!(comp == 0 || (bpp == 32 && comp == 3))) return BmpReaderError::UnsupportedCompression; 114 115 file.seekCur(12); // biSizeImage, biXPelsPerMeter, biYPelsPerMeter 116 colorsUsed = readLE32(file); 117 // BMP spec: colorsUsed==0 means default (2^bpp for paletted formats) 118 if (colorsUsed == 0 && bpp <= 8) colorsUsed = 1u << bpp; 119 if (colorsUsed > 256u) return BmpReaderError::PaletteTooLarge; 120 file.seekCur(4); // biClrImportant 121 122 if (width <= 0 || height <= 0) return BmpReaderError::BadDimensions; 123 124 // Safety limits to prevent memory issues on ESP32 125 constexpr int MAX_IMAGE_WIDTH = 2048; 126 constexpr int MAX_IMAGE_HEIGHT = 3072; 127 if (width > MAX_IMAGE_WIDTH || height > MAX_IMAGE_HEIGHT) { 128 return BmpReaderError::ImageTooLarge; 129 } 130 131 // Pre-calculate Row Bytes to avoid doing this every row 132 rowBytes = (width * bpp + 31) / 32 * 4; 133 134 for (int i = 0; i < 256; i++) paletteLum[i] = static_cast<uint8_t>(i); 135 if (colorsUsed > 0) { 136 for (uint32_t i = 0; i < colorsUsed; i++) { 137 uint8_t rgb[4]; 138 file.read(rgb, 4); // Read B, G, R, Reserved in one go 139 paletteLum[i] = (77u * rgb[2] + 150u * rgb[1] + 29u * rgb[0]) >> 8; 140 } 141 } 142 143 if (!file.seek(bfOffBits)) { 144 return BmpReaderError::SeekPixelDataFailed; 145 } 146 147 // Check if palette luminances map cleanly to the display's 4 native gray levels. 148 // Native levels are 0, 85, 170, 255 — i.e. values where (lum >> 6) is lossless. 149 // If all palette entries are near a native level, we can skip dithering entirely. 150 nativePalette = bpp <= 2; // 1-bit and 2-bit are always native 151 if (!nativePalette && colorsUsed > 0) { 152 nativePalette = true; 153 for (uint32_t i = 0; i < colorsUsed; i++) { 154 const uint8_t lum = paletteLum[i]; 155 const uint8_t level = lum >> 6; // quantize to 0-3 156 const uint8_t reconstructed = level * 85; // back to 0, 85, 170, 255 157 if (lum > reconstructed + 21 || lum + 21 < reconstructed) { 158 nativePalette = false; // luminance is too far from any native level 159 break; 160 } 161 } 162 } 163 164 // Decide pixel processing strategy: 165 // - Native palette → direct mapping, no processing needed 166 // - High-color + dithering enabled → error-diffusion dithering (Atkinson or Floyd-Steinberg) 167 // - High-color + dithering disabled → simple quantization (no error diffusion) 168 const bool highColor = !nativePalette; 169 if (highColor && dithering) { 170 if (USE_ATKINSON) { 171 atkinsonDitherer = new AtkinsonDitherer(width); 172 } else { 173 fsDitherer = new FloydSteinbergDitherer(width); 174 } 175 } 176 177 return BmpReaderError::Ok; 178} 179 180// packed 2bpp output, 0 = black, 1 = dark gray, 2 = light gray, 3 = white 181BmpReaderError Bitmap::readNextRow(uint8_t* data, uint8_t* rowBuffer) const { 182 // Note: rowBuffer should be pre-allocated by the caller to size 'rowBytes' 183 if (file.read(rowBuffer, rowBytes) != rowBytes) return BmpReaderError::ShortReadRow; 184 185 prevRowY += 1; 186 187 uint8_t* outPtr = data; 188 uint8_t currentOutByte = 0; 189 int bitShift = 6; 190 int currentX = 0; 191 192 // Helper lambda to pack 2bpp color into the output stream 193 auto packPixel = [&](const uint8_t lum) { 194 uint8_t color; 195 if (atkinsonDitherer) { 196 color = atkinsonDitherer->processPixel(adjustPixel(lum), currentX); 197 } else if (fsDitherer) { 198 color = fsDitherer->processPixel(adjustPixel(lum), currentX); 199 } else { 200 if (nativePalette) { 201 // Palette matches native gray levels: direct mapping (still apply brightness/contrast/gamma) 202 color = static_cast<uint8_t>(adjustPixel(lum) >> 6); 203 } else { 204 // Non-native palette with dithering disabled: simple quantization 205 color = quantize(adjustPixel(lum), currentX, prevRowY); 206 } 207 } 208 currentOutByte |= (color << bitShift); 209 if (bitShift == 0) { 210 *outPtr++ = currentOutByte; 211 currentOutByte = 0; 212 bitShift = 6; 213 } else { 214 bitShift -= 2; 215 } 216 currentX++; 217 }; 218 219 uint8_t lum; 220 221 switch (bpp) { 222 case 32: { 223 const uint8_t* p = rowBuffer; 224 for (int x = 0; x < width; x++) { 225 lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8; 226 packPixel(lum); 227 p += 4; 228 } 229 break; 230 } 231 case 24: { 232 const uint8_t* p = rowBuffer; 233 for (int x = 0; x < width; x++) { 234 lum = (77u * p[2] + 150u * p[1] + 29u * p[0]) >> 8; 235 packPixel(lum); 236 p += 3; 237 } 238 break; 239 } 240 case 8: { 241 for (int x = 0; x < width; x++) { 242 packPixel(paletteLum[rowBuffer[x]]); 243 } 244 break; 245 } 246 case 4: { 247 for (int x = 0; x < width; x++) { 248 const uint8_t nibble = (x & 1) ? (rowBuffer[x >> 1] & 0x0F) : (rowBuffer[x >> 1] >> 4); 249 packPixel(paletteLum[nibble]); 250 } 251 break; 252 } 253 case 2: { 254 for (int x = 0; x < width; x++) { 255 lum = paletteLum[(rowBuffer[x >> 2] >> (6 - ((x & 3) * 2))) & 0x03]; 256 packPixel(lum); 257 } 258 break; 259 } 260 case 1: { 261 for (int x = 0; x < width; x++) { 262 // Get palette index (0 or 1) from bit at position x 263 const uint8_t palIndex = (rowBuffer[x >> 3] & (0x80 >> (x & 7))) ? 1 : 0; 264 // Use palette lookup for proper black/white mapping 265 lum = paletteLum[palIndex]; 266 packPixel(lum); 267 } 268 break; 269 } 270 default: 271 return BmpReaderError::UnsupportedBpp; 272 } 273 274 if (atkinsonDitherer) 275 atkinsonDitherer->nextRow(); 276 else if (fsDitherer) 277 fsDitherer->nextRow(); 278 279 // Flush remaining bits if width is not a multiple of 4 280 if (bitShift != 6) *outPtr = currentOutByte; 281 282 return BmpReaderError::Ok; 283} 284 285BmpReaderError Bitmap::rewindToData() const { 286 if (!file.seek(bfOffBits)) { 287 return BmpReaderError::SeekPixelDataFailed; 288 } 289 290 // Reset dithering when rewinding 291 if (fsDitherer) fsDitherer->reset(); 292 if (atkinsonDitherer) atkinsonDitherer->reset(); 293 294 return BmpReaderError::Ok; 295}