Merge pull request #182 from jpirnay/feat-progressive-jpeg
feat: Add progressive jpeg support (idea from broemp)
This commit is contained in:
@@ -305,6 +305,43 @@ static void flushScaledRow(BmpConvertCtx* ctx) {
|
|||||||
ctx->currentOutY++;
|
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,
|
// JPEGDEC draw callback — receives one MCU-width × MCU-height block at a time,
|
||||||
// in left-to-right, top-to-bottom order (baseline JPEG).
|
// in left-to-right, top-to-bottom order (baseline JPEG).
|
||||||
// Accumulates columns into mcuBuf; once the last column arrives (completing the MCU
|
// 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
|
// Wait for the last MCU column before processing any rows
|
||||||
if (blockX + validW < ctx->srcWidth) return 1;
|
if (blockX + validW < ctx->srcWidth) return 1;
|
||||||
|
|
||||||
// Process each complete source row in this MCU row
|
// Process each complete source row in this MCU row.
|
||||||
const int endRow = blockY + blockH;
|
// 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 < endRow && y < ctx->srcHeight; y++) {
|
for (int y = blockY; y < safeEndRow && y < ctx->srcHeight; y++) {
|
||||||
const uint8_t* srcRow = ctx->mcuBuf + (y - blockY) * ctx->srcWidth;
|
const uint8_t* srcRow = ctx->mcuBuf + (y - blockY) * ctx->srcWidth;
|
||||||
|
|
||||||
if (!ctx->needsScaling) {
|
if (!ctx->needsScaling) {
|
||||||
@@ -385,6 +422,10 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
return false;
|
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;
|
s_jpegFile = &jpegFile;
|
||||||
|
|
||||||
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
|
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
|
||||||
@@ -403,7 +444,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
const int srcWidth = jpeg->getWidth();
|
const int srcWidth = jpeg->getWidth();
|
||||||
const int srcHeight = jpeg->getHeight();
|
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_WIDTH = 2048;
|
||||||
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
||||||
@@ -416,16 +457,20 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
return false;
|
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)
|
// Calculate output dimensions (pre-scale to fit display exactly)
|
||||||
int outWidth = srcWidth;
|
int outWidth = effectiveSrcW;
|
||||||
int outHeight = srcHeight;
|
int outHeight = effectiveSrcH;
|
||||||
uint32_t scaleX_fp = 65536; // 1.0 in 16.16 fixed point
|
uint32_t scaleX_fp = 65536; // 1.0 in 16.16 fixed point
|
||||||
uint32_t scaleY_fp = 65536;
|
uint32_t scaleY_fp = 65536;
|
||||||
bool needsScaling = false;
|
bool needsScaling = false;
|
||||||
|
|
||||||
if (targetWidth > 0 && targetHeight > 0 && (srcWidth != targetWidth || srcHeight != targetHeight)) {
|
if (targetWidth > 0 && targetHeight > 0 && (effectiveSrcW != targetWidth || effectiveSrcH != targetHeight)) {
|
||||||
const float scaleToFitWidth = static_cast<float>(targetWidth) / srcWidth;
|
const float scaleToFitWidth = static_cast<float>(targetWidth) / effectiveSrcW;
|
||||||
const float scaleToFitHeight = static_cast<float>(targetHeight) / srcHeight;
|
const float scaleToFitHeight = static_cast<float>(targetHeight) / effectiveSrcH;
|
||||||
float scale = 1.0f;
|
float scale = 1.0f;
|
||||||
if (crop) {
|
if (crop) {
|
||||||
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
@@ -433,17 +478,17 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
outWidth = static_cast<int>(srcWidth * scale);
|
outWidth = static_cast<int>(effectiveSrcW * scale);
|
||||||
outHeight = static_cast<int>(srcHeight * scale);
|
outHeight = static_cast<int>(effectiveSrcH * scale);
|
||||||
if (outWidth < 1) outWidth = 1;
|
if (outWidth < 1) outWidth = 1;
|
||||||
if (outHeight < 1) outHeight = 1;
|
if (outHeight < 1) outHeight = 1;
|
||||||
|
|
||||||
scaleX_fp = (static_cast<uint32_t>(srcWidth) << 16) / outWidth;
|
scaleX_fp = (static_cast<uint32_t>(effectiveSrcW) << 16) / outWidth;
|
||||||
scaleY_fp = (static_cast<uint32_t>(srcHeight) << 16) / outHeight;
|
scaleY_fp = (static_cast<uint32_t>(effectiveSrcH) << 16) / outHeight;
|
||||||
needsScaling = true;
|
needsScaling = true;
|
||||||
|
|
||||||
LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", srcWidth, srcHeight, outWidth, outHeight, targetWidth,
|
LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", effectiveSrcW, effectiveSrcH, outWidth, outHeight,
|
||||||
targetHeight);
|
targetWidth, targetHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write BMP header with output dimensions
|
// Write BMP header with output dimensions
|
||||||
@@ -461,8 +506,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
|
|
||||||
BmpConvertCtx ctx = {};
|
BmpConvertCtx ctx = {};
|
||||||
ctx.bmpOut = &bmpOut;
|
ctx.bmpOut = &bmpOut;
|
||||||
ctx.srcWidth = srcWidth;
|
ctx.srcWidth = effectiveSrcW;
|
||||||
ctx.srcHeight = srcHeight;
|
ctx.srcHeight = effectiveSrcH;
|
||||||
ctx.outWidth = outWidth;
|
ctx.outWidth = outWidth;
|
||||||
ctx.outHeight = outHeight;
|
ctx.outHeight = outHeight;
|
||||||
ctx.oneBit = oneBit;
|
ctx.oneBit = oneBit;
|
||||||
@@ -489,13 +534,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
}
|
}
|
||||||
} cleanup{ctx, jpeg};
|
} cleanup{ctx, jpeg};
|
||||||
|
|
||||||
// MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale
|
ctx.mcuBuf = static_cast<uint8_t*>(malloc(MAX_MCU_HEIGHT * effectiveSrcW));
|
||||||
ctx.mcuBuf = static_cast<uint8_t*>(malloc(MAX_MCU_HEIGHT * srcWidth));
|
|
||||||
if (!ctx.mcuBuf) {
|
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;
|
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));
|
ctx.bmpRow = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||||
if (!ctx.bmpRow) {
|
if (!ctx.bmpRow) {
|
||||||
@@ -526,13 +570,18 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
||||||
jpeg->setUserPointer(&ctx);
|
jpeg->setUserPointer(&ctx);
|
||||||
|
|
||||||
rc = jpeg->decode(0, 0, 0);
|
rc = jpeg->decode(0, 0, decodeFlags);
|
||||||
|
|
||||||
if (rc != 1 || ctx.error) {
|
if (rc != 1 || ctx.error) {
|
||||||
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
||||||
return false;
|
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");
|
LOG_DBG("JPG", "Successfully converted JPEG to BMP");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user