Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
28 kB · 734 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735#include "JpegToBmpConverter.h"
#include <HalDisplay.h>#include <HalStorage.h>#include <JPEGDEC.h>#include <Logging.h>#include <Memory.h>#include <freertos/FreeRTOS.h>#include <freertos/task.h>
#include <cstdio>#include <cstring>
#include "BitmapHelpers.h"
// ============================================================================// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations// ============================================================================constexpr bool USE_8BIT_OUTPUT = false; // true: 8-bit grayscale (no quantization), false: 2-bit (4 levels)// Dithering method selection (only one should be true, or all false for simple quantization):constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion)constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts)constexpr bool USE_NOISE_DITHERING = false; // Hash-based noise dithering (good for downsampling)// Pre-resize to target display size (CRITICAL: avoids dithering artifacts from post-downsampling)constexpr bool USE_PRESCALE = true; // true: scale image to target size before dithering// ============================================================================
inline void write16(Print& out, const uint16_t value) { out.write(value & 0xFF); out.write((value >> 8) & 0xFF);}
inline void write32(Print& out, const uint32_t value) { out.write(value & 0xFF); out.write((value >> 8) & 0xFF); out.write((value >> 16) & 0xFF); out.write((value >> 24) & 0xFF);}
inline void write32Signed(Print& out, const int32_t value) { out.write(value & 0xFF); out.write((value >> 8) & 0xFF); out.write((value >> 16) & 0xFF); out.write((value >> 24) & 0xFF);}
// Helper function: Write BMP header with 8-bit grayscale (256 levels)void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) { // Calculate row padding (each row must be multiple of 4 bytes) const int bytesPerRow = (width + 3) / 4 * 4; // 8 bits per pixel, padded const int imageSize = bytesPerRow * height; const uint32_t paletteSize = 256 * 4; // 256 colors * 4 bytes (BGRA) const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
// BMP File Header (14 bytes) bmpOut.write('B'); bmpOut.write('M'); write32(bmpOut, fileSize); write32(bmpOut, 0); // Reserved write32(bmpOut, 14 + 40 + paletteSize); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER - 40 bytes) write32(bmpOut, 40); write32Signed(bmpOut, width); write32Signed(bmpOut, -height); // Negative height = top-down bitmap write16(bmpOut, 1); // Color planes write16(bmpOut, 8); // Bits per pixel (8 bits) write32(bmpOut, 0); // BI_RGB (no compression) write32(bmpOut, imageSize); write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI) write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI) write32(bmpOut, 256); // colorsUsed write32(bmpOut, 256); // colorsImportant
// Color Palette (256 grayscale entries x 4 bytes = 1024 bytes) for (int i = 0; i < 256; i++) { bmpOut.write(static_cast<uint8_t>(i)); // Blue bmpOut.write(static_cast<uint8_t>(i)); // Green bmpOut.write(static_cast<uint8_t>(i)); // Red bmpOut.write(static_cast<uint8_t>(0)); // Reserved }}
// Helper function: Write BMP header with 1-bit color depth (black and white)static void writeBmpHeader1bit(Print& bmpOut, const int width, const int height) { // Calculate row padding (each row must be multiple of 4 bytes) const int bytesPerRow = (width + 31) / 32 * 4; // 1 bit per pixel, round up to 4-byte boundary const int imageSize = bytesPerRow * height; const uint32_t fileSize = 62 + imageSize; // 14 (file header) + 40 (DIB header) + 8 (palette) + image
// BMP File Header (14 bytes) bmpOut.write('B'); bmpOut.write('M'); write32(bmpOut, fileSize); // File size write32(bmpOut, 0); // Reserved write32(bmpOut, 62); // Offset to pixel data (14 + 40 + 8)
// DIB Header (BITMAPINFOHEADER - 40 bytes) write32(bmpOut, 40); write32Signed(bmpOut, width); write32Signed(bmpOut, -height); // Negative height = top-down bitmap write16(bmpOut, 1); // Color planes write16(bmpOut, 1); // Bits per pixel (1 bit) write32(bmpOut, 0); // BI_RGB (no compression) write32(bmpOut, imageSize); write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI) write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI) write32(bmpOut, 2); // colorsUsed write32(bmpOut, 2); // colorsImportant
// Color Palette (2 colors x 4 bytes = 8 bytes) // Format: Blue, Green, Red, Reserved (BGRA) // Note: In 1-bit BMP, palette index 0 = black, 1 = white uint8_t palette[8] = { 0x00, 0x00, 0x00, 0x00, // Color 0: Black 0xFF, 0xFF, 0xFF, 0x00 // Color 1: White }; for (const uint8_t i : palette) { bmpOut.write(i); }}
// Helper function: Write BMP header with 2-bit color depthstatic void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) { // Calculate row padding (each row must be multiple of 4 bytes) const int bytesPerRow = (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up const int imageSize = bytesPerRow * height; const uint32_t fileSize = 70 + imageSize; // 14 (file header) + 40 (DIB header) + 16 (palette) + image
// BMP File Header (14 bytes) bmpOut.write('B'); bmpOut.write('M'); write32(bmpOut, fileSize); // File size write32(bmpOut, 0); // Reserved write32(bmpOut, 70); // Offset to pixel data
// DIB Header (BITMAPINFOHEADER - 40 bytes) write32(bmpOut, 40); write32Signed(bmpOut, width); write32Signed(bmpOut, -height); // Negative height = top-down bitmap write16(bmpOut, 1); // Color planes write16(bmpOut, 2); // Bits per pixel (2 bits) write32(bmpOut, 0); // BI_RGB (no compression) write32(bmpOut, imageSize); write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI) write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI) write32(bmpOut, 4); // colorsUsed write32(bmpOut, 4); // colorsImportant
// Color Palette (4 colors x 4 bytes = 16 bytes) // Format: Blue, Green, Red, Reserved (BGRA) uint8_t palette[16] = { 0x00, 0x00, 0x00, 0x00, // Color 0: Black 0x55, 0x55, 0x55, 0x00, // Color 1: Dark gray (85) 0xAA, 0xAA, 0xAA, 0x00, // Color 2: Light gray (170) 0xFF, 0xFF, 0xFF, 0x00 // Color 3: White }; for (const uint8_t i : palette) { bmpOut.write(i); }}
namespace {
// Max MCU height supported by any JPEG (4:2:0 chroma = 16 rows, 4:4:4 = 8 rows)constexpr int MAX_MCU_HEIGHT = 16;constexpr size_t JPEG_DECODER_SIZE = 20 * 1024;constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024;constexpr uint32_t FP_ONE = 1UL << 16;
// Static file pointer for JPEGDEC open callback.// Safe in single-threaded embedded context; never accessed concurrently.static HalFile* s_jpegFile = nullptr;static uint8_t s_jpegIoSinceYield = 0;
static void yieldToIdle() { vTaskDelay(1); }
static void yieldDuringJpegIo() { if (++s_jpegIoSinceYield < 4) return; s_jpegIoSinceYield = 0; yieldToIdle();}
void* bmpJpegOpen(const char* /*filename*/, int32_t* size) { if (!s_jpegFile || !*s_jpegFile) return nullptr; s_jpegIoSinceYield = 0; s_jpegFile->seek(0); *size = static_cast<int32_t>(s_jpegFile->size()); yieldDuringJpegIo(); return s_jpegFile;}
void bmpJpegClose(void* /*handle*/) { // Caller owns the file — do not close it here}
int32_t bmpJpegRead(JPEGFILE* pFile, uint8_t* pBuf, int32_t len) { auto* f = reinterpret_cast<HalFile*>(pFile->fHandle); if (!f) return 0; int32_t n = f->read(pBuf, len); if (n < 0) n = 0; pFile->iPos += n; yieldDuringJpegIo(); return n;}
int32_t bmpJpegSeek(JPEGFILE* pFile, int32_t pos) { auto* f = reinterpret_cast<HalFile*>(pFile->fHandle); if (!f || !f->seek(pos)) return -1; pFile->iPos = pos; yieldDuringJpegIo(); return pos;}
// Context passed to the JPEGDEC draw callback via setUserPointer()struct BmpConvertCtx { Print* bmpOut; int srcWidth; int srcHeight; int outWidth; int outHeight; bool oneBit; int bytesPerRow; bool needsScaling; uint32_t scaleX_fp; // source pixels per output pixel, 16.16 fixed-point uint32_t scaleY_fp; bool smoothUpscale; uint32_t smoothScaleX_fp; uint32_t smoothScaleY_fp;
// Accumulates one MCU row (up to MAX_MCU_HEIGHT source rows × srcWidth pixels) // Filled column-by-column as JPEGDEC callbacks arrive for the same MCU row std::unique_ptr<uint8_t[]> mcuBuf;
// Y-axis area averaging accumulators (needsScaling only) int currentOutY; uint32_t nextOutY_srcStart; // 16.16 fixed-point boundary for the next output row std::unique_ptr<uint32_t[]> rowAccum; std::unique_ptr<uint32_t[]> rowCount;
int smoothNextOutY; int smoothPrevY; std::unique_ptr<uint8_t[]> smoothRows; uint8_t* smoothPrevRow; uint8_t* smoothCurrRow; uint8_t* smoothOutRow;
std::unique_ptr<uint8_t[]> bmpRow;
std::unique_ptr<AtkinsonDitherer> atkinsonDitherer; std::unique_ptr<FloydSteinbergDitherer> fsDitherer; std::unique_ptr<Atkinson1BitDitherer> atkinson1BitDitherer;
uint8_t rowsSinceYield; uint8_t blocksSinceYield; bool error;};
static void yieldDuringDecode(BmpConvertCtx* ctx) { if (++ctx->rowsSinceYield < 8) return; ctx->rowsSinceYield = 0; yieldToIdle();}
static void yieldDuringDecodeBlock(BmpConvertCtx* ctx) { if (++ctx->blocksSinceYield < 16) return; ctx->blocksSinceYield = 0; yieldToIdle();}
// Write a fully-assembled output row (grayscale bytes, length outWidth) to BMPstatic void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY) { memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
if (USE_8BIT_OUTPUT && !ctx->oneBit) { for (int x = 0; x < ctx->outWidth; x++) { ctx->bmpRow[x] = adjustPixel(srcRow[x]); } } else if (ctx->oneBit) { for (int x = 0; x < ctx->outWidth; x++) { const uint8_t bit = ctx->atkinson1BitDitherer ? ctx->atkinson1BitDitherer->processPixel(srcRow[x], x) : quantize1bit(srcRow[x], x, outY); ctx->bmpRow[x / 8] |= (bit << (7 - (x % 8))); } if (ctx->atkinson1BitDitherer) ctx->atkinson1BitDitherer->nextRow(); } else { for (int x = 0; x < ctx->outWidth; x++) { const uint8_t gray = adjustPixel(srcRow[x]); uint8_t twoBit; if (ctx->atkinsonDitherer) { twoBit = ctx->atkinsonDitherer->processPixel(gray, x); } else if (ctx->fsDitherer) { twoBit = ctx->fsDitherer->processPixel(gray, x); } else { twoBit = quantize(gray, x, outY); } ctx->bmpRow[(x * 2) / 8] |= (twoBit << (6 - ((x * 2) % 8))); } if (ctx->atkinsonDitherer) ctx->atkinsonDitherer->nextRow(); else if (ctx->fsDitherer) ctx->fsDitherer->nextRow(); }
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow); yieldDuringDecode(ctx);}
// Matches the progressive-JPEG smoothing used by JpegToFramebufferConverter, but stays// local because cover generation streams dithered BMP rows instead of framebuffer pixels.static uint32_t interpolationStep(const int srcSize, const int outSize) { if (srcSize <= 1 || outSize <= 1) return 0; return (static_cast<uint32_t>(srcSize - 1) << 16) / static_cast<uint32_t>(outSize - 1);}
static uint32_t interpolatedSourceFp(const int outIndex, const int outSize, const int srcSize, const uint32_t step) { if (srcSize <= 1 || outSize <= 1) return 0; if (outIndex >= outSize - 1) return static_cast<uint32_t>(srcSize - 1) << 16; return static_cast<uint32_t>(outIndex) * step;}
static void scaleRowLinear(BmpConvertCtx* ctx, const uint8_t* srcRow, uint8_t* dstRow) { for (int outX = 0; outX < ctx->outWidth; outX++) { const uint32_t srcX_fp = interpolatedSourceFp(outX, ctx->outWidth, ctx->srcWidth, ctx->smoothScaleX_fp); const int x0 = srcX_fp >> 16; const int x1 = (x0 + 1 < ctx->srcWidth) ? (x0 + 1) : x0; const uint32_t fx = srcX_fp & (FP_ONE - 1); dstRow[outX] = static_cast<uint8_t>((srcRow[x0] * (FP_ONE - fx) + srcRow[x1] * fx) >> 16); }}
static void writeBlendedRow(BmpConvertCtx* ctx, const uint8_t* row0, const uint8_t* row1, const uint32_t fy, const int outY) { const uint32_t invFy = FP_ONE - fy; for (int outX = 0; outX < ctx->outWidth; outX++) { ctx->smoothOutRow[outX] = static_cast<uint8_t>((row0[outX] * invFy + row1[outX] * fy) >> 16); } writeOutputRow(ctx, ctx->smoothOutRow, outY);}
static void processSmoothSourceRow(BmpConvertCtx* ctx, const uint8_t* srcRow, const int srcY) { scaleRowLinear(ctx, srcRow, ctx->smoothCurrRow);
if (ctx->smoothPrevY < 0) { uint8_t* tmp = ctx->smoothPrevRow; ctx->smoothPrevRow = ctx->smoothCurrRow; ctx->smoothCurrRow = tmp; ctx->smoothPrevY = srcY; if (ctx->srcHeight <= 1) { while (ctx->smoothNextOutY < ctx->outHeight) { writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY); ctx->smoothNextOutY++; } return; } return; }
while (ctx->smoothNextOutY < ctx->outHeight) { const uint32_t srcY_fp = interpolatedSourceFp(ctx->smoothNextOutY, ctx->outHeight, ctx->srcHeight, ctx->smoothScaleY_fp); const int y0 = srcY_fp >> 16; const int y1 = (y0 + 1 < ctx->srcHeight) ? (y0 + 1) : y0; if (y1 > srcY) break;
const uint8_t* row0 = (y0 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow; const uint8_t* row1 = (y1 == srcY) ? ctx->smoothCurrRow : ctx->smoothPrevRow; writeBlendedRow(ctx, row0, row1, srcY_fp & (FP_ONE - 1), ctx->smoothNextOutY); ctx->smoothNextOutY++; }
uint8_t* tmp = ctx->smoothPrevRow; ctx->smoothPrevRow = ctx->smoothCurrRow; ctx->smoothCurrRow = tmp; ctx->smoothPrevY = srcY;}
static void finishSmoothUpscale(BmpConvertCtx* ctx) { if (ctx->smoothPrevY < 0) { LOG_ERR("JPG", "No progressive rows decoded for smoothing"); ctx->error = true; return; }
while (ctx->smoothNextOutY < ctx->outHeight) { writeOutputRow(ctx, ctx->smoothPrevRow, ctx->smoothNextOutY); ctx->smoothNextOutY++; }}
// Flush one scaled output row from Y-axis accumulators and advance currentOutYstatic void flushScaledRow(BmpConvertCtx* ctx) { memset(ctx->bmpRow.get(), 0, ctx->bytesPerRow);
if (USE_8BIT_OUTPUT && !ctx->oneBit) { for (int x = 0; x < ctx->outWidth; x++) { const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; ctx->bmpRow[x] = adjustPixel(gray); } } else if (ctx->oneBit) { for (int x = 0; x < ctx->outWidth; x++) { const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; const uint8_t bit = ctx->atkinson1BitDitherer ? ctx->atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, ctx->currentOutY); ctx->bmpRow[x / 8] |= (bit << (7 - (x % 8))); } if (ctx->atkinson1BitDitherer) ctx->atkinson1BitDitherer->nextRow(); } else { for (int x = 0; x < ctx->outWidth; x++) { const uint8_t gray = adjustPixel((ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0); uint8_t twoBit; if (ctx->atkinsonDitherer) { twoBit = ctx->atkinsonDitherer->processPixel(gray, x); } else if (ctx->fsDitherer) { twoBit = ctx->fsDitherer->processPixel(gray, x); } else { twoBit = quantize(gray, x, ctx->currentOutY); } ctx->bmpRow[(x * 2) / 8] |= (twoBit << (6 - ((x * 2) % 8))); } if (ctx->atkinsonDitherer) ctx->atkinsonDitherer->nextRow(); else if (ctx->fsDitherer) ctx->fsDitherer->nextRow(); }
ctx->bmpOut->write(ctx->bmpRow.get(), ctx->bytesPerRow); ctx->currentOutY++; yieldDuringDecode(ctx);}
// 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// row), applies scaling + dithering and writes packed BMP rows to bmpOut.int bmpDrawCallback(JPEGDRAW* pDraw) { auto* ctx = reinterpret_cast<BmpConvertCtx*>(pDraw->pUser); if (!ctx || ctx->error) return 0; yieldDuringDecodeBlock(ctx);
const uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels); const int stride = pDraw->iWidth; const int validW = pDraw->iWidthUsed; const int blockH = pDraw->iHeight; const int blockX = pDraw->x; const int blockY = pDraw->y;
// Guard against unexpected callback geometry so we never index past row buffers. if (blockX < 0 || blockY < 0 || blockX >= ctx->srcWidth || blockY >= ctx->srcHeight) { LOG_ERR("JPG", "Unexpected JPEG block origin (%d,%d) for decode grid %dx%d", blockX, blockY, ctx->srcWidth, ctx->srcHeight); ctx->error = true; return 0; }
// Copy block pixels into MCU row buffer for (int r = 0; r < blockH && r < MAX_MCU_HEIGHT; r++) { const int copyW = (blockX + validW <= ctx->srcWidth) ? validW : (ctx->srcWidth - blockX); if (copyW <= 0) continue; memcpy(ctx->mcuBuf.get() + r * ctx->srcWidth + blockX, pixels + r * stride, copyW); }
// 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++) { const uint8_t* srcRow = ctx->mcuBuf.get() + (y - blockY) * ctx->srcWidth;
if (ctx->smoothUpscale) { processSmoothSourceRow(ctx, srcRow, y); } else if (!ctx->needsScaling) { // 1:1 — outWidth == srcWidth, write directly writeOutputRow(ctx, srcRow, y); } else { // Fixed-point area averaging on X axis for (int outX = 0; outX < ctx->outWidth; outX++) { const int srcXStart = (static_cast<uint32_t>(outX) * ctx->scaleX_fp) >> 16; const int srcXEnd = (static_cast<uint32_t>(outX + 1) * ctx->scaleX_fp) >> 16; int sum = 0; int count = 0; for (int srcX = srcXStart; srcX < srcXEnd && srcX < ctx->srcWidth; srcX++) { sum += srcRow[srcX]; count++; } if (count == 0 && srcXStart < ctx->srcWidth) { sum = srcRow[srcXStart]; count = 1; } ctx->rowAccum[outX] += sum; ctx->rowCount[outX] += count; }
// Flush output row(s) whose Y boundary we've crossed const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16; while (srcY_fp >= ctx->nextOutY_srcStart && ctx->currentOutY < ctx->outHeight) { flushScaledRow(ctx); ctx->nextOutY_srcStart = static_cast<uint32_t>(ctx->currentOutY + 1) * ctx->scaleY_fp; if (srcY_fp >= ctx->nextOutY_srcStart) continue; memset(ctx->rowAccum.get(), 0, ctx->outWidth * sizeof(uint32_t)); memset(ctx->rowCount.get(), 0, ctx->outWidth * sizeof(uint32_t)); } } }
return ctx->error ? 0 : 1;}
} // namespace
// Internal implementation with configurable target size and bit depthbool JpegToBmpConverter::jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit, bool crop, bool originalThresholds) { LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
if (ESP.getFreeHeap() < MIN_FREE_HEAP) { LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", ESP.getFreeHeap(), MIN_FREE_HEAP); return false; }
s_jpegFile = &jpegFile;
const auto jpeg = makeUniqueNoThrow<JPEGDEC>(); if (!jpeg) { LOG_ERR("JPG", "OOM: JPEG decoder"); return false; }
int rc = jpeg->open("", bmpJpegOpen, bmpJpegClose, bmpJpegRead, bmpJpegSeek, bmpDrawCallback); if (rc != 1) { LOG_ERR("JPG", "JPEG open failed (err=%d)", jpeg->getLastError()); return false; }
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
const int srcWidth = jpeg->getWidth(); const int srcHeight = jpeg->getHeight(); const bool progressiveDecode = (jpeg->getJPEGType() == JPEG_MODE_PROGRESSIVE); // JPEGDEC forces progressive streams to JPEG_SCALE_EIGHTH in DecodeJPEG, // so callback coordinates and MCU buffering must use the reduced decode grid. const int decodedSrcWidth = progressiveDecode ? ((srcWidth + 7) >> 3) : srcWidth; const int decodedSrcHeight = progressiveDecode ? ((srcHeight + 7) >> 3) : srcHeight;
LOG_DBG("JPG", "JPEG dimensions: %dx%d", srcWidth, srcHeight); if (progressiveDecode) { LOG_DBG("JPG", "Progressive JPEG decode uses 1/8 source: %dx%d", decodedSrcWidth, decodedSrcHeight); }
constexpr int MAX_IMAGE_WIDTH = 2048; constexpr int MAX_IMAGE_HEIGHT = 3072;
if (srcWidth <= 0 || srcHeight <= 0 || srcWidth > MAX_IMAGE_WIDTH || srcHeight > MAX_IMAGE_HEIGHT) { LOG_DBG("JPG", "Image too large or invalid (%dx%d), max supported: %dx%d", srcWidth, srcHeight, MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); return false; }
// Calculate output dimensions (pre-scale to fit display exactly) int outWidth = srcWidth; int outHeight = srcHeight; if (targetWidth <= 0 || targetHeight <= 0) { // Without an explicit target, keep decoder-native dimensions. outWidth = decodedSrcWidth; outHeight = decodedSrcHeight; }
const int scaleSrcWidth = decodedSrcWidth; const int scaleSrcHeight = decodedSrcHeight;
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; float scale = 1.0f; if (crop) { scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; } else { scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; }
outWidth = static_cast<int>(srcWidth * scale); outHeight = static_cast<int>(srcHeight * scale); if (outWidth < 1) outWidth = 1; if (outHeight < 1) outHeight = 1;
LOG_DBG("JPG", "Scaling source %dx%d (decode grid %dx%d) -> %dx%d (target %dx%d)", srcWidth, srcHeight, scaleSrcWidth, scaleSrcHeight, outWidth, outHeight, targetWidth, targetHeight); }
if (scaleSrcWidth != outWidth || scaleSrcHeight != outHeight) { scaleX_fp = (static_cast<uint32_t>(scaleSrcWidth) << 16) / outWidth; scaleY_fp = (static_cast<uint32_t>(scaleSrcHeight) << 16) / outHeight; needsScaling = true; }
const bool smoothUpscale = progressiveDecode && needsScaling && scaleSrcWidth <= outWidth && scaleSrcHeight <= outHeight;
// Write BMP header with output dimensions int bytesPerRow; if (USE_8BIT_OUTPUT && !oneBit) { writeBmpHeader8bit(bmpOut, outWidth, outHeight); bytesPerRow = (outWidth + 3) / 4 * 4; } else if (oneBit) { writeBmpHeader1bit(bmpOut, outWidth, outHeight); bytesPerRow = (outWidth + 31) / 32 * 4; } else { writeBmpHeader2bit(bmpOut, outWidth, outHeight); bytesPerRow = (outWidth * 2 + 31) / 32 * 4; }
BmpConvertCtx ctx = {}; ctx.bmpOut = &bmpOut; ctx.srcWidth = scaleSrcWidth; ctx.srcHeight = scaleSrcHeight; ctx.outWidth = outWidth; ctx.outHeight = outHeight; ctx.oneBit = oneBit; ctx.bytesPerRow = bytesPerRow; ctx.needsScaling = needsScaling; ctx.scaleX_fp = scaleX_fp; ctx.scaleY_fp = scaleY_fp; ctx.smoothUpscale = smoothUpscale; ctx.smoothScaleX_fp = interpolationStep(ctx.srcWidth, outWidth); ctx.smoothScaleY_fp = interpolationStep(ctx.srcHeight, outHeight); ctx.smoothNextOutY = 0; ctx.smoothPrevY = -1; ctx.rowsSinceYield = 0; ctx.blocksSinceYield = 0; ctx.error = false;
// MCU row buffer: MAX_MCU_HEIGHT rows × decoded srcWidth columns of grayscale ctx.mcuBuf = makeUniqueNoThrow<uint8_t[]>(MAX_MCU_HEIGHT * ctx.srcWidth); if (!ctx.mcuBuf) { LOG_ERR("JPG", "OOM: MCU buffer (%d bytes)", MAX_MCU_HEIGHT * ctx.srcWidth); return false; } memset(ctx.mcuBuf.get(), 0, MAX_MCU_HEIGHT * ctx.srcWidth);
ctx.bmpRow = makeUniqueNoThrow<uint8_t[]>(bytesPerRow); if (!ctx.bmpRow) { LOG_ERR("JPG", "OOM: BMP row buffer"); return false; }
if (smoothUpscale) { // One contiguous allocation avoids three heap blocks while keeping smoothing line-buffered. const size_t smoothRowsBytes = static_cast<size_t>(outWidth) * 3; ctx.smoothRows = makeUniqueNoThrow<uint8_t[]>(smoothRowsBytes); if (!ctx.smoothRows) { LOG_ERR("JPG", "OOM: progressive smoothing buffers"); return false; } ctx.smoothPrevRow = ctx.smoothRows.get(); ctx.smoothCurrRow = ctx.smoothPrevRow + outWidth; ctx.smoothOutRow = ctx.smoothCurrRow + outWidth; LOG_DBG("JPG", "Progressive smoothing: %dx%d -> %dx%d, buffers=%u bytes", ctx.srcWidth, ctx.srcHeight, outWidth, outHeight, static_cast<unsigned>(smoothRowsBytes)); } else if (needsScaling) { ctx.rowAccum = makeUniqueNoThrow<uint32_t[]>(outWidth); ctx.rowCount = makeUniqueNoThrow<uint32_t[]>(outWidth); if (!ctx.rowAccum || !ctx.rowCount) { LOG_ERR("JPG", "OOM: scaling buffers"); return false; } ctx.nextOutY_srcStart = scaleY_fp; }
if (oneBit) { ctx.atkinson1BitDitherer = makeUniqueNoThrow<Atkinson1BitDitherer>(outWidth); if (!ctx.atkinson1BitDitherer || !ctx.atkinson1BitDitherer->isValid()) { LOG_ERR("JPG", "OOM: Atkinson1BitDitherer"); return false; } } else if (!USE_8BIT_OUTPUT) { if (USE_ATKINSON) { ctx.atkinsonDitherer = makeUniqueNoThrow<AtkinsonDitherer>(outWidth, originalThresholds); if (!ctx.atkinsonDitherer || !ctx.atkinsonDitherer->isValid()) { LOG_ERR("JPG", "OOM: AtkinsonDitherer"); return false; } } else if (USE_FLOYD_STEINBERG) { ctx.fsDitherer = makeUniqueNoThrow<FloydSteinbergDitherer>(outWidth, originalThresholds); if (!ctx.fsDitherer || !ctx.fsDitherer->isValid()) { LOG_ERR("JPG", "OOM: FloydSteinbergDitherer"); return false; } } }
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); jpeg->setUserPointer(&ctx);
rc = jpeg->decode(0, 0, 0);
if (rc == 1 && ctx.smoothUpscale && !ctx.error) { finishSmoothUpscale(&ctx); }
if (rc != 1 || ctx.error) { LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError()); return false; }
LOG_DBG("JPG", "Successfully converted JPEG to BMP"); return true;}
// Core function: Convert JPEG file to 2-bit BMP (uses default target size)bool JpegToBmpConverter::jpegFileToBmpStream(HalFile& jpegFile, Print& bmpOut, bool crop, bool originalThresholds) { // Use runtime display dimensions (swapped for portrait cover sizing) const int targetWidth = display.getDisplayHeight(); const int targetHeight = display.getDisplayWidth(); return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetWidth, targetHeight, false, crop, originalThresholds);}
// Convert with custom target size (for thumbnails, 2-bit)bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(HalFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight) { return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, false);}
// Convert to 1-bit BMP (black and white only, no grays) for fast home screen renderingbool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(HalFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight) { return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true);}