feat: replace picojpeg with JPEGDEC for JPEG image decoding (#1136)
## Summary Replaces the picojpeg library with bitbank2/JPEGDEC for JPEG decoding in the EPUB image pipeline. JPEGDEC provides built-in coarse scaling (1/2, 1/4, 1/8), 8-bit grayscale output, and streaming block-based decoding via callbacks. Includes a pre-build patch script for two JPEGDEC changes affecting progressive JPEG support and EIGHT_BIT_GRAYSCALE mode. Closes #912 ## Additional Context # Example progressive jpeg <img src="https://github.com/user-attachments/assets/e63bb4f8-f862-4aa0-a01f-d1ef43a4b27a" width="400" height="800" /> Good performance increase from JPEGDEC over picojpeg cc @bitbank2 thanks ## Baseline JPEG Decode Performance: picojpeg vs JPEGDEC (float in callback) vs JPEGDEC (fixed-point in callback) Tested with `test_jpeg_images.epub` on device (ESP32-C3), first decode (no cache). | Image | Source | Output | picojpeg | JPEGDEC float | JPEGDEC fixed-point | vs picojpeg | vs float | |-------|--------|--------|----------|---------------|---------------------|-------------|----------| | jpeg_format.jpg | 350x250 | 350x250 | 313 ms | 256 ms | **104 ms** | **3.0x** | **2.5x** | | grayscale_test.jpg | 400x600 | 400x600 | 768 ms | 661 ms | **246 ms** | **3.1x** | **2.7x** | | gradient_test.jpg | 400x500 | 400x500 | 707 ms | 597 ms | **247 ms** | **2.9x** | **2.4x** | | centering_test.jpg | 350x400 | 350x400 | 502 ms | 412 ms | **169 ms** | **3.0x** | **2.4x** | | scaling_test.jpg | 1200x1500 | 464x580 | 5487 ms | 1114 ms | **668 ms** | **8.2x** | **1.7x** | | wide_scaling_test.jpg | 1807x736 | 464x188 | 4237 ms | 642 ms | **497 ms** | **8.5x** | **1.3x** | | cache_test_1.jpg | 400x300 | 400x300 | 422 ms | 348 ms | **141 ms** | **3.0x** | **2.5x** | | cache_test_2.jpg | 400x300 | 400x300 | 424 ms | 349 ms | **142 ms** | **3.0x** | **2.5x** | ### Summary - **1:1 scale (fixed-point vs float)**: ~2.5x faster — eliminating software float on the FPU-less ESP32-C3 is the dominant win - **1:1 scale (fixed-point vs picojpeg)**: ~3.0x faster overall - **Downscaled images (vs picojpeg)**: 8-9x faster — JPEGDEC's coarse scaling + fixed-point draw callback - **Downscaled images (fixed-point vs float)**: 1.3-1.7x — less dramatic since JPEG library decode dominates over the draw callback for fewer output pixels - The fixed-point optimization alone (vs float JPEGDEC) saved **~60% of render time** on 1:1 images, confirming that software float emulation was the primary bottleneck in the draw callback - See thread for discussions on quality of progressive images, https://github.com/crosspoint-reader/crosspoint-reader/pull/1136#issuecomment-3952952315 - and the conclusion https://github.com/crosspoint-reader/crosspoint-reader/pull/1136#issuecomment-3959379386 - Proposal to improve quality added at https://github.com/crosspoint-reader/crosspoint-reader/discussions/1179 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY >**_ --------- Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
@@ -2,44 +2,358 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
#include <JPEGDEC.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <picojpeg.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <new>
|
||||||
|
|
||||||
#include "DitherUtils.h"
|
#include "DitherUtils.h"
|
||||||
#include "PixelCache.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 {
|
struct JpegContext {
|
||||||
FsFile& file;
|
GfxRenderer* renderer;
|
||||||
uint8_t buffer[512];
|
const RenderConfig* config;
|
||||||
size_t bufferPos;
|
int screenWidth;
|
||||||
size_t bufferFilled;
|
int screenHeight;
|
||||||
JpegContext(FsFile& f) : file(f), bufferPos(0), bufferFilled(0) {}
|
|
||||||
|
// Source dimensions after JPEGDEC's built-in scaling
|
||||||
|
int scaledSrcWidth;
|
||||||
|
int scaledSrcHeight;
|
||||||
|
|
||||||
|
// Final output dimensions
|
||||||
|
int dstWidth;
|
||||||
|
int dstHeight;
|
||||||
|
|
||||||
|
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU)
|
||||||
|
int32_t fineScaleFP; // src -> dst mapping
|
||||||
|
int32_t invScaleFP; // dst -> src mapping
|
||||||
|
|
||||||
|
PixelCache cache;
|
||||||
|
bool caching;
|
||||||
|
|
||||||
|
JpegContext()
|
||||||
|
: renderer(nullptr),
|
||||||
|
config(nullptr),
|
||||||
|
screenWidth(0),
|
||||||
|
screenHeight(0),
|
||||||
|
scaledSrcWidth(0),
|
||||||
|
scaledSrcHeight(0),
|
||||||
|
dstWidth(0),
|
||||||
|
dstHeight(0),
|
||||||
|
fineScaleFP(1 << 16),
|
||||||
|
invScaleFP(1 << 16),
|
||||||
|
caching(false) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
if (!Storage.openFileForRead("JPG", std::string(filename), *f)) {
|
||||||
|
delete f;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
*size = f->size();
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void jpegClose(void* handle) {
|
||||||
|
FsFile* f = reinterpret_cast<FsFile*>(handle);
|
||||||
|
if (f) {
|
||||||
|
f->close();
|
||||||
|
delete f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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 useDithering = ctx->config->useDithering;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// === 1:1 fast path: no scaling math ===
|
||||||
|
if (fineScaleFP == FP_ONE) {
|
||||||
|
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||||
|
const int outY = cfgY + dstY;
|
||||||
|
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;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||||
|
if (caching) ctx->cache.setPixel(outX, outY, 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;
|
||||||
|
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;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||||
|
if (caching) ctx->cache.setPixel(outX, outY, 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;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||||
|
if (caching) ctx->cache.setPixel(outX, outY, 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;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||||
|
if (caching) ctx->cache.setPixel(outX, outY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
|
||||||
|
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||||
|
const int outY = cfgY + dstY;
|
||||||
|
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;
|
||||||
|
if (useDithering) {
|
||||||
|
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||||
|
} else {
|
||||||
|
dithered = gray / 85;
|
||||||
|
if (dithered > 3) dithered = 3;
|
||||||
|
}
|
||||||
|
drawPixelWithRenderMode(renderer, outX, outY, dithered);
|
||||||
|
if (caching) ctx->cache.setPixel(outX, outY, dithered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||||
FsFile file;
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
|
||||||
LOG_ERR("JPG", "Failed to open file for dimensions: %s", imagePath.c_str());
|
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
JpegContext context(file);
|
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
|
||||||
pjpeg_image_info_t imageInfo;
|
if (!jpeg) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate JPEG decoder for dimensions");
|
||||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
if (status != 0) {
|
|
||||||
LOG_ERR("JPG", "Failed to init JPEG for dimensions: %d", status);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
out.width = imageInfo.m_width;
|
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, nullptr);
|
||||||
out.height = imageInfo.m_height;
|
if (rc != 1) {
|
||||||
|
LOG_ERR("JPG", "Failed to open JPEG for dimensions (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
|
||||||
|
delete jpeg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.width = jpeg->getWidth();
|
||||||
|
out.height = jpeg->getHeight();
|
||||||
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
|
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
|
||||||
|
|
||||||
|
jpeg->close();
|
||||||
|
delete jpeg;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,246 +361,130 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
|||||||
const RenderConfig& config) {
|
const RenderConfig& config) {
|
||||||
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
||||||
|
|
||||||
FsFile file;
|
size_t freeHeap = ESP.getFreeHeap();
|
||||||
if (!Storage.openFileForRead("JPG", imagePath, file)) {
|
if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
|
||||||
LOG_ERR("JPG", "Failed to open file: %s", imagePath.c_str());
|
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
JpegContext context(file);
|
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
|
||||||
pjpeg_image_info_t imageInfo;
|
if (!jpeg) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
|
||||||
int status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
|
||||||
if (status != 0) {
|
|
||||||
LOG_ERR("JPG", "picojpeg init failed: %d", status);
|
|
||||||
file.close();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validateImageDimensions(imageInfo.m_width, imageInfo.m_height, "JPEG")) {
|
JpegContext ctx;
|
||||||
file.close();
|
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());
|
||||||
|
delete jpeg;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate output dimensions
|
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();
|
||||||
|
delete jpeg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validateImageDimensions(srcWidth, srcHeight, "JPEG")) {
|
||||||
|
jpeg->close();
|
||||||
|
delete jpeg;
|
||||||
|
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;
|
int destWidth, destHeight;
|
||||||
float scale;
|
|
||||||
|
|
||||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||||
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
|
||||||
destWidth = config.maxWidth;
|
destWidth = config.maxWidth;
|
||||||
destHeight = config.maxHeight;
|
destHeight = config.maxHeight;
|
||||||
scale = (float)destWidth / imageInfo.m_width;
|
targetScale = (float)destWidth / srcWidth;
|
||||||
} else {
|
} else {
|
||||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
float scaleX = (config.maxWidth > 0 && srcWidth > config.maxWidth) ? (float)config.maxWidth / srcWidth : 1.0f;
|
||||||
float scaleX = (config.maxWidth > 0 && imageInfo.m_width > config.maxWidth)
|
float scaleY = (config.maxHeight > 0 && srcHeight > config.maxHeight) ? (float)config.maxHeight / srcHeight : 1.0f;
|
||||||
? (float)config.maxWidth / imageInfo.m_width
|
targetScale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
: 1.0f;
|
if (targetScale > 1.0f) targetScale = 1.0f;
|
||||||
float scaleY = (config.maxHeight > 0 && imageInfo.m_height > config.maxHeight)
|
|
||||||
? (float)config.maxHeight / imageInfo.m_height
|
|
||||||
: 1.0f;
|
|
||||||
scale = (scaleX < scaleY) ? scaleX : scaleY;
|
|
||||||
if (scale > 1.0f) scale = 1.0f;
|
|
||||||
|
|
||||||
destWidth = (int)(imageInfo.m_width * scale);
|
destWidth = (int)(srcWidth * targetScale);
|
||||||
destHeight = (int)(imageInfo.m_height * scale);
|
destHeight = (int)(srcHeight * targetScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f), scan type: %d, MCU: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
// Choose JPEGDEC built-in scaling for coarse downscaling.
|
||||||
destWidth, destHeight, scale, imageInfo.m_scanType, imageInfo.m_MCUWidth, imageInfo.m_MCUHeight);
|
// Progressive JPEGs: JPEGDEC forces JPEG_SCALE_EIGHTH internally (DC-only
|
||||||
|
// decode produces 1/8 resolution). We must match this to avoid the if/else
|
||||||
if (!imageInfo.m_pMCUBufR || !imageInfo.m_pMCUBufG || !imageInfo.m_pMCUBufB) {
|
// priority chain in DecodeJPEG selecting a different scale.
|
||||||
LOG_ERR("JPG", "Null buffer pointers in imageInfo");
|
int jpegScaleOption;
|
||||||
file.close();
|
int jpegScaleDenom;
|
||||||
return false;
|
if (isProgressive) {
|
||||||
|
jpegScaleOption = JPEG_SCALE_EIGHTH;
|
||||||
|
jpegScaleDenom = 8;
|
||||||
|
} else {
|
||||||
|
jpegScaleDenom = chooseJpegScale(targetScale, jpegScaleOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int screenWidth = renderer.getScreenWidth();
|
ctx.scaledSrcWidth = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
|
||||||
const int screenHeight = renderer.getScreenHeight();
|
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);
|
||||||
|
|
||||||
// Allocate pixel cache if cachePath is provided
|
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f, jpegScale 1/%d, fineScale %.2f)%s", srcWidth, srcHeight, destWidth,
|
||||||
PixelCache cache;
|
destHeight, targetScale, jpegScaleDenom, (float)destWidth / ctx.scaledSrcWidth,
|
||||||
bool caching = !config.cachePath.empty();
|
isProgressive ? " [progressive]" : "");
|
||||||
if (caching) {
|
|
||||||
if (!cache.allocate(destWidth, destHeight, config.x, config.y)) {
|
// 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");
|
LOG_ERR("JPG", "Failed to allocate cache buffer, continuing without caching");
|
||||||
caching = false;
|
ctx.caching = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int mcuX = 0;
|
unsigned long decodeStart = millis();
|
||||||
int mcuY = 0;
|
rc = jpeg->decode(0, 0, jpegScaleOption);
|
||||||
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
while (mcuY < imageInfo.m_MCUSPerCol) {
|
if (rc != 1) {
|
||||||
status = pjpeg_decode_mcu();
|
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
|
||||||
if (status == PJPG_NO_MORE_BLOCKS) {
|
jpeg->close();
|
||||||
break;
|
delete jpeg;
|
||||||
}
|
|
||||||
if (status != 0) {
|
|
||||||
LOG_ERR("JPG", "MCU decode failed: %d", status);
|
|
||||||
file.close();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Source position in image coordinates
|
jpeg->close();
|
||||||
int srcStartX = mcuX * imageInfo.m_MCUWidth;
|
delete jpeg;
|
||||||
int srcStartY = mcuY * imageInfo.m_MCUHeight;
|
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
|
||||||
|
|
||||||
switch (imageInfo.m_scanType) {
|
|
||||||
case PJPG_GRAYSCALE:
|
|
||||||
for (int row = 0; row < 8; row++) {
|
|
||||||
int srcY = srcStartY + row;
|
|
||||||
int destY = config.y + (int)(srcY * scale);
|
|
||||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
|
||||||
for (int col = 0; col < 8; col++) {
|
|
||||||
int srcX = srcStartX + col;
|
|
||||||
int destX = config.x + (int)(srcX * scale);
|
|
||||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
|
||||||
uint8_t gray = imageInfo.m_pMCUBufR[row * 8 + col];
|
|
||||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
|
||||||
if (dithered > 3) dithered = 3;
|
|
||||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
|
||||||
if (caching) cache.setPixel(destX, destY, dithered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PJPG_YH1V1:
|
|
||||||
for (int row = 0; row < 8; row++) {
|
|
||||||
int srcY = srcStartY + row;
|
|
||||||
int destY = config.y + (int)(srcY * scale);
|
|
||||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
|
||||||
for (int col = 0; col < 8; col++) {
|
|
||||||
int srcX = srcStartX + col;
|
|
||||||
int destX = config.x + (int)(srcX * scale);
|
|
||||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
|
||||||
uint8_t r = imageInfo.m_pMCUBufR[row * 8 + col];
|
|
||||||
uint8_t g = imageInfo.m_pMCUBufG[row * 8 + col];
|
|
||||||
uint8_t b = imageInfo.m_pMCUBufB[row * 8 + col];
|
|
||||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
|
||||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
|
||||||
if (dithered > 3) dithered = 3;
|
|
||||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
|
||||||
if (caching) cache.setPixel(destX, destY, dithered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PJPG_YH2V1:
|
|
||||||
for (int row = 0; row < 8; row++) {
|
|
||||||
int srcY = srcStartY + row;
|
|
||||||
int destY = config.y + (int)(srcY * scale);
|
|
||||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
|
||||||
for (int col = 0; col < 16; col++) {
|
|
||||||
int srcX = srcStartX + col;
|
|
||||||
int destX = config.x + (int)(srcX * scale);
|
|
||||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
|
||||||
int blockIndex = (col < 8) ? 0 : 1;
|
|
||||||
int pixelIndex = row * 8 + (col % 8);
|
|
||||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 64 + pixelIndex];
|
|
||||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 64 + pixelIndex];
|
|
||||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 64 + pixelIndex];
|
|
||||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
|
||||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
|
||||||
if (dithered > 3) dithered = 3;
|
|
||||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
|
||||||
if (caching) cache.setPixel(destX, destY, dithered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PJPG_YH1V2:
|
|
||||||
for (int row = 0; row < 16; row++) {
|
|
||||||
int srcY = srcStartY + row;
|
|
||||||
int destY = config.y + (int)(srcY * scale);
|
|
||||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
|
||||||
for (int col = 0; col < 8; col++) {
|
|
||||||
int srcX = srcStartX + col;
|
|
||||||
int destX = config.x + (int)(srcX * scale);
|
|
||||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
|
||||||
int blockIndex = (row < 8) ? 0 : 1;
|
|
||||||
int pixelIndex = (row % 8) * 8 + col;
|
|
||||||
uint8_t r = imageInfo.m_pMCUBufR[blockIndex * 128 + pixelIndex];
|
|
||||||
uint8_t g = imageInfo.m_pMCUBufG[blockIndex * 128 + pixelIndex];
|
|
||||||
uint8_t b = imageInfo.m_pMCUBufB[blockIndex * 128 + pixelIndex];
|
|
||||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
|
||||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
|
||||||
if (dithered > 3) dithered = 3;
|
|
||||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
|
||||||
if (caching) cache.setPixel(destX, destY, dithered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case PJPG_YH2V2:
|
|
||||||
for (int row = 0; row < 16; row++) {
|
|
||||||
int srcY = srcStartY + row;
|
|
||||||
int destY = config.y + (int)(srcY * scale);
|
|
||||||
if (destY >= screenHeight || destY >= config.y + destHeight) continue;
|
|
||||||
for (int col = 0; col < 16; col++) {
|
|
||||||
int srcX = srcStartX + col;
|
|
||||||
int destX = config.x + (int)(srcX * scale);
|
|
||||||
if (destX >= screenWidth || destX >= config.x + destWidth) continue;
|
|
||||||
int blockX = (col < 8) ? 0 : 1;
|
|
||||||
int blockY = (row < 8) ? 0 : 1;
|
|
||||||
int blockIndex = blockY * 2 + blockX;
|
|
||||||
int pixelIndex = (row % 8) * 8 + (col % 8);
|
|
||||||
int blockOffset = blockIndex * 64;
|
|
||||||
uint8_t r = imageInfo.m_pMCUBufR[blockOffset + pixelIndex];
|
|
||||||
uint8_t g = imageInfo.m_pMCUBufG[blockOffset + pixelIndex];
|
|
||||||
uint8_t b = imageInfo.m_pMCUBufB[blockOffset + pixelIndex];
|
|
||||||
uint8_t gray = (uint8_t)((r * 77 + g * 150 + b * 29) >> 8);
|
|
||||||
uint8_t dithered = config.useDithering ? applyBayerDither4Level(gray, destX, destY) : gray / 85;
|
|
||||||
if (dithered > 3) dithered = 3;
|
|
||||||
drawPixelWithRenderMode(renderer, destX, destY, dithered);
|
|
||||||
if (caching) cache.setPixel(destX, destY, dithered);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
mcuX++;
|
|
||||||
if (mcuX >= imageInfo.m_MCUSPerRow) {
|
|
||||||
mcuX = 0;
|
|
||||||
mcuY++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DBG("JPG", "Decoding complete");
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
// Write cache file if caching was enabled
|
// Write cache file if caching was enabled
|
||||||
if (caching) {
|
if (ctx.caching) {
|
||||||
cache.writeToFile(config.cachePath);
|
ctx.cache.writeToFile(config.cachePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned char JpegToFramebufferConverter::jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
|
||||||
unsigned char* pBytes_actually_read, void* pCallback_data) {
|
|
||||||
JpegContext* context = reinterpret_cast<JpegContext*>(pCallback_data);
|
|
||||||
|
|
||||||
if (context->bufferPos >= context->bufferFilled) {
|
|
||||||
int readCount = context->file.read(context->buffer, sizeof(context->buffer));
|
|
||||||
if (readCount <= 0) {
|
|
||||||
*pBytes_actually_read = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
context->bufferFilled = readCount;
|
|
||||||
context->bufferPos = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned int bytesAvailable = context->bufferFilled - context->bufferPos;
|
|
||||||
unsigned int bytesToCopy = (bytesAvailable < buf_size) ? bytesAvailable : buf_size;
|
|
||||||
|
|
||||||
memcpy(pBuf, &context->buffer[context->bufferPos], bytesToCopy);
|
|
||||||
context->bufferPos += bytesToCopy;
|
|
||||||
*pBytes_actually_read = bytesToCopy;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||||
std::string ext = extension;
|
std::string ext = extension;
|
||||||
for (auto& c : ext) {
|
for (auto& c : ext) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -17,8 +18,4 @@ class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
|
|||||||
|
|
||||||
static bool supportsFormat(const std::string& extension);
|
static bool supportsFormat(const std::string& extension);
|
||||||
const char* getFormatName() const override { return "JPEG"; }
|
const char* getFormatName() const override { return "JPEG"; }
|
||||||
|
|
||||||
private:
|
|
||||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
|
||||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ board_build.partitions = partitions.csv
|
|||||||
extra_scripts =
|
extra_scripts =
|
||||||
pre:scripts/build_html.py
|
pre:scripts/build_html.py
|
||||||
pre:scripts/gen_i18n.py
|
pre:scripts/gen_i18n.py
|
||||||
|
pre:scripts/patch_jpegdec.py
|
||||||
pre:scripts/git_branch.py
|
pre:scripts/git_branch.py
|
||||||
|
|
||||||
; Libraries
|
; Libraries
|
||||||
@@ -57,6 +58,7 @@ lib_deps =
|
|||||||
bblanchon/ArduinoJson @ 7.4.2
|
bblanchon/ArduinoJson @ 7.4.2
|
||||||
ricmoo/QRCode @ 0.0.1
|
ricmoo/QRCode @ 0.0.1
|
||||||
bitbank2/PNGdec @ ^1.0.0
|
bitbank2/PNGdec @ ^1.0.0
|
||||||
|
bitbank2/JPEGDEC @ ^1.8.0
|
||||||
links2004/WebSockets @ 2.7.3
|
links2004/WebSockets @ 2.7.3
|
||||||
|
|
||||||
[env:default]
|
[env:default]
|
||||||
|
|||||||
117
scripts/patch_jpegdec.py
Normal file
117
scripts/patch_jpegdec.py
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
PlatformIO pre-build script: patch JPEGDEC library for progressive JPEG support.
|
||||||
|
|
||||||
|
Two patches are applied:
|
||||||
|
|
||||||
|
1. JPEGMakeHuffTables: Skip AC Huffman table construction for progressive JPEGs.
|
||||||
|
JPEGDEC 1.8.x fails to open progressive JPEGs because JPEGMakeHuffTables()
|
||||||
|
cannot build AC tables with 11+-bit codes (the "slow tables" path is disabled).
|
||||||
|
Since progressive decode only uses DC coefficients, AC tables are not needed.
|
||||||
|
|
||||||
|
2. JPEGDecodeMCU_P: Guard pMCU writes against MCU_SKIP (-8).
|
||||||
|
The non-progressive JPEGDecodeMCU checks `iMCU >= 0` before writing to pMCU,
|
||||||
|
but JPEGDecodeMCU_P does not. When EIGHT_BIT_GRAYSCALE mode skips chroma
|
||||||
|
channels by passing MCU_SKIP, the unguarded write goes to a wild pointer
|
||||||
|
(sMCUs[0xFFFFF8]) and crashes.
|
||||||
|
|
||||||
|
Both patches are applied idempotently so it is safe to run on every build.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Import("env")
|
||||||
|
import os
|
||||||
|
|
||||||
|
def patch_jpegdec(env):
|
||||||
|
# Find the JPEGDEC library in libdeps
|
||||||
|
libdeps_dir = os.path.join(env["PROJECT_DIR"], ".pio", "libdeps")
|
||||||
|
if not os.path.isdir(libdeps_dir):
|
||||||
|
return
|
||||||
|
for env_dir in os.listdir(libdeps_dir):
|
||||||
|
jpeg_inl = os.path.join(libdeps_dir, env_dir, "JPEGDEC", "src", "jpeg.inl")
|
||||||
|
if os.path.isfile(jpeg_inl):
|
||||||
|
_apply_ac_table_patch(jpeg_inl)
|
||||||
|
_apply_mcu_skip_patch(jpeg_inl)
|
||||||
|
|
||||||
|
def _apply_ac_table_patch(filepath):
|
||||||
|
MARKER = "// CrossPoint patch: skip AC tables for progressive JPEG"
|
||||||
|
with open(filepath, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
if MARKER in content:
|
||||||
|
return # already patched
|
||||||
|
|
||||||
|
OLD = """\
|
||||||
|
}
|
||||||
|
// now do AC components (up to 4 tables of 16-bit codes)"""
|
||||||
|
|
||||||
|
NEW = """\
|
||||||
|
}
|
||||||
|
""" + MARKER + """
|
||||||
|
// Progressive JPEG: only DC coefficients are decoded (first scan), so AC
|
||||||
|
// Huffman tables are not needed. Skip building them to avoid failing on
|
||||||
|
// 11+-bit AC codes that the optimized table builder cannot handle.
|
||||||
|
if (pJPEG->ucMode == 0xc2)
|
||||||
|
return 1;
|
||||||
|
// now do AC components (up to 4 tables of 16-bit codes)"""
|
||||||
|
|
||||||
|
if OLD not in content:
|
||||||
|
print("WARNING: JPEGDEC AC table patch target not found in %s — library may have been updated" % filepath)
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(OLD, NEW, 1)
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print("Patched JPEGDEC: skip AC tables for progressive JPEG: %s" % filepath)
|
||||||
|
|
||||||
|
def _apply_mcu_skip_patch(filepath):
|
||||||
|
MARKER = "// CrossPoint patch: guard pMCU write for MCU_SKIP"
|
||||||
|
with open(filepath, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
if MARKER in content:
|
||||||
|
return # already patched
|
||||||
|
|
||||||
|
# Patch 1: Guard the unconditional pMCU[0] write in JPEGDecodeMCU_P.
|
||||||
|
# This is the DC coefficient store that crashes when iMCU = MCU_SKIP (-8).
|
||||||
|
OLD_DC = """\
|
||||||
|
pMCU[0] = (short)*iDCPredictor; // store in MCU[0]
|
||||||
|
}
|
||||||
|
// Now get the other 63 AC coefficients"""
|
||||||
|
|
||||||
|
NEW_DC = """\
|
||||||
|
""" + MARKER + """
|
||||||
|
if (iMCU >= 0)
|
||||||
|
pMCU[0] = (short)*iDCPredictor; // store in MCU[0]
|
||||||
|
}
|
||||||
|
// Now get the other 63 AC coefficients"""
|
||||||
|
|
||||||
|
if OLD_DC not in content:
|
||||||
|
print("WARNING: JPEGDEC MCU_SKIP patch target not found in %s — library may have been updated" % filepath)
|
||||||
|
return
|
||||||
|
|
||||||
|
content = content.replace(OLD_DC, NEW_DC, 1)
|
||||||
|
|
||||||
|
# Patch 2: Guard the successive approximation pMCU[0] write.
|
||||||
|
# This path is taken on subsequent scans (cApproxBitsHigh != 0), which we
|
||||||
|
# don't normally hit (we only decode first scan), but guard it for safety.
|
||||||
|
OLD_SA = """\
|
||||||
|
pMCU[0] |= iPositive;
|
||||||
|
}
|
||||||
|
goto mcu_done; // that's it"""
|
||||||
|
|
||||||
|
NEW_SA = """\
|
||||||
|
if (iMCU >= 0)
|
||||||
|
pMCU[0] |= iPositive;
|
||||||
|
}
|
||||||
|
goto mcu_done; // that's it"""
|
||||||
|
|
||||||
|
if OLD_SA in content:
|
||||||
|
content = content.replace(OLD_SA, NEW_SA, 1)
|
||||||
|
|
||||||
|
with open(filepath, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print("Patched JPEGDEC: guard pMCU writes for MCU_SKIP in JPEGDecodeMCU_P: %s" % filepath)
|
||||||
|
|
||||||
|
# Apply patches immediately when this pre: script runs, before compilation starts.
|
||||||
|
# Previously used env.AddPreAction("buildprog", ...) which deferred patching until
|
||||||
|
# the link step — after the library was already compiled from unpatched source.
|
||||||
|
patch_jpegdec(env)
|
||||||
Reference in New Issue
Block a user