refactor: replace picojpeg with JPEGDEC for cover art conversion (#1517)
## Summary - Removes the vendored `picojpeg` library and rewrites `JpegToBmpConverter` to use the already-present `JPEGDEC` (bitbank2) dependency - Eliminates the redundancy of having two JPEG decoders in the firmware - All BMP output (headers, fixed-point scaling, Atkinson/Floyd-Steinberg dithering) is identical to before — cached cover BMPs are unaffected ## Size impact | | Before | After | Delta | |---|---|---|---| | Flash | 5,754,089 bytes (87.8%) | 5,744,777 bytes (87.7%) | **−9,312 bytes** | | RAM | 95,212 bytes (29.1%) | 92,852 bytes (28.3%) | **−2,360 bytes** | ## Implementation notes - `bmpDrawCallback` receives MCU-sized blocks from JPEGDEC (up to 16 rows × MCU-width), accumulates them into a pre-allocated `mcuBuf`, and applies the same scaling + dithering logic once each MCU row is complete - File I/O uses a file-scope static `FsFile*` (safe in single-threaded embedded context) via JPEGDEC's open/read/seek callbacks — same pattern as `JpegToFramebufferConverter` - Added a 52 KB free-heap guard before allocating the JPEGDEC object (~17 KB) - `lib/picojpeg/` deleted (2,087 lines of C removed) ## Test plan - [ ] Build compiles without warnings - [ ] Cover art BMP cache regenerates correctly for EPUB books - [ ] Home screen thumbnails (1-bit BMP path) render correctly - [ ] Custom-size thumbnails (`jpegFileToBmpStreamWithSize`) render correctly 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -2,22 +2,15 @@
|
|||||||
|
|
||||||
#include <HalDisplay.h>
|
#include <HalDisplay.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
|
#include <JPEGDEC.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <picojpeg.h>
|
|
||||||
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
#include "BitmapHelpers.h"
|
#include "BitmapHelpers.h"
|
||||||
|
|
||||||
// Context structure for picojpeg callback
|
|
||||||
struct JpegReadContext {
|
|
||||||
FsFile& file;
|
|
||||||
uint8_t buffer[512];
|
|
||||||
size_t bufferPos;
|
|
||||||
size_t bufferFilled;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations
|
// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -165,103 +158,292 @@ static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callback function for picojpeg to read JPEG data
|
namespace {
|
||||||
unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const unsigned char buf_size,
|
|
||||||
unsigned char* pBytes_actually_read, void* pCallback_data) {
|
|
||||||
auto* context = static_cast<JpegReadContext*>(pCallback_data);
|
|
||||||
|
|
||||||
if (!context || !context->file) {
|
// Max MCU height supported by any JPEG (4:2:0 chroma = 16 rows, 4:4:4 = 8 rows)
|
||||||
return PJPG_STREAM_READ_ERROR;
|
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;
|
||||||
|
|
||||||
|
// Static file pointer for JPEGDEC open callback.
|
||||||
|
// Safe in single-threaded embedded context; never accessed concurrently.
|
||||||
|
static FsFile* s_jpegFile = nullptr;
|
||||||
|
|
||||||
|
void* bmpJpegOpen(const char* /*filename*/, int32_t* size) {
|
||||||
|
if (!s_jpegFile || !*s_jpegFile) return nullptr;
|
||||||
|
s_jpegFile->seek(0);
|
||||||
|
*size = static_cast<int32_t>(s_jpegFile->size());
|
||||||
|
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<FsFile*>(pFile->fHandle);
|
||||||
|
if (!f) return 0;
|
||||||
|
int32_t n = f->read(pBuf, len);
|
||||||
|
if (n < 0) n = 0;
|
||||||
|
pFile->iPos += n;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t bmpJpegSeek(JPEGFILE* pFile, int32_t pos) {
|
||||||
|
auto* f = reinterpret_cast<FsFile*>(pFile->fHandle);
|
||||||
|
if (!f || !f->seek(pos)) return -1;
|
||||||
|
pFile->iPos = pos;
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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
|
||||||
|
uint32_t* rowAccum;
|
||||||
|
uint32_t* rowCount;
|
||||||
|
|
||||||
|
uint8_t* bmpRow;
|
||||||
|
|
||||||
|
AtkinsonDitherer* atkinsonDitherer;
|
||||||
|
FloydSteinbergDitherer* fsDitherer;
|
||||||
|
Atkinson1BitDitherer* atkinson1BitDitherer;
|
||||||
|
|
||||||
|
bool error;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write a fully-assembled output row (grayscale bytes, length outWidth) to BMP
|
||||||
|
static void writeOutputRow(BmpConvertCtx* ctx, const uint8_t* srcRow, int outY) {
|
||||||
|
memset(ctx->bmpRow, 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to refill our context buffer
|
ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow);
|
||||||
if (context->bufferPos >= context->bufferFilled) {
|
}
|
||||||
context->bufferFilled = context->file.read(context->buffer, sizeof(context->buffer));
|
|
||||||
context->bufferPos = 0;
|
|
||||||
|
|
||||||
if (context->bufferFilled == 0) {
|
// Flush one scaled output row from Y-axis accumulators and advance currentOutY
|
||||||
// EOF or error
|
static void flushScaledRow(BmpConvertCtx* ctx) {
|
||||||
*pBytes_actually_read = 0;
|
memset(ctx->bmpRow, 0, ctx->bytesPerRow);
|
||||||
return 0; // Success (EOF is normal)
|
|
||||||
|
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, ctx->bytesPerRow);
|
||||||
|
ctx->currentOutY++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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 + 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 + (y - blockY) * ctx->srcWidth;
|
||||||
|
|
||||||
|
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, 0, ctx->outWidth * sizeof(uint32_t));
|
||||||
|
memset(ctx->rowCount, 0, ctx->outWidth * sizeof(uint32_t));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy available bytes to picojpeg's buffer
|
return ctx->error ? 0 : 1;
|
||||||
const size_t available = context->bufferFilled - context->bufferPos;
|
|
||||||
const size_t toRead = available < buf_size ? available : buf_size;
|
|
||||||
|
|
||||||
memcpy(pBuf, context->buffer + context->bufferPos, toRead);
|
|
||||||
context->bufferPos += toRead;
|
|
||||||
*pBytes_actually_read = static_cast<unsigned char>(toRead);
|
|
||||||
|
|
||||||
return 0; // Success
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
// Internal implementation with configurable target size and bit depth
|
// Internal implementation with configurable target size and bit depth
|
||||||
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||||
bool oneBit, bool crop) {
|
bool oneBit, bool crop) {
|
||||||
LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight);
|
||||||
|
|
||||||
// Setup context for picojpeg callback
|
if (ESP.getFreeHeap() < MIN_FREE_HEAP) {
|
||||||
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", ESP.getFreeHeap(), MIN_FREE_HEAP);
|
||||||
|
|
||||||
// Initialize picojpeg decoder
|
|
||||||
pjpeg_image_info_t imageInfo;
|
|
||||||
const unsigned char status = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, 0);
|
|
||||||
if (status != 0) {
|
|
||||||
LOG_ERR("JPG", "JPEG decode init failed with error code: %d", status);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("JPG", "JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
s_jpegFile = &jpegFile;
|
||||||
imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol);
|
|
||||||
|
JPEGDEC* jpeg = new (std::nothrow) JPEGDEC();
|
||||||
|
if (!jpeg) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate 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());
|
||||||
|
delete jpeg;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int srcWidth = jpeg->getWidth();
|
||||||
|
const int srcHeight = jpeg->getHeight();
|
||||||
|
|
||||||
|
LOG_DBG("JPG", "JPEG dimensions: %dx%d", srcWidth, srcHeight);
|
||||||
|
|
||||||
// Safety limits to prevent memory issues on ESP32
|
|
||||||
constexpr int MAX_IMAGE_WIDTH = 2048;
|
constexpr int MAX_IMAGE_WIDTH = 2048;
|
||||||
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
constexpr int MAX_IMAGE_HEIGHT = 3072;
|
||||||
constexpr int MAX_MCU_ROW_BYTES = 65536;
|
|
||||||
|
|
||||||
if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) {
|
if (srcWidth <= 0 || srcHeight <= 0 || srcWidth > MAX_IMAGE_WIDTH || srcHeight > MAX_IMAGE_HEIGHT) {
|
||||||
LOG_DBG("JPG", "Image too large (%dx%d), max supported: %dx%d", imageInfo.m_width, imageInfo.m_height,
|
LOG_DBG("JPG", "Image too large or invalid (%dx%d), max supported: %dx%d", srcWidth, srcHeight, MAX_IMAGE_WIDTH,
|
||||||
MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT);
|
MAX_IMAGE_HEIGHT);
|
||||||
|
jpeg->close();
|
||||||
|
delete jpeg;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate output dimensions (pre-scale to fit display exactly)
|
// Calculate output dimensions (pre-scale to fit display exactly)
|
||||||
int outWidth = imageInfo.m_width;
|
int outWidth = srcWidth;
|
||||||
int outHeight = imageInfo.m_height;
|
int outHeight = srcHeight;
|
||||||
// Use fixed-point scaling (16.16) for sub-pixel accuracy
|
|
||||||
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 && (imageInfo.m_width != targetWidth || imageInfo.m_height != targetHeight)) {
|
if (targetWidth > 0 && targetHeight > 0 && (srcWidth != targetWidth || srcHeight != targetHeight)) {
|
||||||
// Calculate scale to fit/fill target dimensions while maintaining aspect ratio
|
const float scaleToFitWidth = static_cast<float>(targetWidth) / srcWidth;
|
||||||
const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width;
|
const float scaleToFitHeight = static_cast<float>(targetHeight) / srcHeight;
|
||||||
const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height;
|
float scale = 1.0f;
|
||||||
// We scale to the smaller dimension, so we can potentially crop later.
|
if (crop) {
|
||||||
float scale = 1.0;
|
|
||||||
if (crop) { // if we will crop, scale to the smaller dimension
|
|
||||||
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
} else { // else, scale to the larger dimension to fit
|
} else {
|
||||||
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
outWidth = static_cast<int>(imageInfo.m_width * scale);
|
outWidth = static_cast<int>(srcWidth * scale);
|
||||||
outHeight = static_cast<int>(imageInfo.m_height * scale);
|
outHeight = static_cast<int>(srcHeight * scale);
|
||||||
|
|
||||||
// Ensure at least 1 pixel
|
|
||||||
if (outWidth < 1) outWidth = 1;
|
if (outWidth < 1) outWidth = 1;
|
||||||
if (outHeight < 1) outHeight = 1;
|
if (outHeight < 1) outHeight = 1;
|
||||||
|
|
||||||
// Calculate fixed-point scale factors (source pixels per output pixel)
|
scaleX_fp = (static_cast<uint32_t>(srcWidth) << 16) / outWidth;
|
||||||
// scaleX_fp = (srcWidth << 16) / outWidth
|
scaleY_fp = (static_cast<uint32_t>(srcHeight) << 16) / outHeight;
|
||||||
scaleX_fp = (static_cast<uint32_t>(imageInfo.m_width) << 16) / outWidth;
|
|
||||||
scaleY_fp = (static_cast<uint32_t>(imageInfo.m_height) << 16) / outHeight;
|
|
||||||
needsScaling = true;
|
needsScaling = true;
|
||||||
|
|
||||||
LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth, outHeight,
|
LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", srcWidth, srcHeight, outWidth, outHeight, targetWidth,
|
||||||
targetWidth, targetHeight);
|
targetHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write BMP header with output dimensions
|
// Write BMP header with output dimensions
|
||||||
@@ -271,285 +453,84 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm
|
|||||||
bytesPerRow = (outWidth + 3) / 4 * 4;
|
bytesPerRow = (outWidth + 3) / 4 * 4;
|
||||||
} else if (oneBit) {
|
} else if (oneBit) {
|
||||||
writeBmpHeader1bit(bmpOut, outWidth, outHeight);
|
writeBmpHeader1bit(bmpOut, outWidth, outHeight);
|
||||||
bytesPerRow = (outWidth + 31) / 32 * 4; // 1 bit per pixel
|
bytesPerRow = (outWidth + 31) / 32 * 4;
|
||||||
} else {
|
} else {
|
||||||
writeBmpHeader2bit(bmpOut, outWidth, outHeight);
|
writeBmpHeader2bit(bmpOut, outWidth, outHeight);
|
||||||
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
|
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* rowBuffer = nullptr;
|
BmpConvertCtx ctx = {};
|
||||||
uint8_t* mcuRowBuffer = nullptr;
|
ctx.bmpOut = &bmpOut;
|
||||||
AtkinsonDitherer* atkinsonDitherer = nullptr;
|
ctx.srcWidth = srcWidth;
|
||||||
FloydSteinbergDitherer* fsDitherer = nullptr;
|
ctx.srcHeight = srcHeight;
|
||||||
Atkinson1BitDitherer* atkinson1BitDitherer = nullptr;
|
ctx.outWidth = outWidth;
|
||||||
uint32_t* rowAccum = nullptr; // Accumulator for each output X (32-bit for larger sums)
|
ctx.outHeight = outHeight;
|
||||||
uint32_t* rowCount = nullptr; // Count of source pixels accumulated per output X
|
ctx.oneBit = oneBit;
|
||||||
|
ctx.bytesPerRow = bytesPerRow;
|
||||||
|
ctx.needsScaling = needsScaling;
|
||||||
|
ctx.scaleX_fp = scaleX_fp;
|
||||||
|
ctx.scaleY_fp = scaleY_fp;
|
||||||
|
ctx.error = false;
|
||||||
|
|
||||||
// RAII guard: frees all heap resources on any return path, including early exits.
|
// RAII guard: frees all heap resources on any return path
|
||||||
// Holds references so it always sees the latest pointer values assigned below.
|
|
||||||
struct Cleanup {
|
struct Cleanup {
|
||||||
uint8_t*& rowBuffer;
|
BmpConvertCtx& ctx;
|
||||||
uint8_t*& mcuRowBuffer;
|
JPEGDEC* jpeg;
|
||||||
AtkinsonDitherer*& atkinsonDitherer;
|
|
||||||
FloydSteinbergDitherer*& fsDitherer;
|
|
||||||
Atkinson1BitDitherer*& atkinson1BitDitherer;
|
|
||||||
uint32_t*& rowAccum;
|
|
||||||
uint32_t*& rowCount;
|
|
||||||
~Cleanup() {
|
~Cleanup() {
|
||||||
delete[] rowAccum;
|
delete[] ctx.rowAccum;
|
||||||
delete[] rowCount;
|
delete[] ctx.rowCount;
|
||||||
delete atkinsonDitherer;
|
delete ctx.atkinsonDitherer;
|
||||||
delete fsDitherer;
|
delete ctx.fsDitherer;
|
||||||
delete atkinson1BitDitherer;
|
delete ctx.atkinson1BitDitherer;
|
||||||
free(mcuRowBuffer);
|
free(ctx.mcuBuf);
|
||||||
free(rowBuffer);
|
free(ctx.bmpRow);
|
||||||
|
jpeg->close();
|
||||||
|
delete jpeg;
|
||||||
}
|
}
|
||||||
} cleanup{rowBuffer, mcuRowBuffer, atkinsonDitherer, fsDitherer, atkinson1BitDitherer, rowAccum, rowCount};
|
} cleanup{ctx, jpeg};
|
||||||
|
|
||||||
// Allocate row buffer
|
// MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale
|
||||||
rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
ctx.mcuBuf = static_cast<uint8_t*>(malloc(MAX_MCU_HEIGHT * srcWidth));
|
||||||
if (!rowBuffer) {
|
if (!ctx.mcuBuf) {
|
||||||
LOG_ERR("JPG", "Failed to allocate row buffer");
|
LOG_ERR("JPG", "Failed to allocate MCU buffer (%d bytes)", MAX_MCU_HEIGHT * srcWidth);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
memset(ctx.mcuBuf, 0, MAX_MCU_HEIGHT * srcWidth);
|
||||||
|
|
||||||
// Allocate a buffer for one MCU row worth of grayscale pixels
|
ctx.bmpRow = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||||
// This is the minimal memory needed for streaming conversion
|
if (!ctx.bmpRow) {
|
||||||
const int mcuPixelHeight = imageInfo.m_MCUHeight;
|
LOG_ERR("JPG", "Failed to allocate BMP row buffer");
|
||||||
const int mcuRowPixels = imageInfo.m_width * mcuPixelHeight;
|
|
||||||
|
|
||||||
// Validate MCU row buffer size before allocation
|
|
||||||
if (mcuRowPixels > MAX_MCU_ROW_BYTES) {
|
|
||||||
LOG_DBG("JPG", "MCU row buffer too large (%d bytes), max: %d", mcuRowPixels, MAX_MCU_ROW_BYTES);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mcuRowBuffer = static_cast<uint8_t*>(malloc(mcuRowPixels));
|
|
||||||
if (!mcuRowBuffer) {
|
|
||||||
LOG_ERR("JPG", "Failed to allocate MCU row buffer (%d bytes)", mcuRowPixels);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ditherer if enabled
|
|
||||||
// Use OUTPUT dimensions for dithering (after prescaling)
|
|
||||||
if (oneBit) {
|
|
||||||
// For 1-bit output, use Atkinson dithering for better quality
|
|
||||||
atkinson1BitDitherer = new Atkinson1BitDitherer(outWidth);
|
|
||||||
} else if (!USE_8BIT_OUTPUT) {
|
|
||||||
if (USE_ATKINSON) {
|
|
||||||
atkinsonDitherer = new AtkinsonDitherer(outWidth);
|
|
||||||
} else if (USE_FLOYD_STEINBERG) {
|
|
||||||
fsDitherer = new FloydSteinbergDitherer(outWidth);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For scaling: accumulate source rows into scaled output rows
|
|
||||||
// We need to track which source Y maps to which output Y
|
|
||||||
// Using fixed-point: srcY_fp = outY * scaleY_fp (gives source Y in 16.16 format)
|
|
||||||
int currentOutY = 0; // Current output row being accumulated
|
|
||||||
uint32_t nextOutY_srcStart = 0; // Source Y where next output row starts (16.16 fixed point)
|
|
||||||
|
|
||||||
if (needsScaling) {
|
if (needsScaling) {
|
||||||
rowAccum = new uint32_t[outWidth]();
|
ctx.rowAccum = new (std::nothrow) uint32_t[outWidth]();
|
||||||
rowCount = new uint32_t[outWidth]();
|
ctx.rowCount = new (std::nothrow) uint32_t[outWidth]();
|
||||||
nextOutY_srcStart = scaleY_fp; // First boundary is at scaleY_fp (source Y for outY=1)
|
if (!ctx.rowAccum || !ctx.rowCount) {
|
||||||
|
LOG_ERR("JPG", "Failed to allocate scaling buffers");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.nextOutY_srcStart = scaleY_fp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process MCUs row-by-row and write to BMP as we go (top-down)
|
if (oneBit) {
|
||||||
const int mcuPixelWidth = imageInfo.m_MCUWidth;
|
ctx.atkinson1BitDitherer = new (std::nothrow) Atkinson1BitDitherer(outWidth);
|
||||||
|
} else if (!USE_8BIT_OUTPUT) {
|
||||||
for (int mcuY = 0; mcuY < imageInfo.m_MCUSPerCol; mcuY++) {
|
if (USE_ATKINSON) {
|
||||||
// Clear the MCU row buffer
|
ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(outWidth);
|
||||||
memset(mcuRowBuffer, 0, mcuRowPixels);
|
} else if (USE_FLOYD_STEINBERG) {
|
||||||
|
ctx.fsDitherer = new (std::nothrow) FloydSteinbergDitherer(outWidth);
|
||||||
// Decode one row of MCUs
|
|
||||||
for (int mcuX = 0; mcuX < imageInfo.m_MCUSPerRow; mcuX++) {
|
|
||||||
const unsigned char mcuStatus = pjpeg_decode_mcu();
|
|
||||||
if (mcuStatus != 0) {
|
|
||||||
if (mcuStatus == PJPG_NO_MORE_BLOCKS) {
|
|
||||||
LOG_ERR("JPG", "Unexpected end of blocks at MCU (%d, %d)", mcuX, mcuY);
|
|
||||||
} else {
|
|
||||||
LOG_ERR("JPG", "JPEG decode MCU failed at (%d, %d) with error code: %d", mcuX, mcuY, mcuStatus);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// picojpeg stores MCU data in 8x8 blocks
|
|
||||||
// Block layout: H2V2(16x16)=0,64,128,192 H2V1(16x8)=0,64 H1V2(8x16)=0,128
|
|
||||||
for (int blockY = 0; blockY < mcuPixelHeight; blockY++) {
|
|
||||||
for (int blockX = 0; blockX < mcuPixelWidth; blockX++) {
|
|
||||||
const int pixelX = mcuX * mcuPixelWidth + blockX;
|
|
||||||
if (pixelX >= imageInfo.m_width) continue;
|
|
||||||
|
|
||||||
// Calculate proper block offset for picojpeg buffer
|
|
||||||
const int blockCol = blockX / 8;
|
|
||||||
const int blockRow = blockY / 8;
|
|
||||||
const int localX = blockX % 8;
|
|
||||||
const int localY = blockY % 8;
|
|
||||||
const int blocksPerRow = mcuPixelWidth / 8;
|
|
||||||
const int blockIndex = blockRow * blocksPerRow + blockCol;
|
|
||||||
const int pixelOffset = blockIndex * 64 + localY * 8 + localX;
|
|
||||||
|
|
||||||
uint8_t gray;
|
|
||||||
if (imageInfo.m_comps == 1) {
|
|
||||||
gray = imageInfo.m_pMCUBufR[pixelOffset];
|
|
||||||
} else {
|
|
||||||
const uint8_t r = imageInfo.m_pMCUBufR[pixelOffset];
|
|
||||||
const uint8_t g = imageInfo.m_pMCUBufG[pixelOffset];
|
|
||||||
const uint8_t b = imageInfo.m_pMCUBufB[pixelOffset];
|
|
||||||
gray = (r * 25 + g * 50 + b * 25) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
mcuRowBuffer[blockY * imageInfo.m_width + pixelX] = gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process source rows from this MCU row
|
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
||||||
const int startRow = mcuY * mcuPixelHeight;
|
jpeg->setUserPointer(&ctx);
|
||||||
const int endRow = (mcuY + 1) * mcuPixelHeight;
|
|
||||||
|
|
||||||
for (int y = startRow; y < endRow && y < imageInfo.m_height; y++) {
|
rc = jpeg->decode(0, 0, 0);
|
||||||
const int bufferY = y - startRow;
|
|
||||||
|
|
||||||
if (!needsScaling) {
|
if (rc != 1 || ctx.error) {
|
||||||
// No scaling - direct output (1:1 mapping)
|
LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError());
|
||||||
memset(rowBuffer, 0, bytesPerRow);
|
return false;
|
||||||
|
|
||||||
if (USE_8BIT_OUTPUT && !oneBit) {
|
|
||||||
for (int x = 0; x < outWidth; x++) {
|
|
||||||
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
|
||||||
rowBuffer[x] = adjustPixel(gray);
|
|
||||||
}
|
|
||||||
} else if (oneBit) {
|
|
||||||
// 1-bit output with Atkinson dithering for better quality
|
|
||||||
for (int x = 0; x < outWidth; x++) {
|
|
||||||
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
|
||||||
const uint8_t bit =
|
|
||||||
atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x) : quantize1bit(gray, x, y);
|
|
||||||
// Pack 1-bit value: MSB first, 8 pixels per byte
|
|
||||||
const int byteIndex = x / 8;
|
|
||||||
const int bitOffset = 7 - (x % 8);
|
|
||||||
rowBuffer[byteIndex] |= (bit << bitOffset);
|
|
||||||
}
|
|
||||||
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
|
||||||
} else {
|
|
||||||
// 2-bit output
|
|
||||||
for (int x = 0; x < outWidth; x++) {
|
|
||||||
const uint8_t gray = adjustPixel(mcuRowBuffer[bufferY * imageInfo.m_width + x]);
|
|
||||||
uint8_t twoBit;
|
|
||||||
if (atkinsonDitherer) {
|
|
||||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
|
||||||
} else if (fsDitherer) {
|
|
||||||
twoBit = fsDitherer->processPixel(gray, x);
|
|
||||||
} else {
|
|
||||||
twoBit = quantize(gray, x, y);
|
|
||||||
}
|
|
||||||
const int byteIndex = (x * 2) / 8;
|
|
||||||
const int bitOffset = 6 - ((x * 2) % 8);
|
|
||||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
|
||||||
}
|
|
||||||
if (atkinsonDitherer)
|
|
||||||
atkinsonDitherer->nextRow();
|
|
||||||
else if (fsDitherer)
|
|
||||||
fsDitherer->nextRow();
|
|
||||||
}
|
|
||||||
bmpOut.write(rowBuffer, bytesPerRow);
|
|
||||||
} else {
|
|
||||||
// Fixed-point area averaging for exact fit scaling
|
|
||||||
// For each output pixel X, accumulate source pixels that map to it
|
|
||||||
// srcX range for outX: [outX * scaleX_fp >> 16, (outX+1) * scaleX_fp >> 16)
|
|
||||||
const uint8_t* srcRow = mcuRowBuffer + bufferY * imageInfo.m_width;
|
|
||||||
|
|
||||||
for (int outX = 0; outX < outWidth; outX++) {
|
|
||||||
// Calculate source X range for this output pixel
|
|
||||||
const int srcXStart = (static_cast<uint32_t>(outX) * scaleX_fp) >> 16;
|
|
||||||
const int srcXEnd = (static_cast<uint32_t>(outX + 1) * scaleX_fp) >> 16;
|
|
||||||
|
|
||||||
// Accumulate all source pixels in this range
|
|
||||||
int sum = 0;
|
|
||||||
int count = 0;
|
|
||||||
for (int srcX = srcXStart; srcX < srcXEnd && srcX < imageInfo.m_width; srcX++) {
|
|
||||||
sum += srcRow[srcX];
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle edge case: if no pixels in range, use nearest
|
|
||||||
if (count == 0 && srcXStart < imageInfo.m_width) {
|
|
||||||
sum = srcRow[srcXStart];
|
|
||||||
count = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
rowAccum[outX] += sum;
|
|
||||||
rowCount[outX] += count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've crossed into the next output row(s)
|
|
||||||
// Current source Y in fixed point: y << 16
|
|
||||||
const uint32_t srcY_fp = static_cast<uint32_t>(y + 1) << 16;
|
|
||||||
|
|
||||||
// Output all rows whose boundaries we've crossed (handles both up and downscaling)
|
|
||||||
// For upscaling, one source row may produce multiple output rows
|
|
||||||
while (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
|
|
||||||
memset(rowBuffer, 0, bytesPerRow);
|
|
||||||
|
|
||||||
if (USE_8BIT_OUTPUT && !oneBit) {
|
|
||||||
for (int x = 0; x < outWidth; x++) {
|
|
||||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
|
||||||
rowBuffer[x] = adjustPixel(gray);
|
|
||||||
}
|
|
||||||
} else if (oneBit) {
|
|
||||||
// 1-bit output with Atkinson dithering for better quality
|
|
||||||
for (int x = 0; x < outWidth; x++) {
|
|
||||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
|
||||||
const uint8_t bit = atkinson1BitDitherer ? atkinson1BitDitherer->processPixel(gray, x)
|
|
||||||
: quantize1bit(gray, x, currentOutY);
|
|
||||||
// Pack 1-bit value: MSB first, 8 pixels per byte
|
|
||||||
const int byteIndex = x / 8;
|
|
||||||
const int bitOffset = 7 - (x % 8);
|
|
||||||
rowBuffer[byteIndex] |= (bit << bitOffset);
|
|
||||||
}
|
|
||||||
if (atkinson1BitDitherer) atkinson1BitDitherer->nextRow();
|
|
||||||
} else {
|
|
||||||
// 2-bit output
|
|
||||||
for (int x = 0; x < outWidth; x++) {
|
|
||||||
const uint8_t gray = adjustPixel((rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0);
|
|
||||||
uint8_t twoBit;
|
|
||||||
if (atkinsonDitherer) {
|
|
||||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
|
||||||
} else if (fsDitherer) {
|
|
||||||
twoBit = fsDitherer->processPixel(gray, x);
|
|
||||||
} else {
|
|
||||||
twoBit = quantize(gray, x, currentOutY);
|
|
||||||
}
|
|
||||||
const int byteIndex = (x * 2) / 8;
|
|
||||||
const int bitOffset = 6 - ((x * 2) % 8);
|
|
||||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
|
||||||
}
|
|
||||||
if (atkinsonDitherer)
|
|
||||||
atkinsonDitherer->nextRow();
|
|
||||||
else if (fsDitherer)
|
|
||||||
fsDitherer->nextRow();
|
|
||||||
}
|
|
||||||
|
|
||||||
bmpOut.write(rowBuffer, bytesPerRow);
|
|
||||||
currentOutY++;
|
|
||||||
|
|
||||||
// Update boundary for next output row
|
|
||||||
nextOutY_srcStart = static_cast<uint32_t>(currentOutY + 1) * scaleY_fp;
|
|
||||||
|
|
||||||
// For upscaling: don't reset accumulators if next output row uses same source data
|
|
||||||
// Only reset when we'll move to a new source row
|
|
||||||
if (srcY_fp >= nextOutY_srcStart) {
|
|
||||||
// More output rows to emit from same source - keep accumulator data
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Moving to next source row - reset accumulators
|
|
||||||
memset(rowAccum, 0, outWidth * sizeof(uint32_t));
|
|
||||||
memset(rowCount, 0, outWidth * sizeof(uint32_t));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("JPG", "Successfully converted JPEG to BMP");
|
LOG_DBG("JPG", "Successfully converted JPEG to BMP");
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ class Print;
|
|||||||
class ZipFile;
|
class ZipFile;
|
||||||
|
|
||||||
class JpegToBmpConverter {
|
class JpegToBmpConverter {
|
||||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
|
||||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
|
||||||
static bool jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
static bool jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||||
bool oneBit, bool crop = true);
|
bool oneBit, bool crop = true);
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,124 +0,0 @@
|
|||||||
//------------------------------------------------------------------------------
|
|
||||||
// picojpeg - Public domain, Rich Geldreich <richgel99@gmail.com>
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
#ifndef PICOJPEG_H
|
|
||||||
#define PICOJPEG_H
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Error codes
|
|
||||||
enum {
|
|
||||||
PJPG_NO_MORE_BLOCKS = 1,
|
|
||||||
PJPG_BAD_DHT_COUNTS,
|
|
||||||
PJPG_BAD_DHT_INDEX,
|
|
||||||
PJPG_BAD_DHT_MARKER,
|
|
||||||
PJPG_BAD_DQT_MARKER,
|
|
||||||
PJPG_BAD_DQT_TABLE,
|
|
||||||
PJPG_BAD_PRECISION,
|
|
||||||
PJPG_BAD_HEIGHT,
|
|
||||||
PJPG_BAD_WIDTH,
|
|
||||||
PJPG_TOO_MANY_COMPONENTS,
|
|
||||||
PJPG_BAD_SOF_LENGTH,
|
|
||||||
PJPG_BAD_VARIABLE_MARKER,
|
|
||||||
PJPG_BAD_DRI_LENGTH,
|
|
||||||
PJPG_BAD_SOS_LENGTH,
|
|
||||||
PJPG_BAD_SOS_COMP_ID,
|
|
||||||
PJPG_W_EXTRA_BYTES_BEFORE_MARKER,
|
|
||||||
PJPG_NO_ARITHMITIC_SUPPORT,
|
|
||||||
PJPG_UNEXPECTED_MARKER,
|
|
||||||
PJPG_NOT_JPEG,
|
|
||||||
PJPG_UNSUPPORTED_MARKER,
|
|
||||||
PJPG_BAD_DQT_LENGTH,
|
|
||||||
PJPG_TOO_MANY_BLOCKS,
|
|
||||||
PJPG_UNDEFINED_QUANT_TABLE,
|
|
||||||
PJPG_UNDEFINED_HUFF_TABLE,
|
|
||||||
PJPG_NOT_SINGLE_SCAN,
|
|
||||||
PJPG_UNSUPPORTED_COLORSPACE,
|
|
||||||
PJPG_UNSUPPORTED_SAMP_FACTORS,
|
|
||||||
PJPG_DECODE_ERROR,
|
|
||||||
PJPG_BAD_RESTART_MARKER,
|
|
||||||
PJPG_ASSERTION_ERROR,
|
|
||||||
PJPG_BAD_SOS_SPECTRAL,
|
|
||||||
PJPG_BAD_SOS_SUCCESSIVE,
|
|
||||||
PJPG_STREAM_READ_ERROR,
|
|
||||||
PJPG_NOTENOUGHMEM,
|
|
||||||
PJPG_UNSUPPORTED_COMP_IDENT,
|
|
||||||
PJPG_UNSUPPORTED_QUANT_TABLE,
|
|
||||||
PJPG_UNSUPPORTED_MODE, // picojpeg doesn't support progressive JPEG's
|
|
||||||
};
|
|
||||||
|
|
||||||
// Scan types
|
|
||||||
typedef enum { PJPG_GRAYSCALE, PJPG_YH1V1, PJPG_YH2V1, PJPG_YH1V2, PJPG_YH2V2 } pjpeg_scan_type_t;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
// Image resolution
|
|
||||||
int m_width;
|
|
||||||
int m_height;
|
|
||||||
|
|
||||||
// Number of components (1 or 3)
|
|
||||||
int m_comps;
|
|
||||||
|
|
||||||
// Total number of minimum coded units (MCU's) per row/col.
|
|
||||||
int m_MCUSPerRow;
|
|
||||||
int m_MCUSPerCol;
|
|
||||||
|
|
||||||
// Scan type
|
|
||||||
pjpeg_scan_type_t m_scanType;
|
|
||||||
|
|
||||||
// MCU width/height in pixels (each is either 8 or 16 depending on the scan type)
|
|
||||||
int m_MCUWidth;
|
|
||||||
int m_MCUHeight;
|
|
||||||
|
|
||||||
// m_pMCUBufR, m_pMCUBufG, and m_pMCUBufB are pointers to internal MCU Y or RGB pixel component buffers.
|
|
||||||
// Each time pjpegDecodeMCU() is called successfully these buffers will be filled with 8x8 pixel blocks of Y or RGB
|
|
||||||
// pixels. Each MCU consists of (m_MCUWidth/8)*(m_MCUHeight/8) Y/RGB blocks: 1 for greyscale/no subsampling, 2 for
|
|
||||||
// H1V2/H2V1, or 4 blocks for H2V2 sampling factors. Each block is a contiguous array of 64 (8x8) bytes of a single
|
|
||||||
// component: either Y for grayscale images, or R, G or B components for color images.
|
|
||||||
//
|
|
||||||
// The 8x8 pixel blocks are organized in these byte arrays like this:
|
|
||||||
//
|
|
||||||
// PJPG_GRAYSCALE: Each MCU is decoded to a single block of 8x8 grayscale pixels.
|
|
||||||
// Only the values in m_pMCUBufR are valid. Each 8 bytes is a row of pixels (raster order: left to right, top to
|
|
||||||
// bottom) from the 8x8 block.
|
|
||||||
//
|
|
||||||
// PJPG_H1V1: Each MCU contains is decoded to a single block of 8x8 RGB pixels.
|
|
||||||
//
|
|
||||||
// PJPG_YH2V1: Each MCU is decoded to 2 blocks, or 16x8 pixels.
|
|
||||||
// The 2 RGB blocks are at byte offsets: 0, 64
|
|
||||||
//
|
|
||||||
// PJPG_YH1V2: Each MCU is decoded to 2 blocks, or 8x16 pixels.
|
|
||||||
// The 2 RGB blocks are at byte offsets: 0,
|
|
||||||
// 128
|
|
||||||
//
|
|
||||||
// PJPG_YH2V2: Each MCU is decoded to 4 blocks, or 16x16 pixels.
|
|
||||||
// The 2x2 block array is organized at byte offsets: 0, 64,
|
|
||||||
// 128, 192
|
|
||||||
//
|
|
||||||
// It is up to the caller to copy or blit these pixels from these buffers into the destination bitmap.
|
|
||||||
unsigned char* m_pMCUBufR;
|
|
||||||
unsigned char* m_pMCUBufG;
|
|
||||||
unsigned char* m_pMCUBufB;
|
|
||||||
} pjpeg_image_info_t;
|
|
||||||
|
|
||||||
typedef unsigned char (*pjpeg_need_bytes_callback_t)(unsigned char* pBuf, unsigned char buf_size,
|
|
||||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
|
||||||
|
|
||||||
// Initializes the decompressor. Returns 0 on success, or one of the above error codes on failure.
|
|
||||||
// pNeed_bytes_callback will be called to fill the decompressor's internal input buffer.
|
|
||||||
// If reduce is 1, only the first pixel of each block will be decoded. This mode is much faster because it skips the AC
|
|
||||||
// dequantization, IDCT and chroma upsampling of every image pixel. Not thread safe.
|
|
||||||
unsigned char pjpeg_decode_init(pjpeg_image_info_t* pInfo, pjpeg_need_bytes_callback_t pNeed_bytes_callback,
|
|
||||||
void* pCallback_data, unsigned char reduce);
|
|
||||||
|
|
||||||
// Decompresses the file's next MCU. Returns 0 on success, PJPG_NO_MORE_BLOCKS if no more blocks are available, or an
|
|
||||||
// error code. Must be called a total of m_MCUSPerRow*m_MCUSPerCol times to completely decompress the image. Not thread
|
|
||||||
// safe.
|
|
||||||
unsigned char pjpeg_decode_mcu(void);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif // PICOJPEG_H
|
|
||||||
Reference in New Issue
Block a user