diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 0dd78727..a36d11ad 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -305,6 +305,43 @@ static void flushScaledRow(BmpConvertCtx* ctx) { ctx->currentOutY++; } +// Scans JPEG markers for SOF2 (progressive DCT) — JPEGDEC only handles baseline/sequential. +static bool isProgressiveJpeg(FsFile& file) { + file.seek(0); + uint8_t buf[2]; + if (file.read(buf, 2) != 2 || buf[0] != 0xFF || buf[1] != 0xD8) { + file.seek(0); + return false; + } + while (file.available() >= 2) { + uint8_t b; + if (file.read(&b, 1) != 1 || b != 0xFF) break; + // skip fill bytes (JPEG allows 0xFF padding before a marker byte) + do { + if (file.read(&b, 1) != 1) { + file.seek(0); + return false; + } + } while (b == 0xFF); + const uint8_t marker = b; + if (marker == 0xC2) { + LOG_DBG("JPG", "Detected progressive JPEG (SOF2)"); + file.seek(0); + return true; + } + if (marker == 0xC0 || marker == 0xC1 || marker == 0xC3) { + file.seek(0); + return false; + } + if (marker == 0xD9) break; + if (file.read(buf, 2) != 2) break; + const int segLen = (static_cast(buf[0]) << 8) | buf[1]; + if (segLen < 2 || !file.seek(file.position() + segLen - 2)) break; + } + file.seek(0); + return false; +} + // JPEGDEC draw callback — receives one MCU-width × MCU-height block at a time, // in left-to-right, top-to-bottom order (baseline JPEG). // Accumulates columns into mcuBuf; once the last column arrives (completing the MCU @@ -330,10 +367,10 @@ int bmpDrawCallback(JPEGDRAW* pDraw) { // Wait for the last MCU column before processing any rows if (blockX + validW < ctx->srcWidth) return 1; - // Process each complete source row in this MCU row - const int endRow = blockY + blockH; - - for (int y = blockY; y < endRow && y < ctx->srcHeight; y++) { + // Process each complete source row in this MCU row. + // Clamp to MAX_MCU_HEIGHT so srcRow never indexes past the populated mcuBuf rows. + const int safeEndRow = blockY + std::min(blockH, MAX_MCU_HEIGHT); + for (int y = blockY; y < safeEndRow && y < ctx->srcHeight; y++) { const uint8_t* srcRow = ctx->mcuBuf + (y - blockY) * ctx->srcWidth; if (!ctx->needsScaling) { @@ -385,6 +422,10 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm return false; } + // Progressive JPEGs (SOF2) must use JPEG_SCALE_EIGHTH — the only mode safe with the MCU_SKIP patch. + // The 1/8-scale output is then passed through the custom scaler to reach the target dimensions. + const bool progressive = isProgressiveJpeg(jpegFile); + s_jpegFile = &jpegFile; JPEGDEC* jpeg = new (std::nothrow) JPEGDEC(); @@ -403,7 +444,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm const int srcWidth = jpeg->getWidth(); const int srcHeight = jpeg->getHeight(); - LOG_DBG("JPG", "JPEG dimensions: %dx%d", srcWidth, srcHeight); + LOG_DBG("JPG", "JPEG dimensions: %dx%d%s", srcWidth, srcHeight, progressive ? " (progressive)" : ""); constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_HEIGHT = 3072; @@ -416,16 +457,20 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm return false; } + const int effectiveSrcW = progressive ? (srcWidth + 7) / 8 : srcWidth; + const int effectiveSrcH = progressive ? (srcHeight + 7) / 8 : srcHeight; + const int decodeFlags = progressive ? JPEG_SCALE_EIGHTH : 0; + // Calculate output dimensions (pre-scale to fit display exactly) - int outWidth = srcWidth; - int outHeight = srcHeight; + int outWidth = effectiveSrcW; + int outHeight = effectiveSrcH; uint32_t scaleX_fp = 65536; // 1.0 in 16.16 fixed point uint32_t scaleY_fp = 65536; bool needsScaling = false; - if (targetWidth > 0 && targetHeight > 0 && (srcWidth != targetWidth || srcHeight != targetHeight)) { - const float scaleToFitWidth = static_cast(targetWidth) / srcWidth; - const float scaleToFitHeight = static_cast(targetHeight) / srcHeight; + if (targetWidth > 0 && targetHeight > 0 && (effectiveSrcW != targetWidth || effectiveSrcH != targetHeight)) { + const float scaleToFitWidth = static_cast(targetWidth) / effectiveSrcW; + const float scaleToFitHeight = static_cast(targetHeight) / effectiveSrcH; float scale = 1.0f; if (crop) { scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; @@ -433,17 +478,17 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; } - outWidth = static_cast(srcWidth * scale); - outHeight = static_cast(srcHeight * scale); + outWidth = static_cast(effectiveSrcW * scale); + outHeight = static_cast(effectiveSrcH * scale); if (outWidth < 1) outWidth = 1; if (outHeight < 1) outHeight = 1; - scaleX_fp = (static_cast(srcWidth) << 16) / outWidth; - scaleY_fp = (static_cast(srcHeight) << 16) / outHeight; + scaleX_fp = (static_cast(effectiveSrcW) << 16) / outWidth; + scaleY_fp = (static_cast(effectiveSrcH) << 16) / outHeight; needsScaling = true; - LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", srcWidth, srcHeight, outWidth, outHeight, targetWidth, - targetHeight); + LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", effectiveSrcW, effectiveSrcH, outWidth, outHeight, + targetWidth, targetHeight); } // Write BMP header with output dimensions @@ -461,8 +506,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm BmpConvertCtx ctx = {}; ctx.bmpOut = &bmpOut; - ctx.srcWidth = srcWidth; - ctx.srcHeight = srcHeight; + ctx.srcWidth = effectiveSrcW; + ctx.srcHeight = effectiveSrcH; ctx.outWidth = outWidth; ctx.outHeight = outHeight; ctx.oneBit = oneBit; @@ -489,13 +534,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm } } cleanup{ctx, jpeg}; - // MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale - ctx.mcuBuf = static_cast(malloc(MAX_MCU_HEIGHT * srcWidth)); + ctx.mcuBuf = static_cast(malloc(MAX_MCU_HEIGHT * effectiveSrcW)); if (!ctx.mcuBuf) { - LOG_ERR("JPG", "Failed to allocate MCU buffer (%d bytes)", MAX_MCU_HEIGHT * srcWidth); + LOG_ERR("JPG", "Failed to allocate MCU buffer (%d bytes)", MAX_MCU_HEIGHT * effectiveSrcW); return false; } - memset(ctx.mcuBuf, 0, MAX_MCU_HEIGHT * srcWidth); + memset(ctx.mcuBuf, 0, MAX_MCU_HEIGHT * effectiveSrcW); ctx.bmpRow = static_cast(malloc(bytesPerRow)); if (!ctx.bmpRow) { @@ -526,13 +570,18 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); jpeg->setUserPointer(&ctx); - rc = jpeg->decode(0, 0, 0); + rc = jpeg->decode(0, 0, decodeFlags); if (rc != 1 || ctx.error) { LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError()); return false; } + if (ctx.needsScaling && ctx.currentOutY < ctx.outHeight) { + LOG_ERR("JPG", "JPEG decode incomplete: %d/%d output rows written", ctx.currentOutY, ctx.outHeight); + return false; + } + LOG_DBG("JPG", "Successfully converted JPEG to BMP"); return true; }