Files
Crosspoint/lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
T
2026-05-02 18:19:23 +02:00

647 lines
22 KiB
C++

#include "JpegToFramebufferConverter.h"
#include <BitmapHelpers.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
#include <JPEGDEC.h>
#include <Logging.h>
#include <cstdlib>
#include <memory>
#include <new>
#include "DirectPixelWriter.h"
#include "DitherUtils.h"
#include "PixelCache.h"
namespace {
// Context struct passed through JPEGDEC callbacks to avoid global mutable state.
// The draw callback receives this via pDraw->pUser (set by setUserPointer()).
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by jpegOpen()).
struct JpegContext {
GfxRenderer* renderer{nullptr};
const RenderConfig* config{nullptr};
int screenWidth{0};
int screenHeight{0};
// Source dimensions after JPEGDEC's built-in scaling
int scaledSrcWidth{0};
int scaledSrcHeight{0};
// Final output dimensions
int dstWidth{0};
int dstHeight{0};
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU)
int32_t fineScaleFP{1 << 16}; // src -> dst mapping
int32_t invScaleFP{1 << 16}; // dst -> src mapping
PixelCache cache;
bool caching{false};
// See PngContext for the rationale: monochromeOutput requests a 1-bit Atkinson dither
// emitting only 0/3 so the BW DirectPixelWriter (`pixelValue < 3` rule) maps cleanly.
int oneBitDitherRow{-1};
std::unique_ptr<Atkinson1BitDitherer> atkinson1BitDitherer;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
int currentDitherRow{-1};
std::unique_ptr<AtkinsonDitherer> atkinsonDitherer;
std::unique_ptr<DiffusedBayerDitherer> diffusedBayerDitherer;
#endif
};
// Advance the 1-bit Atkinson ditherer to the requested destination row.
// Handles non-monotonic row walks (block-based JPEG decode) by reset+replay.
void prepareOneBitDitherRow(JpegContext& ctx, int dstY) {
if (!ctx.atkinson1BitDitherer) return;
if (ctx.oneBitDitherRow == -1 || dstY < ctx.oneBitDitherRow) {
ctx.atkinson1BitDitherer->reset();
ctx.oneBitDitherRow = dstY;
return;
}
while (ctx.oneBitDitherRow < dstY) {
ctx.atkinson1BitDitherer->nextRow();
ctx.oneBitDitherRow++;
}
}
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
void prepareDitherRow(JpegContext& ctx, int dstY) {
if (!ctx.config || !ctx.config->useDithering) return;
if (ctx.currentDitherRow == -1 || dstY < ctx.currentDitherRow) {
if (ctx.atkinsonDitherer) ctx.atkinsonDitherer->reset();
if (ctx.diffusedBayerDitherer) ctx.diffusedBayerDitherer->reset();
ctx.currentDitherRow = dstY;
return;
}
while (ctx.currentDitherRow < dstY) {
if (ctx.atkinsonDitherer) ctx.atkinsonDitherer->nextRow();
if (ctx.diffusedBayerDitherer) ctx.diffusedBayerDitherer->nextRow();
ctx.currentDitherRow++;
}
}
uint8_t ditherGray(JpegContext& ctx, uint8_t gray, int localX, int outX, int outY) {
if (ctx.atkinson1BitDitherer) {
return ctx.atkinson1BitDitherer->processPixel(gray, localX) ? 3 : 0;
}
if (!ctx.config || !ctx.config->useDithering) {
return quantizeGray4Level(gray);
}
switch (ctx.config->ditherMode) {
case ImageDitherMode::Atkinson:
if (ctx.atkinsonDitherer) {
return ctx.atkinsonDitherer->processPixel(gray, localX);
}
break;
case ImageDitherMode::DiffusedBayer:
if (ctx.diffusedBayerDitherer) {
return ctx.diffusedBayerDitherer->processPixel(gray, localX, outX, outY);
}
break;
case ImageDitherMode::Bayer:
case ImageDitherMode::COUNT:
default:
break;
}
return applyBayerDither4Level(gray, outX, outY);
}
#else
uint8_t ditherGray(JpegContext& ctx, uint8_t gray, int localX, int outX, int outY) {
if (ctx.atkinson1BitDitherer) {
return ctx.atkinson1BitDitherer->processPixel(gray, localX) ? 3 : 0;
}
(void)localX;
return applyBayerDither4Level(gray, outX, outY);
}
#endif
// File I/O callbacks use pFile->fHandle to access the FsFile*,
// avoiding the need for global file state.
void* jpegOpen(const char* filename, int32_t* size) {
FsFile* f =
new FsFile(); // NOLINT(cppcoreguidelines-owning-memory) — ownership transferred via void* to JPEGDEC callbacks
if (!Storage.openFileForRead("JPG", std::string(filename), *f)) {
delete f; // NOLINT(cppcoreguidelines-owning-memory)
return nullptr;
}
*size = f->size();
return f;
}
void jpegClose(void* handle) {
FsFile* f = reinterpret_cast<FsFile*>(handle);
if (f) {
f->close();
delete f; // NOLINT(cppcoreguidelines-owning-memory)
}
}
// JPEGDEC tracks file position via pFile->iPos internally (e.g. JPEGGetMoreData
// checks iPos < iSize to decide whether more data is available). The callbacks
// MUST maintain iPos to match the actual file position, otherwise progressive
// JPEGs with large headers fail during parsing.
int32_t jpegRead(JPEGFILE* pFile, uint8_t* pBuf, int32_t len) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
if (!f) return 0;
int32_t bytesRead = f->read(pBuf, len);
if (bytesRead < 0) return 0;
pFile->iPos += bytesRead;
return bytesRead;
}
int32_t jpegSeek(JPEGFILE* pFile, int32_t pos) {
FsFile* f = reinterpret_cast<FsFile*>(pFile->fHandle);
if (!f) return -1;
if (!f->seek(pos)) return -1;
pFile->iPos = pos;
return pos;
}
// JPEGDEC object is ~17 KB due to internal decode buffers.
// Heap-allocate on demand so memory is only used during active decode.
constexpr size_t JPEG_DECODER_APPROX_SIZE = 20 * 1024;
constexpr size_t MIN_FREE_HEAP_FOR_JPEG = JPEG_DECODER_APPROX_SIZE + 16 * 1024;
bool readJpegDimensionsFromHeader(const std::string& imagePath, ImageDimensions& out) {
FsFile f;
if (!Storage.openFileForRead("JPG", imagePath, f)) {
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
return false;
}
auto readByte = [&f](uint8_t& b) -> bool { return f.read(&b, 1) == 1; };
auto readU16BE = [&f](uint16_t& v) -> bool {
uint8_t b[2];
if (f.read(b, 2) != 2) return false;
v = static_cast<uint16_t>((static_cast<uint16_t>(b[0]) << 8) | b[1]);
return true;
};
uint8_t b0 = 0;
uint8_t b1 = 0;
if (!readByte(b0) || !readByte(b1) || b0 != 0xFF || b1 != 0xD8) {
f.close();
LOG_ERR("JPG", "Not a JPEG file: %s", imagePath.c_str());
return false;
}
while (f.available()) {
uint8_t prefix = 0;
if (!readByte(prefix)) break;
if (prefix != 0xFF) continue;
uint8_t marker = 0;
do {
if (!readByte(marker)) {
f.close();
return false;
}
} while (marker == 0xFF);
if (marker == 0x00 || marker == 0xD8 || marker == 0xD9 || (marker >= 0xD0 && marker <= 0xD7)) {
continue;
}
uint16_t segLen = 0;
if (!readU16BE(segLen) || segLen < 2) {
f.close();
return false;
}
const bool isSof = (marker >= 0xC0 && marker <= 0xC3) || (marker >= 0xC5 && marker <= 0xC7) ||
(marker >= 0xC9 && marker <= 0xCB) || (marker >= 0xCD && marker <= 0xCF);
if (isSof) {
uint8_t sof[5];
if (segLen < 7 || f.read(sof, sizeof(sof)) != static_cast<int>(sizeof(sof))) {
f.close();
return false;
}
uint16_t height = static_cast<uint16_t>((static_cast<uint16_t>(sof[1]) << 8) | sof[2]);
uint16_t width = static_cast<uint16_t>((static_cast<uint16_t>(sof[3]) << 8) | sof[4]);
f.close();
if (width == 0 || height == 0) {
LOG_ERR("JPG", "Invalid JPEG dimensions %ux%u: %s", width, height, imagePath.c_str());
return false;
}
out.width = static_cast<int16_t>(width);
out.height = static_cast<int16_t>(height);
return true;
}
const int32_t skip = static_cast<int32_t>(segLen) - 2;
if (!f.seek(f.position() + skip)) {
f.close();
return false;
}
}
f.close();
LOG_ERR("JPG", "No SOF marker found for dimensions: %s", imagePath.c_str());
return false;
}
// Choose JPEGDEC's built-in scale factor for coarse downscaling.
// Returns the scale denominator (1, 2, 4, or 8) and sets jpegScaleOption.
int chooseJpegScale(float targetScale, int& jpegScaleOption) {
if (targetScale <= 0.125f) {
jpegScaleOption = JPEG_SCALE_EIGHTH;
return 8;
}
if (targetScale <= 0.25f) {
jpegScaleOption = JPEG_SCALE_QUARTER;
return 4;
}
if (targetScale <= 0.5f) {
jpegScaleOption = JPEG_SCALE_HALF;
return 2;
}
jpegScaleOption = 0;
return 1;
}
// Fixed-point 16.16 arithmetic avoids software float emulation on ESP32-C3 (no FPU).
constexpr int FP_SHIFT = 16;
constexpr int32_t FP_ONE = 1 << FP_SHIFT;
constexpr int32_t FP_MASK = FP_ONE - 1;
int jpegDrawCallback(JPEGDRAW* pDraw) {
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
if (!ctx || !ctx->config || !ctx->renderer) return 0;
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
const int stride = pDraw->iWidth;
const int validW = pDraw->iWidthUsed;
const int blockH = pDraw->iHeight;
if (stride <= 0 || blockH <= 0 || validW <= 0) return 1;
const bool caching = ctx->caching;
const int32_t fineScaleFP = ctx->fineScaleFP;
const int32_t invScaleFP = ctx->invScaleFP;
GfxRenderer& renderer = *ctx->renderer;
const int cfgX = ctx->config->x;
const int cfgY = ctx->config->y;
const int blockX = pDraw->x;
const int blockY = pDraw->y;
// Determine destination pixel range covered by this source block
const int srcYEnd = blockY + blockH;
const int srcXEnd = blockX + validW;
int dstYStart = (int)((int64_t)blockY * fineScaleFP >> FP_SHIFT);
int dstYEnd = (srcYEnd >= ctx->scaledSrcHeight) ? ctx->dstHeight : (int)((int64_t)srcYEnd * fineScaleFP >> FP_SHIFT);
int dstXStart = (int)((int64_t)blockX * fineScaleFP >> FP_SHIFT);
int dstXEnd = (srcXEnd >= ctx->scaledSrcWidth) ? ctx->dstWidth : (int)((int64_t)srcXEnd * fineScaleFP >> FP_SHIFT);
// Pre-clamp destination ranges to screen bounds (eliminates per-pixel screen checks)
int clampYMax = ctx->dstHeight;
if (ctx->screenHeight - cfgY < clampYMax) clampYMax = ctx->screenHeight - cfgY;
if (dstYStart < -cfgY) dstYStart = -cfgY;
if (dstYEnd > clampYMax) dstYEnd = clampYMax;
int clampXMax = ctx->dstWidth;
if (ctx->screenWidth - cfgX < clampXMax) clampXMax = ctx->screenWidth - cfgX;
if (dstXStart < -cfgX) dstXStart = -cfgX;
if (dstXEnd > clampXMax) dstXEnd = clampXMax;
if (dstYStart >= dstYEnd || dstXStart >= dstXEnd) return 1;
// Pre-compute orientation and render-mode state once per callback invocation
DirectPixelWriter pw;
pw.init(renderer);
DirectCacheWriter cw;
if (caching) {
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
}
// === 1:1 fast path: no scaling math ===
if (fineScaleFP == FP_ONE) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
prepareOneBitDitherRow(*ctx, dstY);
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY);
#endif
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const uint8_t* row = &pixels[(dstY - blockY) * stride];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
uint8_t gray = row[dstX - blockX];
uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
return 1;
}
// === Bilinear interpolation (upscale: fineScale > 1.0) ===
// Smooths block boundaries that would otherwise create visible banding
// on progressive JPEG DC-only decode (1/8 resolution upscaled to target).
if (fineScaleFP > FP_ONE) {
// Pre-compute safe X range where lx0 and lx0+1 are both in [0, validW-1].
// Only the left/right edge pixels (typically 0-2 and 1-8 respectively) need clamping.
int safeXStart = (int)(((int64_t)blockX * fineScaleFP + FP_MASK) >> FP_SHIFT);
int safeXEnd = (int)((int64_t)(blockX + validW - 1) * fineScaleFP >> FP_SHIFT);
if (safeXStart < dstXStart) safeXStart = dstXStart;
if (safeXEnd > dstXEnd) safeXEnd = dstXEnd;
if (safeXStart > safeXEnd) safeXEnd = safeXStart;
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
prepareOneBitDitherRow(*ctx, dstY);
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY);
#endif
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
const int32_t fy = srcFyFP & FP_MASK;
const int32_t fyInv = FP_ONE - fy;
int ly0 = (srcFyFP >> FP_SHIFT) - blockY;
int ly1 = ly0 + 1;
if (ly0 < 0) ly0 = 0;
if (ly0 >= blockH) ly0 = blockH - 1;
if (ly1 >= blockH) ly1 = blockH - 1;
const uint8_t* row0 = &pixels[ly0 * stride];
const uint8_t* row1 = &pixels[ly1 * stride];
// Left edge (with X boundary clamping)
for (int dstX = dstXStart; dstX < safeXStart; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
int lx1 = lx0 + 1;
if (lx0 < 0) lx0 = 0;
if (lx1 < 0) lx1 = 0;
if (lx0 >= validW) lx0 = validW - 1;
if (lx1 >= validW) lx1 = validW - 1;
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
for (int dstX = safeXStart; dstX < safeXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
const int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
int top = ((int)row0[lx0] * fxInv + (int)row0[lx0 + 1] * fx) >> FP_SHIFT;
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx0 + 1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
// Right edge (with X boundary clamping)
for (int dstX = safeXEnd; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
int lx1 = lx0 + 1;
if (lx0 >= validW) lx0 = validW - 1;
if (lx1 >= validW) lx1 = validW - 1;
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
return 1;
}
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
prepareOneBitDitherRow(*ctx, dstY);
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY);
#endif
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
int ly = (srcFyFP >> FP_SHIFT) - blockY;
if (ly < 0) ly = 0;
if (ly >= blockH) ly = blockH - 1;
const uint8_t* row = &pixels[ly * stride];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
int lx = (srcFxFP >> FP_SHIFT) - blockX;
if (lx < 0) lx = 0;
if (lx >= validW) lx = validW - 1;
uint8_t gray = row[lx];
uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
return 1;
}
} // namespace
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
if (!readJpegDimensionsFromHeader(imagePath, out)) {
return false;
}
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
return true;
}
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
const RenderConfig& config) {
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
size_t freeHeap = ESP.getFreeHeap();
if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
return false;
}
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
if (!jpeg) {
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
return false;
}
JpegContext ctx;
ctx.renderer = &renderer;
ctx.config = &config;
ctx.screenWidth = renderer.getScreenWidth();
ctx.screenHeight = renderer.getScreenHeight();
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
if (rc != 1) {
LOG_ERR("JPG", "Failed to open JPEG (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
return false;
}
int srcWidth = jpeg->getWidth();
int srcHeight = jpeg->getHeight();
if (srcWidth <= 0 || srcHeight <= 0) {
LOG_ERR("JPG", "Invalid JPEG dimensions: %dx%d", srcWidth, srcHeight);
jpeg->close();
return false;
}
if (!validateImageDimensions(srcWidth, srcHeight, "JPEG")) {
jpeg->close();
return false;
}
bool isProgressive = jpeg->getJPEGType() == JPEG_MODE_PROGRESSIVE;
if (isProgressive) {
LOG_INF("JPG", "Progressive JPEG detected - decoding DC coefficients only (lower quality)");
}
// Calculate overall target scale
float targetScale;
int destWidth, destHeight;
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
destWidth = config.maxWidth;
destHeight = config.maxHeight;
targetScale = (float)destWidth / srcWidth;
} else {
float scaleX = (config.maxWidth > 0 && srcWidth > config.maxWidth) ? (float)config.maxWidth / srcWidth : 1.0f;
float scaleY = (config.maxHeight > 0 && srcHeight > config.maxHeight) ? (float)config.maxHeight / srcHeight : 1.0f;
targetScale = (scaleX < scaleY) ? scaleX : scaleY;
if (targetScale > 1.0f) targetScale = 1.0f;
destWidth = (int)(srcWidth * targetScale);
destHeight = (int)(srcHeight * targetScale);
}
// Choose JPEGDEC built-in scaling for coarse downscaling.
// Progressive JPEGs: JPEGDEC forces JPEG_SCALE_EIGHTH internally (DC-only
// decode produces 1/8 resolution). We must match this to avoid the if/else
// priority chain in DecodeJPEG selecting a different scale.
int jpegScaleOption;
int jpegScaleDenom;
if (isProgressive) {
jpegScaleOption = JPEG_SCALE_EIGHTH;
jpegScaleDenom = 8;
} else {
jpegScaleDenom = chooseJpegScale(targetScale, jpegScaleOption);
}
ctx.scaledSrcWidth = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
ctx.scaledSrcHeight = (srcHeight + jpegScaleDenom - 1) / jpegScaleDenom;
ctx.dstWidth = destWidth;
ctx.dstHeight = destHeight;
ctx.fineScaleFP = (int32_t)((int64_t)destWidth * FP_ONE / ctx.scaledSrcWidth);
ctx.invScaleFP = (int32_t)((int64_t)ctx.scaledSrcWidth * FP_ONE / destWidth);
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f, jpegScale 1/%d, fineScale %.2f)%s", srcWidth, srcHeight, destWidth,
destHeight, targetScale, jpegScaleDenom, (float)destWidth / ctx.scaledSrcWidth,
isProgressive ? " [progressive]" : "");
// Set pixel type to 8-bit grayscale (must be after open())
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
jpeg->setUserPointer(&ctx);
// Allocate cache buffer using final output dimensions
ctx.caching = !config.cachePath.empty();
if (ctx.caching) {
if (!ctx.cache.allocate(destWidth, destHeight, config.x, config.y)) {
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
ctx.caching = false;
}
}
// See PngToFramebufferConverter for rationale: BW-only display needs a 1-bit
// dither so mid-grays don't collapse to black under DirectPixelWriter's `< 3` rule.
if (config.monochromeOutput) {
ctx.atkinson1BitDitherer.reset(new (std::nothrow) Atkinson1BitDitherer(destWidth));
if (!ctx.atkinson1BitDitherer) {
LOG_ERR("JPG", "Failed to allocate 1-bit Atkinson ditherer, falling back to 4-level dither");
}
}
if (config.useDithering && !ctx.atkinson1BitDitherer) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
switch (config.ditherMode) {
case ImageDitherMode::Atkinson:
ctx.atkinsonDitherer.reset(new (std::nothrow) AtkinsonDitherer(destWidth));
if (!ctx.atkinsonDitherer) {
LOG_ERR("JPG", "Failed to allocate Atkinson ditherer, falling back to Bayer");
}
break;
case ImageDitherMode::DiffusedBayer:
ctx.diffusedBayerDitherer.reset(new (std::nothrow) DiffusedBayerDitherer(destWidth));
if (!ctx.diffusedBayerDitherer) {
LOG_ERR("JPG", "Failed to allocate diffused Bayer ditherer, falling back to Bayer");
}
break;
case ImageDitherMode::Bayer:
case ImageDitherMode::COUNT:
default:
break;
}
#endif
}
unsigned long decodeStart = millis();
rc = jpeg->decode(0, 0, jpegScaleOption);
unsigned long decodeTime = millis() - decodeStart;
if (rc != 1) {
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
jpeg->close();
return false;
}
jpeg->close();
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
// Write cache file if caching was enabled
if (ctx.caching) {
ctx.cache.writeToFile(config.cachePath);
}
return true;
}
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
return FsHelpers::hasJpgExtension(extension);
}