Merge pull request #182 from jpirnay/feat-progressive-jpeg

feat: Add progressive jpeg support (idea from broemp)
This commit is contained in:
jpirnay
2026-05-07 22:17:26 +02:00
committed by GitHub
+72 -23
View File
@@ -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<int>(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<float>(targetWidth) / srcWidth;
const float scaleToFitHeight = static_cast<float>(targetHeight) / srcHeight;
if (targetWidth > 0 && targetHeight > 0 && (effectiveSrcW != targetWidth || effectiveSrcH != targetHeight)) {
const float scaleToFitWidth = static_cast<float>(targetWidth) / effectiveSrcW;
const float scaleToFitHeight = static_cast<float>(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<int>(srcWidth * scale);
outHeight = static_cast<int>(srcHeight * scale);
outWidth = static_cast<int>(effectiveSrcW * scale);
outHeight = static_cast<int>(effectiveSrcH * scale);
if (outWidth < 1) outWidth = 1;
if (outHeight < 1) outHeight = 1;
scaleX_fp = (static_cast<uint32_t>(srcWidth) << 16) / outWidth;
scaleY_fp = (static_cast<uint32_t>(srcHeight) << 16) / outHeight;
scaleX_fp = (static_cast<uint32_t>(effectiveSrcW) << 16) / outWidth;
scaleY_fp = (static_cast<uint32_t>(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<uint8_t*>(malloc(MAX_MCU_HEIGHT * srcWidth));
ctx.mcuBuf = static_cast<uint8_t*>(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<uint8_t*>(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;
}