JpegToBmpConverter.cpp
1 #include "JpegToBmpConverter.h" 2 3 #include <HardwareSerial.h> 4 #include <SdFat.h> 5 #include <picojpeg.h> 6 7 #include <cstdio> 8 #include <cstring> 9 10 #include "BitmapHelpers.h" 11 12 // Context structure for picojpeg callback 13 struct JpegReadContext { 14 FsFile& file; 15 uint8_t buffer[512]; 16 size_t bufferPos; 17 size_t bufferFilled; 18 }; 19 20 // ============================================================================ 21 // IMAGE PROCESSING OPTIONS - Toggle these to test different configurations 22 // ============================================================================ 23 constexpr bool USE_8BIT_OUTPUT = false; // true: 8-bit grayscale (no quantization), false: 2-bit (4 levels) 24 // Dithering method selection (only one should be true, or all false for simple quantization): 25 constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion) 26 constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts) 27 constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering (good for downsampling) 28 // Pre-resize to target display size (CRITICAL: avoids dithering artifacts from post-downsampling) 29 constexpr bool USE_PRESCALE = true; // true: scale image to target size before dithering 30 constexpr int TARGET_MAX_WIDTH = 480; // Max width for cover images (portrait display width) 31 constexpr int TARGET_MAX_HEIGHT = 800; // Max height for cover images (portrait display height) 32 // ============================================================================ 33 34 inline void write16(Print& out, const uint16_t value) { 35 out.write(value & 0xFF); 36 out.write((value >> 8) & 0xFF); 37 } 38 39 inline void write32(Print& out, const uint32_t value) { 40 out.write(value & 0xFF); 41 out.write((value >> 8) & 0xFF); 42 out.write((value >> 16) & 0xFF); 43 out.write((value >> 24) & 0xFF); 44 } 45 46 inline void write32Signed(Print& out, const int32_t value) { 47 out.write(value & 0xFF); 48 out.write((value >> 8) & 0xFF); 49 out.write((value >> 16) & 0xFF); 50 out.write((value >> 24) & 0xFF); 51 } 52 53 // Helper function: Write BMP header with 8-bit grayscale (256 levels) 54 void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) { 55 // Calculate row padding (each row must be multiple of 4 bytes) 56 const int bytesPerRow = (width + 3) / 4 * 4; // 8 bits per pixel, padded 57 const int imageSize = bytesPerRow * height; 58 const uint32_t paletteSize = 256 * 4; // 256 colors * 4 bytes (BGRA) 59 const uint32_t fileSize = 14 + 40 + paletteSize + imageSize; 60 61 // BMP File Header (14 bytes) 62 bmpOut.write('B'); 63 bmpOut.write('M'); 64 write32(bmpOut, fileSize); 65 write32(bmpOut, 0); // Reserved 66 write32(bmpOut, 14 + 40 + paletteSize); // Offset to pixel data 67 68 // DIB Header (BITMAPINFOHEADER - 40 bytes) 69 write32(bmpOut, 40); 70 write32Signed(bmpOut, width); 71 write32Signed(bmpOut, -height); // Negative height = top-down bitmap 72 write16(bmpOut, 1); // Color planes 73 write16(bmpOut, 8); // Bits per pixel (8 bits) 74 write32(bmpOut, 0); // BI_RGB (no compression) 75 write32(bmpOut, imageSize); 76 write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI) 77 write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI) 78 write32(bmpOut, 256); // colorsUsed 79 write32(bmpOut, 256); // colorsImportant 80 81 // Color Palette (256 grayscale entries x 4 bytes = 1024 bytes) 82 for (int i = 0; i < 256; i++) { 83 bmpOut.write(static_cast<uint8_t>(i)); // Blue 84 bmpOut.write(static_cast<uint8_t>(i)); // Green 85 bmpOut.write(static_cast<uint8_t>(i)); // Red 86 bmpOut.write(static_cast<uint8_t>(0)); // Reserved 87 } 88 } 89 90 // Helper function: Write BMP header with 1-bit color depth (black and white) 91 static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) { 92 // Calculate row padding (each row must be multiple of 4 bytes) 93 const int bytesPerRow = (width + 31) / 32 * 4; // 1 bit per pixel, round up to 4-byte boundary 94 const int imageSize = bytesPerRow * height; 95 const uint32_t fileSize = 62 + imageSize; // 14 (file header) + 40 (DIB header) + 8 (palette) + image 96 97 // BMP File Header (14 bytes) 98 bmpOut.write('B'); 99 bmpOut.write('M'); 100 write32(bmpOut, fileSize); // File size 101 write32(bmpOut, 0); // Reserved 102 write32(bmpOut, 62); // Offset to pixel data (14 + 40 + 8) 103 104 // DIB Header (BITMAPINFOHEADER - 40 bytes) 105 write32(bmpOut, 40); 106 write32Signed(bmpOut, width); 107 write32Signed(bmpOut, -height); // Negative height = top-down bitmap 108 write16(bmpOut, 1); // Color planes 109 write16(bmpOut, 1); // Bits per pixel (1 bit) 110 write32(bmpOut, 0); // BI_RGB (no compression) 111 write32(bmpOut, imageSize); 112 write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI) 113 write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI) 114 write32(bmpOut, 2); // colorsUsed 115 write32(bmpOut, 2); // colorsImportant 116 117 // Color Palette (2 colors x 4 bytes = 8 bytes) 118 // Format: Blue, Green, Red, Reserved (BGRA) 119 // Note: In 1-bit BMP, palette index 0 = black, 1 = white 120 uint8_t palette[8] = { 121 0x00, 0x00, 0x00, 0x00, // Color 0: Black 122 0xFF, 0xFF, 0xFF, 0x00 // Color 1: White 123 }; 124 for (const uint8_t i : palette) { 125 bmpOut.write(i); 126 } 127 } 128 129 // Helper function: Write BMP header with 2-bit color depth 130 static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) { 131 // Calculate row padding (each row must be multiple of 4 bytes) 132 const int bytesPerRow = (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up 133 const int imageSize = bytesPerRow * height; 134 const uint32_t fileSize = 70 + imageSize; // 14 (file header) + 40 (DIB header) + 16 (palette) + image 135 136 // BMP File Header (14 bytes) 137 bmpOut.write('B'); 138 bmpOut.write('M'); 139 write32(bmpOut, fileSize); // File size 140 write32(bmpOut, 0); // Reserved 141 write32(bmpOut, 70); // Offset to pixel data 142 143 // DIB Header (BITMAPINFOHEADER - 40 bytes) 144 write32(bmpOut, 40); 145 write32Signed(bmpOut, width); 146 write32Signed(bmpOut, -height); // Negative height = top-down bitmap 147 write16(bmpOut, 1); // Color planes 148 write16(bmpOut, 2); // Bits per pixel (2 bits) 149 write32(bmpOut, 0); // BI_RGB (no compression) 150 write32(bmpOut, imageSize); 151 write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI) 152 write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI) 153 write32(bmpOut, 4); // colorsUsed 154 write32(bmpOut, 4); // colorsImportant 155 156 // Color Palette (4 colors x 4 bytes = 16 bytes) 157 // Format: Blue, Green, Red, Reserved (BGRA) 158 uint8_t palette[16] = { 159 0x00, 0x00, 0x00, 0x00, // Color 0: Black 160 0x55, 0x55, 0x55, 0x00, // Color 1: Dark gray (85) 161 0xAA, 0xAA, 0xAA, 0x00, // Color 2: Light gray (170) 162 0xFF, 0xFF, 0xFF, 0x00 // Color 3: White 163 }; 164 for (const uint8_t i : palette) { 165 bmpOut.write(i); 166 } 167 } 168 169 // JPEG SOF markers - detect unsupported encoding types 170 // SOF0 (0xC0) = Baseline DCT (supported) 171 // SOF1 (0xC1) = Extended sequential DCT (supported) 172 // SOF2 (0xC2) = Progressive DCT (NOT supported) 173 // SOF9 (0xC9) = Extended sequential DCT, arithmetic (NOT supported) 174 // SOF10 (0xCA) = Progressive DCT, arithmetic (NOT supported) 175 static bool isUnsupportedJpeg(FsFile& file) { 176 const uint64_t originalPos = file.position(); 177 file.seek(0); 178 179 uint8_t buf[2]; 180 bool isProgressive = false; 181 182 // Scan for SOF marker 183 while (file.read(buf, 1) == 1) { 184 if (buf[0] != 0xFF) continue; 185 186 if (file.read(buf, 1) != 1) break; 187 188 // Skip padding FFs 189 while (buf[0] == 0xFF) { 190 if (file.read(buf, 1) != 1) break; 191 } 192 193 const uint8_t marker = buf[0]; 194 195 // Check for unsupported SOF markers (progressive or arithmetic coding) 196 if (marker == 0xC2 || marker == 0xC9 || marker == 0xCA) { 197 isProgressive = true; 198 break; 199 } 200 201 // Baseline/extended sequential DCT - supported 202 if (marker == 0xC0 || marker == 0xC1) { 203 isProgressive = false; 204 break; 205 } 206 207 // Skip variable-length segments (not SOF, SOS, or standalone markers) 208 if (marker != 0xD8 && marker != 0xD9 && marker != 0x01 && !(marker >= 0xD0 && marker <= 0xD7)) { 209 if (file.read(buf, 2) != 2) break; 210 const uint16_t len = (buf[0] << 8) | buf[1]; 211 if (len < 2) break; 212 file.seek(file.position() + len - 2); 213 } 214 } 215 216 file.seek(originalPos); 217 return isProgressive; 218 } 219 220 // Callback function for picojpeg to read JPEG data 221 unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const unsigned char buf_size, 222 unsigned char* pBytes_actually_read, void* pCallback_data) { 223 auto* context = static_cast<JpegReadContext*>(pCallback_data); 224 225 if (!context || !context->file) { 226 return PJPG_STREAM_READ_ERROR; 227 } 228 229 // Check if we need to refill our context buffer 230 if (context->bufferPos >= context->bufferFilled) { 231 context->bufferFilled = context->file.read(context->buffer, sizeof(context->buffer)); 232 context->bufferPos = 0; 233 234 if (context->bufferFilled == 0) { 235 // EOF or error 236 *pBytes_actually_read = 0; 237 return 0; // Success (EOF is normal) 238 } 239 } 240 241 // Copy available bytes to picojpeg's buffer 242 const size_t available = context->bufferFilled - context->bufferPos; 243 const size_t toRead = available < buf_size ? available : buf_size; 244 245 memcpy(pBuf, context->buffer + context->bufferPos, toRead); 246 context->bufferPos += toRead; 247 *pBytes_actually_read = static_cast<unsigned char>(toRead); 248 249 return 0; // Success 250 } 251 252 // Internal implementation with configurable target size and bit depth 253 bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, 254 bool oneBit, bool quickMode) { 255 Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)%s\n", millis(), oneBit ? "1-bit" : "2-bit", 256 targetWidth, targetHeight, quickMode ? " [QUICK]" : ""); 257 258 // Check for unsupported JPEG encoding (progressive or arithmetic) before attempting decode 259 if (isUnsupportedJpeg(jpegFile)) { 260 Serial.printf("[%lu] [JPG] Unsupported JPEG encoding (progressive or arithmetic), skipping\n", millis()); 261 return false; 262 } 263 264 // Setup context for picojpeg callback 265 JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0}; 266 267 // Initialize picojpeg decoder 268 pjpeg_image_info_t imageInfo; 269 const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0); 270 if (status != 0) { 271 Serial.printf("[%lu] [JPG] JPEG decode init failed with error code: %d\n", millis(), status); 272 return false; 273 } 274 275 Serial.printf("[%lu] [JPG] JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d\n", millis(), imageInfo.m_width, 276 imageInfo.m_height, imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol); 277 278 // Safety limits to prevent memory issues on ESP32 279 constexpr int MAX_IMAGE_WIDTH = 2048; 280 constexpr int MAX_IMAGE_HEIGHT = 3072; 281 constexpr int MAX_MCU_ROW_BYTES = 65536; 282 283 if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) { 284 Serial.printf("[%lu] [JPG] Image too large (%dx%d), max supported: %dx%d\n", millis(), imageInfo.m_width, 285 imageInfo.m_height, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); 286 return false; 287 } 288 289 // Calculate output dimensions (pre-scale to fit display exactly) 290 int outWidth = imageInfo.m_width; 291 int outHeight = imageInfo.m_height; 292 // Use fixed-point scaling (16.16) for sub-pixel accuracy 293 uint32_t scaleX_fp = 65536; // 1.0 in 16.16 fixed point 294 uint32_t scaleY_fp = 65536; 295 bool needsScaling = false; 296 297 if (targetWidth > 0 && targetHeight > 0 && (imageInfo.m_width > targetWidth || imageInfo.m_height > targetHeight)) { 298 // Calculate scale to fit within target dimensions while maintaining aspect ratio 299 const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width; 300 const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height; 301 // We scale to the smaller dimension, so we can potentially crop later. 302 const float scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; 303 304 outWidth = static_cast<int>(imageInfo.m_width * scale); 305 outHeight = static_cast<int>(imageInfo.m_height * scale); 306 307 // Ensure at least 1 pixel 308 if (outWidth < 1) outWidth = 1; 309 if (outHeight < 1) outHeight = 1; 310 311 // Calculate fixed-point scale factors (source pixels per output pixel) 312 // scaleX_fp = (srcWidth << 16) / outWidth 313 scaleX_fp = (static_cast<uint32_t>(imageInfo.m_width) << 16) / outWidth; 314 scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight; 315 needsScaling = true; 316 317 Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width, 318 imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight); 319 } 320 321 // Write BMP header with output dimensions 322 int bytesPerRow; 323 if (USE_8BIT_OUTPUT && !oneBit) { 324 writeBmpHeader8bit(bmpOut, outWidth, outHeight); 325 bytesPerRow = (outWidth + 3) / 4 * 4; 326 } else if (oneBit) { 327 writeBmpHeader1bit(bmpOut, outWidth, outHeight); 328 bytesPerRow = (outWidth + 31) / 32 * 4; // 1 bit per pixel 329 } else { 330 writeBmpHeader2bit(bmpOut, outWidth, outHeight); 331 bytesPerRow = (outWidth * 2 + 31) / 32 * 4; 332 } 333 334 // Allocate row buffer 335 auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow)); 336 if (!rowBuffer) { 337 Serial.printf("[%lu] [JPG] Failed to allocate row buffer\n", millis()); 338 return false; 339 } 340 341 // Allocate a buffer for one MCU row worth of grayscale pixels 342 // This is the minimal memory needed for streaming conversion 343 const int mcuPixelHeight = imageInfo.m_MCUHeight; 344 const int mcuRowPixels = imageInfo.m_width * mcuPixelHeight; 345 346 // Validate MCU row buffer size before allocation 347 if (mcuRowPixels > MAX_MCU_ROW_BYTES) { 348 Serial.printf("[%lu] [JPG] MCU row buffer too large (%d bytes), max: %d\n", millis(), mcuRowPixels, 349 MAX_MCU_ROW_BYTES); 350 free(rowBuffer); 351 return false; 352 } 353 354 auto* mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels)); 355 if (!mcuRowBuffer) { 356 Serial.printf("[%lu] [JPG] Failed to allocate MCU row buffer (%d bytes)\n", millis(), mcuRowPixels); 357 free(rowBuffer); 358 return false; 359 } 360 361 // Create ditherer if enabled (skip in quickMode for faster preview) 362 // Use OUTPUT dimensions for dithering (after prescaling) 363 AtkinsonDitherer* atkinsonDitherer = nullptr; 364 FloydSteinbergDitherer* fsDitherer = nullptr; 365 Atkinson1BitDitherer* atkinson1BitDitherer = nullptr; 366 367 if (!quickMode) { 368 if (oneBit) { 369 // For 1-bit output, use Atkinson dithering for better quality 370 atkinson1BitDitherer = new (std::nothrow) Atkinson1BitDitherer(outWidth); 371 if (!atkinson1BitDitherer) { 372 Serial.printf("[%lu] [JPG] Failed to allocate 1-bit ditherer\n", millis()); 373 free(mcuRowBuffer); 374 free(rowBuffer); 375 return false; 376 } 377 } else if (!USE_8BIT_OUTPUT) { 378 if (USE_ATKINSON) { 379 atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(outWidth); 380 if (!atkinsonDitherer) { 381 Serial.printf("[%lu] [JPG] Failed to allocate Atkinson ditherer\n", millis()); 382 free(mcuRowBuffer); 383 free(rowBuffer); 384 return false; 385 } 386 } else if (USE_FLOYD_STEINBERG) { 387 fsDitherer = new (std::nothrow) FloydSteinbergDitherer(outWidth); 388 if (!fsDitherer) { 389 Serial.printf("[%lu] [JPG] Failed to allocate Floyd-Steinberg ditherer\n", millis()); 390 free(mcuRowBuffer); 391 free(rowBuffer); 392 return false; 393 } 394 } 395 } 396 } 397 398 // For scaling: accumulate source rows into scaled output rows 399 // We need to track which source Y maps to which output Y 400 // Using fixed-point: srcY_fp = outY * scaleY_fp (gives source Y in 16.16 format) 401 uint32_t* rowAccum = nullptr; // Accumulator for each output X (32-bit for larger sums) 402 uint16_t* rowCount = nullptr; // Count of source pixels accumulated per output X 403 int currentOutY = 0; // Current output row being accumulated 404 uint32_t nextOutY_srcStart = 0; // Source Y where next output row starts (16.16 fixed point) 405 406 if (needsScaling) { 407 rowAccum = new uint32_t[outWidth](); 408 rowCount = new uint16_t[outWidth](); 409 nextOutY_srcStart = scaleY_fp; // First boundary is at scaleY_fp (source Y for outY=1) 410 } 411 412 // Process MCUs row-by-row and write to BMP as we go (top-down) 413 const int mcuPixelWidth = imageInfo.m_MCUWidth; 414 415 for (int mcuY = 0; mcuY < imageInfo.m_MCUSPerCol; mcuY++) { 416 // Clear the MCU row buffer 417 memset(mcuRowBuffer, 0, mcuRowPixels); 418 419 // Decode one row of MCUs 420 for (int mcuX = 0; mcuX < imageInfo.m_MCUSPerRow; mcuX++) { 421 const unsigned char mcuStatus = pjpeg_decode_mcu(); 422 if (mcuStatus != 0) { 423 if (mcuStatus == PJPG_NO_MORE_BLOCKS) { 424 Serial.printf("[%lu] [JPG] Unexpected end of blocks at MCU (%d, %d)\n", millis(), mcuX, mcuY); 425 } else { 426 Serial.printf("[%lu] [JPG] JPEG decode MCU failed at (%d, %d) with error code: %d\n", millis(), mcuX, mcuY, 427 mcuStatus); 428 } 429 free(mcuRowBuffer); 430 free(rowBuffer); 431 return false; 432 } 433 434 // picojpeg stores MCU data in 8x8 blocks 435 // Block layout: H2V2(16x16)=0,64,128,192 H2V1(16x8)=0,64 H1V2(8x16)=0,128 436 for (int blockY = 0; blockY < mcuPixelHeight; blockY++) { 437 for (int blockX = 0; blockX < mcuPixelWidth; blockX++) { 438 const int pixelX = mcuX * mcuPixelWidth + blockX; 439 if (pixelX >= imageInfo.m_width) continue; 440 441 // Calculate proper block offset for picojpeg buffer 442 const int blockCol = blockX / 8; 443 const int blockRow = blockY / 8; 444 const int localX = blockX % 8; 445 const int localY = blockY % 8; 446 const int blocksPerRow = mcuPixelWidth / 8; 447 const int blockIndex = blockRow * blocksPerRow + blockCol; 448 const int pixelOffset = blockIndex * 64 + localY * 8 + localX; 449 450 uint8_t gray; 451 if (imageInfo.m_comps == 1) { 452 gray = imageInfo.m_pMCUBufR[pixelOffset]; 453 } else { 454 const uint8_t r = imageInfo.m_pMCUBufR[pixelOffset]; 455 const uint8_t g = imageInfo.m_pMCUBufG[pixelOffset]; 456 const uint8_t b = imageInfo.m_pMCUBufB[pixelOffset]; 457 gray = rgbToGray(r, g, b); 458 } 459 460 mcuRowBuffer[blockY * imageInfo.m_width + pixelX] = gray; 461 } 462 } 463 } 464 465 // Process source rows from this MCU row 466 const int startRow = mcuY * mcuPixelHeight; 467 const int endRow = (mcuY + 1) * mcuPixelHeight; 468 469 for (int y = startRow; y < endRow && y < imageInfo.m_height; y++) { 470 const int bufferY = y - startRow; 471 472 if (!needsScaling) { 473 // No scaling - direct output (1:1 mapping) 474 memset(rowBuffer, 0, bytesPerRow); 475 476 if (USE_8BIT_OUTPUT && !oneBit) { 477 for (int x = 0; x < outWidth; x++) { 478 const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x]; 479 rowBuffer[x] = adjustPixel(gray); 480 } 481 } else if (oneBit) { 482 // 1-bit output with Atkinson dithering for better quality 483 for (int x = 0; x < outWidth; x++) { 484 const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x]; 485 const uint8_t bit = 486 atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, y); 487 // Pack 1-bit value: MSB first, 8 pixels per byte 488 const int byteIndex = x / 8; 489 const int bitOffset = 7 - (x % 8); 490 rowBuffer[byteIndex] |= (bit << bitOffset); 491 } 492 if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow(); 493 } else { 494 // 2-bit output 495 for (int x = 0; x < outWidth; x++) { 496 const uint8_t gray = adjustPixel(mcuRowBuffer[bufferY * imageInfo.m_width + x]); 497 uint8_t twoBit; 498 if (quickMode) { 499 // Quick mode: simple threshold (faster, no dithering) 500 twoBit = quantizeSimple(gray); 501 } else if (atkinsonDitherer) { 502 twoBit = atkinsonDitherer->processPixel(gray, x); 503 } else if (fsDitherer) { 504 twoBit = fsDitherer->processPixel(gray, x); 505 } else { 506 twoBit = quantize(gray, x, y); 507 } 508 const int byteIndex = (x * 2) / 8; 509 const int bitOffset = 6 - ((x * 2) % 8); 510 rowBuffer[byteIndex] |= (twoBit << bitOffset); 511 } 512 if (atkinsonDitherer) 513 atkinsonDitherer->nextRow(); 514 else if (fsDitherer) 515 fsDitherer->nextRow(); 516 } 517 bmpOut.write(rowBuffer, bytesPerRow); 518 } else { 519 // Fixed-point area averaging for exact fit scaling 520 // For each output pixel X, accumulate source pixels that map to it 521 // srcX range for outX: [outX * scaleX_fp >> 16, (outX+1) * scaleX_fp >> 16) 522 const uint8_t* srcRow = mcuRowBuffer + bufferY * imageInfo.m_width; 523 524 for (int outX = 0; outX < outWidth; outX++) { 525 // Calculate source X range for this output pixel 526 const int srcXStart = (static_cast<uint32_t>(outX) * scaleX_fp) >> 16; 527 const int srcXEnd = (static_cast<uint32_t>(outX + 1) * scaleX_fp) >> 16; 528 529 // Accumulate all source pixels in this range 530 int sum = 0; 531 int count = 0; 532 for (int srcX = srcXStart; srcX < srcXEnd && srcX < imageInfo.m_width; srcX++) { 533 sum += srcRow[srcX]; 534 count++; 535 } 536 537 // Handle edge case: if no pixels in range, use nearest 538 if (count == 0 && srcXStart < imageInfo.m_width) { 539 sum = srcRow[srcXStart]; 540 count = 1; 541 } 542 543 rowAccum[outX] += sum; 544 rowCount[outX] += count; 545 } 546 547 // Check if we've crossed into the next output row 548 // Current source Y in fixed point: y << 16 549 const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16; 550 551 // Output row when source Y crosses the boundary 552 if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) { 553 memset(rowBuffer, 0, bytesPerRow); 554 555 if (USE_8BIT_OUTPUT && !oneBit) { 556 for (int x = 0; x < outWidth; x++) { 557 const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0; 558 rowBuffer[x] = adjustPixel(gray); 559 } 560 } else if (oneBit) { 561 // 1-bit output with Atkinson dithering for better quality 562 for (int x = 0; x < outWidth; x++) { 563 const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0; 564 const uint8_t bit = atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) 565 : quantize1bit(gray, x, currentOutY); 566 // Pack 1-bit value: MSB first, 8 pixels per byte 567 const int byteIndex = x / 8; 568 const int bitOffset = 7 - (x % 8); 569 rowBuffer[byteIndex] |= (bit << bitOffset); 570 } 571 if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow(); 572 } else { 573 // 2-bit output 574 for (int x = 0; x < outWidth; x++) { 575 const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0); 576 uint8_t twoBit; 577 if (quickMode) { 578 // Quick mode: simple threshold (faster, no dithering) 579 twoBit = quantizeSimple(gray); 580 } else if (atkinsonDitherer) { 581 twoBit = atkinsonDitherer->processPixel(gray, x); 582 } else if (fsDitherer) { 583 twoBit = fsDitherer->processPixel(gray, x); 584 } else { 585 twoBit = quantize(gray, x, currentOutY); 586 } 587 const int byteIndex = (x * 2) / 8; 588 const int bitOffset = 6 - ((x * 2) % 8); 589 rowBuffer[byteIndex] |= (twoBit << bitOffset); 590 } 591 if (atkinsonDitherer) 592 atkinsonDitherer->nextRow(); 593 else if (fsDitherer) 594 fsDitherer->nextRow(); 595 } 596 597 bmpOut.write(rowBuffer, bytesPerRow); 598 currentOutY++; 599 600 // Reset accumulators for next output row 601 memset(rowAccum, 0, outWidth * sizeof(uint32_t)); 602 memset(rowCount, 0, outWidth * sizeof(uint16_t)); 603 604 // Update boundary for next output row 605 nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp; 606 } 607 } 608 } 609 } 610 611 // Clean up 612 if (rowAccum) { 613 delete[] rowAccum; 614 } 615 if (rowCount) { 616 delete[] rowCount; 617 } 618 if (atkinsonDitherer) { 619 delete atkinsonDitherer; 620 } 621 if (fsDitherer) { 622 delete fsDitherer; 623 } 624 if (atkinson1BitDitherer) { 625 delete atkinson1BitDitherer; 626 } 627 free(mcuRowBuffer); 628 free(rowBuffer); 629 630 Serial.printf("[%lu] [JPG] Successfully converted JPEG to BMP\n", millis()); 631 return true; 632 } 633 634 // Core function: Convert JPEG file to 2-bit BMP (uses default target size) 635 bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) { 636 return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false); 637 } 638 639 // Convert with custom target size (for thumbnails, 2-bit) 640 bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, 641 int targetMaxHeight) { 642 return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, false); 643 } 644 645 // Convert to 1-bit BMP (black and white only, no grays) using default target size 646 bool JpegToBmpConverter::jpegFileTo1BitBmpStream(FsFile& jpegFile, Print& bmpOut) { 647 return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, true); 648 } 649 650 // Convert to 1-bit BMP with custom target size (for thumbnails) 651 bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, 652 int targetMaxHeight) { 653 return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true); 654 } 655 656 // Quick preview mode: simple threshold instead of dithering (faster but lower quality) 657 bool JpegToBmpConverter::jpegFileToBmpStreamQuick(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, 658 int targetMaxHeight) { 659 return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, false, true); 660 }