PngToBmpConverter.cpp
1 #include "PngToBmpConverter.h" 2 3 #include <HardwareSerial.h> 4 #include <SdFat.h> 5 #include <pngle.h> 6 7 #include <cstring> 8 9 #include "BitmapHelpers.h" 10 11 namespace { 12 constexpr int MAX_IMAGE_WIDTH = 2048; 13 constexpr int MAX_IMAGE_HEIGHT = 3072; 14 15 inline void write16(Print& out, const uint16_t value) { 16 out.write(value & 0xFF); 17 out.write((value >> 8) & 0xFF); 18 } 19 20 inline void write32(Print& out, const uint32_t value) { 21 out.write(value & 0xFF); 22 out.write((value >> 8) & 0xFF); 23 out.write((value >> 16) & 0xFF); 24 out.write((value >> 24) & 0xFF); 25 } 26 27 inline void write32Signed(Print& out, const int32_t value) { 28 out.write(value & 0xFF); 29 out.write((value >> 8) & 0xFF); 30 out.write((value >> 16) & 0xFF); 31 out.write((value >> 24) & 0xFF); 32 } 33 34 void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) { 35 const int bytesPerRow = (width * 2 + 31) / 32 * 4; 36 const int imageSize = bytesPerRow * height; 37 const uint32_t fileSize = 70 + imageSize; 38 39 bmpOut.write('B'); 40 bmpOut.write('M'); 41 write32(bmpOut, fileSize); 42 write32(bmpOut, 0); 43 write32(bmpOut, 70); 44 45 write32(bmpOut, 40); 46 write32Signed(bmpOut, width); 47 write32Signed(bmpOut, -height); 48 write16(bmpOut, 1); 49 write16(bmpOut, 2); 50 write32(bmpOut, 0); 51 write32(bmpOut, imageSize); 52 write32(bmpOut, 2835); 53 write32(bmpOut, 2835); 54 write32(bmpOut, 4); 55 write32(bmpOut, 4); 56 57 uint8_t palette[16] = { 58 0x00, 0x00, 0x00, 0x00, // Black 59 0x55, 0x55, 0x55, 0x00, // Dark gray 60 0xAA, 0xAA, 0xAA, 0x00, // Light gray 61 0xFF, 0xFF, 0xFF, 0x00 // White 62 }; 63 for (const uint8_t i : palette) { 64 bmpOut.write(i); 65 } 66 } 67 68 struct PngContext { 69 FsFile* pngFile; 70 Print* bmpOut; 71 int srcWidth; 72 int srcHeight; 73 int outWidth; 74 int outHeight; 75 int targetMaxWidth; 76 int targetMaxHeight; 77 uint32_t scaleX_fp; 78 uint32_t scaleY_fp; 79 bool needsScaling; 80 bool headerWritten; 81 bool quickMode; // Fast preview: simple threshold instead of dithering 82 bool initFailed; // Set when allocation fails in pngInitCallback 83 int currentSrcY; 84 int currentOutY; 85 uint32_t nextOutY_srcStart; 86 87 // Row buffers 88 uint8_t* srcRowBuffer; // Source row grayscale 89 uint8_t* outRowBuffer; // Output BMP row 90 uint32_t* rowAccum; // Accumulator for scaling 91 uint16_t* rowCount; // Count for scaling 92 AtkinsonDitherer* ditherer; 93 94 int bytesPerRow; 95 }; 96 97 void pngDrawCallback(pngle_t* pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) { 98 auto* ctx = static_cast<PngContext*>(pngle_get_user_data(pngle)); 99 if (!ctx || ctx->initFailed || !ctx->srcRowBuffer) return; 100 101 // Convert to grayscale using LUT 102 const uint8_t gray = rgbToGray(rgba[0], rgba[1], rgba[2]); 103 104 // Handle alpha: blend with white background 105 const uint8_t alpha = rgba[3]; 106 const uint8_t blendedGray = (gray * alpha + 255 * (255 - alpha)) / 255; 107 108 // Store in source row buffer 109 if (x < static_cast<uint32_t>(ctx->srcWidth)) { 110 ctx->srcRowBuffer[x] = blendedGray; 111 } 112 113 // Check if row complete (x is last pixel of row) 114 if (x == static_cast<uint32_t>(ctx->srcWidth - 1)) { 115 // Process complete row 116 if (!ctx->needsScaling) { 117 // Direct output 118 memset(ctx->outRowBuffer, 0, ctx->bytesPerRow); 119 for (int outX = 0; outX < ctx->outWidth; outX++) { 120 const uint8_t gray = adjustPixel(ctx->srcRowBuffer[outX]); 121 uint8_t twoBit; 122 if (ctx->quickMode) { 123 // Simple threshold quantization (faster) 124 twoBit = quantizeSimple(gray); 125 } else { 126 twoBit = ctx->ditherer ? ctx->ditherer->processPixel(gray, outX) : quantize(gray, outX, y); 127 } 128 const int byteIndex = (outX * 2) / 8; 129 const int bitOffset = 6 - ((outX * 2) % 8); 130 ctx->outRowBuffer[byteIndex] |= (twoBit << bitOffset); 131 } 132 if (ctx->ditherer && !ctx->quickMode) ctx->ditherer->nextRow(); 133 ctx->bmpOut->write(ctx->outRowBuffer, ctx->bytesPerRow); 134 } else { 135 // Scaling: accumulate source pixels 136 for (int outX = 0; outX < ctx->outWidth; outX++) { 137 const int srcXStart = (static_cast<uint32_t>(outX) * ctx->scaleX_fp) >> 16; 138 const int srcXEnd = (static_cast<uint32_t>(outX + 1) * ctx->scaleX_fp) >> 16; 139 140 int sum = 0; 141 int count = 0; 142 for (int srcX = srcXStart; srcX < srcXEnd && srcX < ctx->srcWidth; srcX++) { 143 sum += ctx->srcRowBuffer[srcX]; 144 count++; 145 } 146 if (count == 0 && srcXStart < ctx->srcWidth) { 147 sum = ctx->srcRowBuffer[srcXStart]; 148 count = 1; 149 } 150 ctx->rowAccum[outX] += sum; 151 ctx->rowCount[outX] += count; 152 } 153 154 ctx->currentSrcY++; 155 const uint32_t srcY_fp = static_cast<uint32_t>(ctx->currentSrcY) << 16; 156 157 if (srcY_fp >= ctx->nextOutY_srcStart && ctx->currentOutY < ctx->outHeight) { 158 memset(ctx->outRowBuffer, 0, ctx->bytesPerRow); 159 for (int outX = 0; outX < ctx->outWidth; outX++) { 160 const uint8_t gray = adjustPixel((ctx->rowCount[outX] > 0) ? (ctx->rowAccum[outX] / ctx->rowCount[outX]) : 0); 161 uint8_t twoBit; 162 if (ctx->quickMode) { 163 // Simple threshold quantization (faster) 164 twoBit = quantizeSimple(gray); 165 } else { 166 twoBit = ctx->ditherer ? ctx->ditherer->processPixel(gray, outX) : quantize(gray, outX, ctx->currentOutY); 167 } 168 const int byteIndex = (outX * 2) / 8; 169 const int bitOffset = 6 - ((outX * 2) % 8); 170 ctx->outRowBuffer[byteIndex] |= (twoBit << bitOffset); 171 } 172 if (ctx->ditherer && !ctx->quickMode) ctx->ditherer->nextRow(); 173 ctx->bmpOut->write(ctx->outRowBuffer, ctx->bytesPerRow); 174 ctx->currentOutY++; 175 176 memset(ctx->rowAccum, 0, ctx->outWidth * sizeof(uint32_t)); 177 memset(ctx->rowCount, 0, ctx->outWidth * sizeof(uint16_t)); 178 ctx->nextOutY_srcStart = static_cast<uint32_t>(ctx->currentOutY + 1) * ctx->scaleY_fp; 179 } 180 } 181 } 182 } 183 184 void pngInitCallback(pngle_t* pngle, uint32_t w, uint32_t h) { 185 auto* ctx = static_cast<PngContext*>(pngle_get_user_data(pngle)); 186 if (!ctx) return; 187 188 ctx->srcWidth = w; 189 ctx->srcHeight = h; 190 191 Serial.printf("[%lu] [PNG] Image dimensions: %dx%d\n", millis(), w, h); 192 193 if (w > MAX_IMAGE_WIDTH || h > MAX_IMAGE_HEIGHT) { 194 Serial.printf("[%lu] [PNG] Image too large\n", millis()); 195 return; 196 } 197 198 // Calculate output dimensions 199 ctx->outWidth = w; 200 ctx->outHeight = h; 201 ctx->scaleX_fp = 65536; 202 ctx->scaleY_fp = 65536; 203 ctx->needsScaling = false; 204 205 if (ctx->targetMaxWidth > 0 && ctx->targetMaxHeight > 0 && 206 (static_cast<int>(w) > ctx->targetMaxWidth || static_cast<int>(h) > ctx->targetMaxHeight)) { 207 const float scaleToFitWidth = static_cast<float>(ctx->targetMaxWidth) / w; 208 const float scaleToFitHeight = static_cast<float>(ctx->targetMaxHeight) / h; 209 const float scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; 210 211 ctx->outWidth = static_cast<int>(w * scale); 212 ctx->outHeight = static_cast<int>(h * scale); 213 if (ctx->outWidth < 1) ctx->outWidth = 1; 214 if (ctx->outHeight < 1) ctx->outHeight = 1; 215 216 ctx->scaleX_fp = (static_cast<uint32_t>(w) << 16) / ctx->outWidth; 217 ctx->scaleY_fp = (static_cast<uint32_t>(h) << 16) / ctx->outHeight; 218 ctx->needsScaling = true; 219 220 Serial.printf("[%lu] [PNG] Scaling %dx%d -> %dx%d\n", millis(), w, h, ctx->outWidth, ctx->outHeight); 221 } 222 223 // Allocate buffers 224 ctx->srcRowBuffer = static_cast<uint8_t*>(malloc(w)); 225 ctx->bytesPerRow = (ctx->outWidth * 2 + 31) / 32 * 4; 226 ctx->outRowBuffer = static_cast<uint8_t*>(malloc(ctx->bytesPerRow)); 227 228 if (!ctx->srcRowBuffer || !ctx->outRowBuffer) { 229 Serial.printf("[%lu] [PNG] Failed to allocate row buffers\n", millis()); 230 free(ctx->srcRowBuffer); // safe if nullptr 231 free(ctx->outRowBuffer); // safe if nullptr 232 ctx->srcRowBuffer = nullptr; 233 ctx->outRowBuffer = nullptr; 234 ctx->initFailed = true; 235 return; 236 } 237 238 if (ctx->needsScaling) { 239 ctx->rowAccum = new (std::nothrow) uint32_t[ctx->outWidth](); 240 ctx->rowCount = new (std::nothrow) uint16_t[ctx->outWidth](); 241 if (!ctx->rowAccum || !ctx->rowCount) { 242 Serial.printf("[%lu] [PNG] Failed to allocate scaling buffers\n", millis()); 243 free(ctx->srcRowBuffer); 244 free(ctx->outRowBuffer); 245 delete[] ctx->rowAccum; // safe if nullptr 246 delete[] ctx->rowCount; // safe if nullptr 247 ctx->srcRowBuffer = nullptr; 248 ctx->outRowBuffer = nullptr; 249 ctx->rowAccum = nullptr; 250 ctx->rowCount = nullptr; 251 ctx->initFailed = true; 252 return; 253 } 254 ctx->nextOutY_srcStart = ctx->scaleY_fp; 255 } 256 257 // Skip ditherer allocation in quickMode for faster preview 258 if (!ctx->quickMode) { 259 ctx->ditherer = new (std::nothrow) AtkinsonDitherer(ctx->outWidth); 260 if (!ctx->ditherer) { 261 Serial.printf("[%lu] [PNG] Failed to allocate ditherer\n", millis()); 262 free(ctx->srcRowBuffer); 263 free(ctx->outRowBuffer); 264 delete[] ctx->rowAccum; 265 delete[] ctx->rowCount; 266 ctx->srcRowBuffer = nullptr; 267 ctx->outRowBuffer = nullptr; 268 ctx->rowAccum = nullptr; 269 ctx->rowCount = nullptr; 270 ctx->initFailed = true; 271 return; 272 } 273 } 274 ctx->currentSrcY = 0; 275 ctx->currentOutY = 0; 276 277 // Write BMP header 278 writeBmpHeader2bit(*ctx->bmpOut, ctx->outWidth, ctx->outHeight); 279 ctx->headerWritten = true; 280 } 281 282 bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight, 283 bool quickMode) { 284 Serial.printf("[%lu] [PNG] Converting PNG to BMP (target: %dx%d)%s\n", millis(), targetMaxWidth, targetMaxHeight, 285 quickMode ? " [QUICK]" : ""); 286 287 pngle_t* pngle = pngle_new(); 288 if (!pngle) { 289 Serial.printf("[%lu] [PNG] Failed to create pngle instance\n", millis()); 290 return false; 291 } 292 293 PngContext ctx = {}; 294 ctx.pngFile = &pngFile; 295 ctx.bmpOut = &bmpOut; 296 ctx.targetMaxWidth = targetMaxWidth; 297 ctx.targetMaxHeight = targetMaxHeight; 298 ctx.headerWritten = false; 299 ctx.quickMode = quickMode; 300 301 pngle_set_user_data(pngle, &ctx); 302 pngle_set_init_callback(pngle, pngInitCallback); 303 pngle_set_draw_callback(pngle, pngDrawCallback); 304 305 // Read and feed PNG data 306 uint8_t buffer[1024]; 307 int bytesRead; 308 bool success = true; 309 310 while ((bytesRead = pngFile.read(buffer, sizeof(buffer))) > 0) { 311 int fed = pngle_feed(pngle, buffer, bytesRead); 312 if (fed < 0) { 313 Serial.printf("[%lu] [PNG] pngle_feed error: %s\n", millis(), pngle_error(pngle)); 314 success = false; 315 break; 316 } 317 } 318 319 // Cleanup 320 if (ctx.srcRowBuffer) free(ctx.srcRowBuffer); 321 if (ctx.outRowBuffer) free(ctx.outRowBuffer); 322 if (ctx.rowAccum) delete[] ctx.rowAccum; 323 if (ctx.rowCount) delete[] ctx.rowCount; 324 if (ctx.ditherer) delete ctx.ditherer; 325 326 pngle_destroy(pngle); 327 328 if (success && ctx.headerWritten) { 329 Serial.printf("[%lu] [PNG] Successfully converted PNG to BMP (%dx%d)\n", millis(), ctx.outWidth, ctx.outHeight); 330 return true; 331 } 332 333 return false; 334 } 335 336 } // namespace 337 338 bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, 339 int targetMaxHeight) { 340 return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, false); 341 } 342 343 bool PngToBmpConverter::pngFileToBmpStreamQuick(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, 344 int targetMaxHeight) { 345 return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true); 346 }