diff --git a/.gitignore b/.gitignore index f4bbcdfce..719fb43dd 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ build .history/ /.venv *.local* +/plans \ No newline at end of file diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index cb0b18017..fe4963bd1 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -611,7 +611,7 @@ bool Epub::generateCoverBmp(bool cropped) const { std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; } std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; } -bool Epub::generateThumbBmp(int height) const { +bool Epub::generateThumbBmp(int height, uint32_t deadline) const { // Already generated, return true if (Storage.exists(getThumbBmpPath(height).c_str())) { return true; @@ -633,9 +633,15 @@ bool Epub::generateThumbBmp(int height) const { if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { return false; } - readItemContentsToStream(coverImageHref, coverJpg, 1024); + const bool extractedJpg = readItemContentsToStream(coverImageHref, coverJpg, 1024, deadline); coverJpg.close(); + if (!extractedJpg) { + LOG_ERR("EBP", "Failed to extract JPG cover (deadline or error)"); + Storage.remove(coverJpgTempPath.c_str()); + return false; + } + if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { return false; } @@ -650,7 +656,7 @@ bool Epub::generateThumbBmp(int height) const { int THUMB_TARGET_WIDTH = height * 0.6; int THUMB_TARGET_HEIGHT = height; const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH, - THUMB_TARGET_HEIGHT); + THUMB_TARGET_HEIGHT, deadline); coverJpg.close(); thumbBmp.close(); Storage.remove(coverJpgTempPath.c_str()); @@ -669,9 +675,15 @@ bool Epub::generateThumbBmp(int height) const { if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) { return false; } - readItemContentsToStream(coverImageHref, coverPng, 1024); + const bool extractedPng = readItemContentsToStream(coverImageHref, coverPng, 1024, deadline); coverPng.close(); + if (!extractedPng) { + LOG_ERR("EBP", "Failed to extract PNG cover (deadline or error)"); + Storage.remove(coverPngTempPath.c_str()); + return false; + } + if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) { return false; } @@ -683,8 +695,8 @@ bool Epub::generateThumbBmp(int height) const { } int THUMB_TARGET_WIDTH = height * 0.6; int THUMB_TARGET_HEIGHT = height; - const bool success = - PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT); + const bool success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, + THUMB_TARGET_HEIGHT, deadline); coverPng.close(); thumbBmp.close(); Storage.remove(coverPngTempPath.c_str()); @@ -723,14 +735,15 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size return content; } -bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const { +bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize, + uint32_t deadline) const { if (itemHref.empty()) { LOG_DBG("EBP", "Failed to read item, empty href"); return false; } const std::string path = FsHelpers::normalisePath(itemHref); - return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize); + return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize, deadline); } bool Epub::getItemSize(const std::string& itemHref, size_t* size) const { diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 9ffa8d37c..31508354a 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -55,10 +55,10 @@ class Epub { bool generateCoverBmp(bool cropped = false) const; std::string getThumbBmpPath() const; std::string getThumbBmpPath(int height) const; - bool generateThumbBmp(int height) const; + bool generateThumbBmp(int height, uint32_t deadline = 0) const; uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr, bool trailingNullByte = false) const; - bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const; + bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize, uint32_t deadline = 0) const; bool getItemSize(const std::string& itemHref, size_t* size) const; BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const; BookMetadataCache::TocEntry getTocItem(int tocIndex) const; diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index df91c1a1c..eba56cc7c 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -227,6 +227,13 @@ STR_THEME_CLASSIC: "Classic" STR_THEME_LYRA: "Lyra" STR_THEME_LYRA_EXTENDED: "Lyra Extended" STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix" +STR_HOME_COVER: "Home cover" +STR_COVER_ENABLED: "Enabled" +STR_COVER_TIMEOUT: "Timeout (3s)" +STR_COVER_DISABLED: "Disabled" +STR_COVER_ACTION_DISABLE: "Disable cover" +STR_COVER_ACTION_ENABLE: "Enable cover" +STR_COVER_ACTION_GENERATE: "Generate cover" STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons" STR_OPDS_BROWSER: "OPDS Browser" STR_COVER_CUSTOM: "Cover + Custom" diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index bdc368aba..dba1bc2a7 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -1,22 +1,15 @@ #include "JpegToBmpConverter.h" #include +#include #include -#include #include #include +#include #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 // ============================================================================ @@ -166,103 +159,299 @@ static void writeBmpHeader2bit(Print& bmpOut, const int width, const int height) } } -// Callback function for picojpeg to read JPEG data -unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const unsigned char buf_size, - unsigned char* pBytes_actually_read, void* pCallback_data) { - auto* context = static_cast(pCallback_data); +namespace { + +// Max MCU height supported by any JPEG (4:2:0 chroma = 16 rows, 4:4:4 = 8 rows) +constexpr int MAX_MCU_HEIGHT = 16; +constexpr size_t JPEG_DECODER_SIZE = 20 * 1024; +constexpr size_t MIN_FREE_HEAP = JPEG_DECODER_SIZE + 32 * 1024; + +// 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(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(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(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; + uint32_t deadline; +}; - if (!context || !context->file) { - return PJPG_STREAM_READ_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 - if (context->bufferPos >= context->bufferFilled) { - context->bufferFilled = context->file.read(context->buffer, sizeof(context->buffer)); - context->bufferPos = 0; + ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow); +} - if (context->bufferFilled == 0) { - // EOF or error - *pBytes_actually_read = 0; - return 0; // Success (EOF is normal) +// Flush one scaled output row from Y-axis accumulators and advance currentOutY +static void flushScaledRow(BmpConvertCtx* ctx) { + memset(ctx->bmpRow, 0, ctx->bytesPerRow); + + if (USE_8BIT_OUTPUT && !ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; + ctx->bmpRow[x] = adjustPixel(gray); + } + } else if (ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; + const uint8_t bit = ctx->atkinson1BitDitherer ? ctx->atkinson1BitDitherer->processPixel(gray, x) + : quantize1bit(gray, x, ctx->currentOutY); + ctx->bmpRow[x / 8] |= (bit << (7 - (x % 8))); } + if (ctx->atkinson1BitDitherer) ctx->atkinson1BitDitherer->nextRow(); + } else { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = adjustPixel((ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0); + uint8_t twoBit; + if (ctx->atkinsonDitherer) { + twoBit = ctx->atkinsonDitherer->processPixel(gray, x); + } else if (ctx->fsDitherer) { + twoBit = ctx->fsDitherer->processPixel(gray, x); + } else { + twoBit = quantize(gray, x, ctx->currentOutY); + } + ctx->bmpRow[(x * 2) / 8] |= (twoBit << (6 - ((x * 2) % 8))); + } + if (ctx->atkinsonDitherer) + ctx->atkinsonDitherer->nextRow(); + else if (ctx->fsDitherer) + ctx->fsDitherer->nextRow(); } - // Copy available bytes to picojpeg's buffer - const size_t available = context->bufferFilled - context->bufferPos; - const size_t toRead = available < buf_size ? available : buf_size; + ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow); + ctx->currentOutY++; +} - memcpy(pBuf, context->buffer + context->bufferPos, toRead); - context->bufferPos += toRead; - *pBytes_actually_read = static_cast(toRead); +// 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(pDraw->pUser); + if (!ctx || ctx->error) return 0; + + if (ctx->deadline != 0 && static_cast(millis() - ctx->deadline) >= 0) { + LOG_ERR("JPG", "Decode deadline exceeded at MCU row %d", pDraw->y); + ctx->error = true; + return 0; + } + + const uint8_t* pixels = reinterpret_cast(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); + } - return 0; // Success + // 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(outX) * ctx->scaleX_fp) >> 16; + const int srcXEnd = (static_cast(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(y + 1) << 16; + while (srcY_fp >= ctx->nextOutY_srcStart && ctx->currentOutY < ctx->outHeight) { + flushScaledRow(ctx); + ctx->nextOutY_srcStart = static_cast(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)); + } + } + } + + return ctx->error ? 0 : 1; } +} // namespace + // Internal implementation with configurable target size and bit depth bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, - bool oneBit, bool crop) { + bool oneBit, bool crop, uint32_t deadline) { LOG_DBG("JPG", "Converting JPEG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight); - // Setup context for picojpeg callback - JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0}; + if (ESP.getFreeHeap() < MIN_FREE_HEAP) { + LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", ESP.getFreeHeap(), MIN_FREE_HEAP); + return false; + } - // 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); + s_jpegFile = &jpegFile; + + JPEGDEC* jpeg = new (std::nothrow) JPEGDEC(); + if (!jpeg) { + LOG_ERR("JPG", "Failed to allocate JPEG decoder"); return false; } - LOG_DBG("JPG", "JPEG dimensions: %dx%d, components: %d, MCUs: %dx%d", imageInfo.m_width, imageInfo.m_height, - imageInfo.m_comps, imageInfo.m_MCUSPerRow, imageInfo.m_MCUSPerCol); + 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_HEIGHT = 3072; - constexpr int MAX_MCU_ROW_BYTES = 65536; - if (imageInfo.m_width > MAX_IMAGE_WIDTH || imageInfo.m_height > MAX_IMAGE_HEIGHT) { - LOG_DBG("JPG", "Image too large (%dx%d), max supported: %dx%d", imageInfo.m_width, imageInfo.m_height, - MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT); + if (srcWidth <= 0 || srcHeight <= 0 || srcWidth > MAX_IMAGE_WIDTH || srcHeight > MAX_IMAGE_HEIGHT) { + LOG_DBG("JPG", "Image too large or invalid (%dx%d), max supported: %dx%d", srcWidth, srcHeight, MAX_IMAGE_WIDTH, + MAX_IMAGE_HEIGHT); + jpeg->close(); + delete jpeg; return false; } // Calculate output dimensions (pre-scale to fit display exactly) - int outWidth = imageInfo.m_width; - int outHeight = imageInfo.m_height; - // Use fixed-point scaling (16.16) for sub-pixel accuracy + int outWidth = srcWidth; + int outHeight = srcHeight; uint32_t scaleX_fp = 65536; // 1.0 in 16.16 fixed point uint32_t scaleY_fp = 65536; bool needsScaling = false; - if (targetWidth > 0 && targetHeight > 0 && (imageInfo.m_width != targetWidth || imageInfo.m_height != targetHeight)) { - // Calculate scale to fit/fill target dimensions while maintaining aspect ratio - const float scaleToFitWidth = static_cast(targetWidth) / imageInfo.m_width; - const float scaleToFitHeight = static_cast(targetHeight) / imageInfo.m_height; - // We scale to the smaller dimension, so we can potentially crop later. - float scale = 1.0; - if (crop) { // if we will crop, scale to the smaller dimension + if (targetWidth > 0 && targetHeight > 0 && (srcWidth != targetWidth || srcHeight != targetHeight)) { + const float scaleToFitWidth = static_cast(targetWidth) / srcWidth; + const float scaleToFitHeight = static_cast(targetHeight) / srcHeight; + float scale = 1.0f; + if (crop) { scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; - } else { // else, scale to the larger dimension to fit + } else { scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; } - outWidth = static_cast(imageInfo.m_width * scale); - outHeight = static_cast(imageInfo.m_height * scale); - - // Ensure at least 1 pixel + outWidth = static_cast(srcWidth * scale); + outHeight = static_cast(srcHeight * scale); if (outWidth < 1) outWidth = 1; if (outHeight < 1) outHeight = 1; - // Calculate fixed-point scale factors (source pixels per output pixel) - // scaleX_fp = (srcWidth << 16) / outWidth - scaleX_fp = (static_cast(imageInfo.m_width) << 16) / outWidth; - scaleY_fp = (static_cast(imageInfo.m_height) << 16) / outHeight; + scaleX_fp = (static_cast(srcWidth) << 16) / outWidth; + scaleY_fp = (static_cast(srcHeight) << 16) / outHeight; needsScaling = true; - LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", imageInfo.m_width, imageInfo.m_height, outWidth, outHeight, - targetWidth, targetHeight); + LOG_DBG("JPG", "Scaling %dx%d -> %dx%d (target %dx%d)", srcWidth, srcHeight, outWidth, outHeight, targetWidth, + targetHeight); } // Write BMP header with output dimensions @@ -272,285 +461,85 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm bytesPerRow = (outWidth + 3) / 4 * 4; } else if (oneBit) { writeBmpHeader1bit(bmpOut, outWidth, outHeight); - bytesPerRow = (outWidth + 31) / 32 * 4; // 1 bit per pixel + bytesPerRow = (outWidth + 31) / 32 * 4; } else { writeBmpHeader2bit(bmpOut, outWidth, outHeight); bytesPerRow = (outWidth * 2 + 31) / 32 * 4; } - uint8_t* rowBuffer = nullptr; - uint8_t* mcuRowBuffer = nullptr; - AtkinsonDitherer* atkinsonDitherer = nullptr; - FloydSteinbergDitherer* fsDitherer = nullptr; - Atkinson1BitDitherer* atkinson1BitDitherer = nullptr; - uint32_t* rowAccum = nullptr; // Accumulator for each output X (32-bit for larger sums) - uint32_t* rowCount = nullptr; // Count of source pixels accumulated per output X - - // RAII guard: frees all heap resources on any return path, including early exits. - // Holds references so it always sees the latest pointer values assigned below. + BmpConvertCtx ctx = {}; + ctx.bmpOut = &bmpOut; + ctx.srcWidth = srcWidth; + ctx.srcHeight = srcHeight; + ctx.outWidth = outWidth; + ctx.outHeight = outHeight; + ctx.oneBit = oneBit; + ctx.bytesPerRow = bytesPerRow; + ctx.needsScaling = needsScaling; + ctx.scaleX_fp = scaleX_fp; + ctx.scaleY_fp = scaleY_fp; + ctx.error = false; + ctx.deadline = deadline; + + // RAII guard: frees all heap resources on any return path struct Cleanup { - uint8_t*& rowBuffer; - uint8_t*& mcuRowBuffer; - AtkinsonDitherer*& atkinsonDitherer; - FloydSteinbergDitherer*& fsDitherer; - Atkinson1BitDitherer*& atkinson1BitDitherer; - uint32_t*& rowAccum; - uint32_t*& rowCount; + BmpConvertCtx& ctx; + JPEGDEC* jpeg; ~Cleanup() { - delete[] rowAccum; - delete[] rowCount; - delete atkinsonDitherer; - delete fsDitherer; - delete atkinson1BitDitherer; - free(mcuRowBuffer); - free(rowBuffer); + delete[] ctx.rowAccum; + delete[] ctx.rowCount; + delete ctx.atkinsonDitherer; + delete ctx.fsDitherer; + delete ctx.atkinson1BitDitherer; + free(ctx.mcuBuf); + free(ctx.bmpRow); + jpeg->close(); + delete jpeg; } - } cleanup{rowBuffer, mcuRowBuffer, atkinsonDitherer, fsDitherer, atkinson1BitDitherer, rowAccum, rowCount}; + } cleanup{ctx, jpeg}; - // Allocate row buffer - rowBuffer = static_cast(malloc(bytesPerRow)); - if (!rowBuffer) { - LOG_ERR("JPG", "Failed to allocate row buffer"); + // MCU row buffer: MAX_MCU_HEIGHT rows × srcWidth columns of grayscale + ctx.mcuBuf = static_cast(malloc(MAX_MCU_HEIGHT * srcWidth)); + if (!ctx.mcuBuf) { + LOG_ERR("JPG", "Failed to allocate MCU buffer (%d bytes)", MAX_MCU_HEIGHT * srcWidth); return false; } + memset(ctx.mcuBuf, 0, MAX_MCU_HEIGHT * srcWidth); - // Allocate a buffer for one MCU row worth of grayscale pixels - // This is the minimal memory needed for streaming conversion - const int mcuPixelHeight = imageInfo.m_MCUHeight; - 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); + ctx.bmpRow = static_cast(malloc(bytesPerRow)); + if (!ctx.bmpRow) { + LOG_ERR("JPG", "Failed to allocate BMP row buffer"); return false; } - mcuRowBuffer = static_cast(malloc(mcuRowPixels)); - if (!mcuRowBuffer) { - LOG_ERR("JPG", "Failed to allocate MCU row buffer (%d bytes)", mcuRowPixels); - return false; + if (needsScaling) { + ctx.rowAccum = new (std::nothrow) uint32_t[outWidth](); + ctx.rowCount = new (std::nothrow) uint32_t[outWidth](); + if (!ctx.rowAccum || !ctx.rowCount) { + LOG_ERR("JPG", "Failed to allocate scaling buffers"); + return false; + } + ctx.nextOutY_srcStart = scaleY_fp; } - // 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); + ctx.atkinson1BitDitherer = new (std::nothrow) Atkinson1BitDitherer(outWidth); } else if (!USE_8BIT_OUTPUT) { if (USE_ATKINSON) { - atkinsonDitherer = new AtkinsonDitherer(outWidth); + ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(outWidth); } else if (USE_FLOYD_STEINBERG) { - fsDitherer = new FloydSteinbergDitherer(outWidth); + ctx.fsDitherer = new (std::nothrow) 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) { - rowAccum = new uint32_t[outWidth](); - rowCount = new uint32_t[outWidth](); - nextOutY_srcStart = scaleY_fp; // First boundary is at scaleY_fp (source Y for outY=1) - } + jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); + jpeg->setUserPointer(&ctx); - // Process MCUs row-by-row and write to BMP as we go (top-down) - const int mcuPixelWidth = imageInfo.m_MCUWidth; - - for (int mcuY = 0; mcuY < imageInfo.m_MCUSPerCol; mcuY++) { - // Clear the MCU row buffer - memset(mcuRowBuffer, 0, mcuRowPixels); - - // 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; - } + rc = jpeg->decode(0, 0, 0); - // 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 - const int startRow = mcuY * mcuPixelHeight; - const int endRow = (mcuY + 1) * mcuPixelHeight; - - for (int y = startRow; y < endRow && y < imageInfo.m_height; y++) { - const int bufferY = y - startRow; - - if (!needsScaling) { - // No scaling - direct output (1:1 mapping) - memset(rowBuffer, 0, bytesPerRow); - - 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(outX) * scaleX_fp) >> 16; - const int srcXEnd = (static_cast(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(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(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)); - } - } - } + if (rc != 1 || ctx.error) { + LOG_ERR("JPG", "JPEG decode failed (rc=%d, err=%d)", rc, jpeg->getLastError()); + return false; } LOG_DBG("JPG", "Successfully converted JPEG to BMP"); @@ -570,6 +559,6 @@ bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bm // Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, - int targetMaxHeight) { - return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true); + int targetMaxHeight, uint32_t deadline) { + return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true, deadline); } diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.h b/lib/JpegToBmpConverter/JpegToBmpConverter.h index 125692e46..13257fdd6 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.h +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.h @@ -6,15 +6,14 @@ class Print; class ZipFile; 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, - bool oneBit, bool crop = true); + bool oneBit, bool crop = true, uint32_t deadline = 0); public: static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop = true); // Convert with custom target size (for thumbnails) static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight); // Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering - static bool jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight); + static bool jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight, + uint32_t deadline = 0); }; diff --git a/lib/PngToBmpConverter/PngToBmpConverter.cpp b/lib/PngToBmpConverter/PngToBmpConverter.cpp index 875b46bd8..95915ab38 100644 --- a/lib/PngToBmpConverter/PngToBmpConverter.cpp +++ b/lib/PngToBmpConverter/PngToBmpConverter.cpp @@ -397,7 +397,7 @@ static void convertScanlineToGray(const PngDecodeContext& ctx, uint8_t* grayRow) } bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, - bool oneBit, bool crop) { + bool oneBit, bool crop, uint32_t deadline) { LOG_DBG("PNG", "Converting PNG to %s BMP (target: %dx%d)", oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight); // Verify PNG signature @@ -667,6 +667,12 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu // Process each scanline for (uint32_t y = 0; y < height; y++) { + if (deadline != 0 && static_cast(millis() - deadline) >= 0) { + LOG_ERR("PNG", "Decode deadline exceeded at scanline %u", y); + success = false; + break; + } + // Decode one scanline if (!decodeScanline(ctx)) { LOG_ERR("PNG", "Failed to decode scanline %u", y); @@ -831,6 +837,6 @@ bool PngToBmpConverter::pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOu } bool PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, - int targetMaxHeight) { - return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true); + int targetMaxHeight, uint32_t deadline) { + return pngFileToBmpStreamInternal(pngFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true, deadline); } diff --git a/lib/PngToBmpConverter/PngToBmpConverter.h b/lib/PngToBmpConverter/PngToBmpConverter.h index bf9d3a2c6..16d733b63 100644 --- a/lib/PngToBmpConverter/PngToBmpConverter.h +++ b/lib/PngToBmpConverter/PngToBmpConverter.h @@ -6,10 +6,11 @@ class Print; class PngToBmpConverter { static bool pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOut, int targetWidth, int targetHeight, bool oneBit, - bool crop = true); + bool crop = true, uint32_t deadline = 0); public: static bool pngFileToBmpStream(FsFile& pngFile, Print& bmpOut, bool crop = true); static bool pngFileToBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight); - static bool pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight); + static bool pngFileTo1BitBmpStreamWithSize(FsFile& pngFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight, + uint32_t deadline = 0); }; diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 58fa64d5a..4d4188dd2 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -493,7 +493,7 @@ uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const boo return data; } -bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) { +bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize, uint32_t deadline) { const bool wasOpen = isOpen(); if (!wasOpen && !open()) { return false; @@ -501,11 +501,17 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch FileStatSlim fileStat = {}; if (!loadFileStatSlim(filename, &fileStat)) { + if (!wasOpen) { + close(); + } return false; } const long fileOffset = getDataOffset(fileStat); if (fileOffset < 0) { + if (!wasOpen) { + close(); + } return false; } @@ -526,6 +532,14 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch size_t remaining = inflatedDataSize; while (remaining > 0) { + if (deadline != 0 && static_cast(millis() - deadline) >= 0) { + LOG_ERR("ZIP", "Read deadline exceeded (stored)"); + free(buffer); + if (!wasOpen) { + close(); + } + return false; + } const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize); if (dataRead == 0) { LOG_ERR("ZIP", "Could not read more bytes"); @@ -536,7 +550,14 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch return false; } - out.write(buffer, dataRead); + if (out.write(buffer, dataRead) != dataRead) { + LOG_ERR("ZIP", "Failed to write all output bytes to stream (stored)"); + free(buffer); + if (!wasOpen) { + close(); + } + return false; + } remaining -= dataRead; } @@ -588,6 +609,10 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch size_t totalProduced = 0; while (true) { + if (deadline != 0 && static_cast(millis() - deadline) >= 0) { + LOG_ERR("ZIP", "Decompress deadline exceeded after %zu bytes", totalProduced); + break; + } size_t produced; const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced); diff --git a/lib/ZipFile/ZipFile.h b/lib/ZipFile/ZipFile.h index bc97559dd..b8ee33586 100644 --- a/lib/ZipFile/ZipFile.h +++ b/lib/ZipFile/ZipFile.h @@ -68,5 +68,5 @@ class ZipFile { // Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple // These functions will open and close the zip as needed uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false); - bool readFileToStream(const char* filename, Print& out, size_t chunkSize); + bool readFileToStream(const char* filename, Print& out, size_t chunkSize, uint32_t deadline = 0); }; diff --git a/lib/picojpeg/picojpeg.c b/lib/picojpeg/picojpeg.c deleted file mode 100644 index f612b73c0..000000000 --- a/lib/picojpeg/picojpeg.c +++ /dev/null @@ -1,2087 +0,0 @@ -//------------------------------------------------------------------------------ -// picojpeg.c v1.1 - Public domain, Rich Geldreich -// Nov. 27, 2010 - Initial release -// Feb. 9, 2013 - Added H1V2/H2V1 support, cleaned up macros, signed shift fixes -// Also integrated and tested changes from Chris Phoenix . -//------------------------------------------------------------------------------ -#include "picojpeg.h" -//------------------------------------------------------------------------------ -// Set to 1 if right shifts on signed ints are always unsigned (logical) shifts -// When 1, arithmetic right shifts will be emulated by using a logical shift -// with special case code to ensure the sign bit is replicated. -#define PJPG_RIGHT_SHIFT_IS_ALWAYS_UNSIGNED 0 - -// Define PJPG_INLINE to "inline" if your C compiler supports explicit inlining -#define PJPG_INLINE -//------------------------------------------------------------------------------ -typedef unsigned char uint8; -typedef unsigned short uint16; -typedef signed char int8; -typedef signed short int16; -//------------------------------------------------------------------------------ -#if PJPG_RIGHT_SHIFT_IS_ALWAYS_UNSIGNED -static int16 replicateSignBit16(int8 n) { - switch (n) { - case 0: - return 0x0000; - case 1: - return 0x8000; - case 2: - return 0xC000; - case 3: - return 0xE000; - case 4: - return 0xF000; - case 5: - return 0xF800; - case 6: - return 0xFC00; - case 7: - return 0xFE00; - case 8: - return 0xFF00; - case 9: - return 0xFF80; - case 10: - return 0xFFC0; - case 11: - return 0xFFE0; - case 12: - return 0xFFF0; - case 13: - return 0xFFF8; - case 14: - return 0xFFFC; - case 15: - return 0xFFFE; - default: - return 0xFFFF; - } -} -static PJPG_INLINE int16 arithmeticRightShiftN16(int16 x, int8 n) { - int16 r = (uint16)x >> (uint8)n; - if (x < 0) r |= replicateSignBit16(n); - return r; -} -static PJPG_INLINE long arithmeticRightShift8L(long x) { - long r = (unsigned long)x >> 8U; - if (x < 0) r |= ~(~(unsigned long)0U >> 8U); - return r; -} -#define PJPG_ARITH_SHIFT_RIGHT_N_16(x, n) arithmeticRightShiftN16(x, n) -#define PJPG_ARITH_SHIFT_RIGHT_8_L(x) arithmeticRightShift8L(x) -#else -#define PJPG_ARITH_SHIFT_RIGHT_N_16(x, n) ((x) >> (n)) -#define PJPG_ARITH_SHIFT_RIGHT_8_L(x) ((x) >> 8) -#endif -//------------------------------------------------------------------------------ -// Change as needed - the PJPG_MAX_WIDTH/PJPG_MAX_HEIGHT checks are only present -// to quickly detect bogus files. -#define PJPG_MAX_WIDTH 16384 -#define PJPG_MAX_HEIGHT 16384 -#define PJPG_MAXCOMPSINSCAN 3 -//------------------------------------------------------------------------------ -typedef enum { - M_SOF0 = 0xC0, - M_SOF1 = 0xC1, - M_SOF2 = 0xC2, - M_SOF3 = 0xC3, - - M_SOF5 = 0xC5, - M_SOF6 = 0xC6, - M_SOF7 = 0xC7, - - M_JPG = 0xC8, - M_SOF9 = 0xC9, - M_SOF10 = 0xCA, - M_SOF11 = 0xCB, - - M_SOF13 = 0xCD, - M_SOF14 = 0xCE, - M_SOF15 = 0xCF, - - M_DHT = 0xC4, - - M_DAC = 0xCC, - - M_RST0 = 0xD0, - M_RST1 = 0xD1, - M_RST2 = 0xD2, - M_RST3 = 0xD3, - M_RST4 = 0xD4, - M_RST5 = 0xD5, - M_RST6 = 0xD6, - M_RST7 = 0xD7, - - M_SOI = 0xD8, - M_EOI = 0xD9, - M_SOS = 0xDA, - M_DQT = 0xDB, - M_DNL = 0xDC, - M_DRI = 0xDD, - M_DHP = 0xDE, - M_EXP = 0xDF, - - M_APP0 = 0xE0, - M_APP15 = 0xEF, - - M_JPG0 = 0xF0, - M_JPG13 = 0xFD, - M_COM = 0xFE, - - M_TEM = 0x01, - - M_ERROR = 0x100, - - RST0 = 0xD0 -} JPEG_MARKER; -//------------------------------------------------------------------------------ -static const int8 ZAG[] = { - 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, - 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, - 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, -}; -//------------------------------------------------------------------------------ -// 128 bytes -static int16 gCoeffBuf[8 * 8]; - -// 8*8*4 bytes * 3 = 768 -static uint8 gMCUBufR[256]; -static uint8 gMCUBufG[256]; -static uint8 gMCUBufB[256]; - -// 256 bytes -static int16 gQuant0[8 * 8]; -static int16 gQuant1[8 * 8]; - -// 6 bytes -static int16 gLastDC[3]; - -typedef struct HuffTableT { - uint16 mMinCode[16]; - uint16 mMaxCode[16]; - uint8 mValPtr[16]; -} HuffTable; - -// DC - 192 -static HuffTable gHuffTab0; - -static uint8 gHuffVal0[16]; - -static HuffTable gHuffTab1; -static uint8 gHuffVal1[16]; - -// AC - 672 -static HuffTable gHuffTab2; -static uint8 gHuffVal2[256]; - -static HuffTable gHuffTab3; -static uint8 gHuffVal3[256]; - -static uint8 gValidHuffTables; -static uint8 gValidQuantTables; - -static uint8 gTemFlag; -#define PJPG_MAX_IN_BUF_SIZE 256 -static uint8 gInBuf[PJPG_MAX_IN_BUF_SIZE]; -static uint8 gInBufOfs; -static uint8 gInBufLeft; - -static uint16 gBitBuf; -static uint8 gBitsLeft; -//------------------------------------------------------------------------------ -static uint16 gImageXSize; -static uint16 gImageYSize; -static uint8 gCompsInFrame; -static uint8 gCompIdent[3]; -static uint8 gCompHSamp[3]; -static uint8 gCompVSamp[3]; -static uint8 gCompQuant[3]; - -static uint16 gRestartInterval; -static uint16 gNextRestartNum; -static uint16 gRestartsLeft; - -static uint8 gCompsInScan; -static uint8 gCompList[3]; -static uint8 gCompDCTab[3]; // 0,1 -static uint8 gCompACTab[3]; // 0,1 - -static pjpeg_scan_type_t gScanType; - -static uint8 gMaxBlocksPerMCU; -static uint8 gMaxMCUXSize; -static uint8 gMaxMCUYSize; -static uint16 gMaxMCUSPerRow; -static uint16 gMaxMCUSPerCol; - -static uint16 gNumMCUSRemainingX, gNumMCUSRemainingY; - -static uint8 gMCUOrg[6]; - -static pjpeg_need_bytes_callback_t g_pNeedBytesCallback; -static void* g_pCallback_data; -static uint8 gCallbackStatus; -static uint8 gReduce; -//------------------------------------------------------------------------------ -static void fillInBuf(void) { - unsigned char status; - - // Reserve a few bytes at the beginning of the buffer for putting back ("stuffing") chars. - gInBufOfs = 4; - gInBufLeft = 0; - - status = (*g_pNeedBytesCallback)(gInBuf + gInBufOfs, PJPG_MAX_IN_BUF_SIZE - gInBufOfs, &gInBufLeft, g_pCallback_data); - if (status) { - // The user provided need bytes callback has indicated an error, so record the error and continue trying to decode. - // The highest level pjpeg entrypoints will catch the error and return the non-zero status. - gCallbackStatus = status; - } -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 getChar(void) { - if (!gInBufLeft) { - fillInBuf(); - if (!gInBufLeft) { - gTemFlag = ~gTemFlag; - return gTemFlag ? 0xFF : 0xD9; - } - } - - gInBufLeft--; - return gInBuf[gInBufOfs++]; -} -//------------------------------------------------------------------------------ -static PJPG_INLINE void stuffChar(uint8 i) { - gInBufOfs--; - gInBuf[gInBufOfs] = i; - gInBufLeft++; -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 getOctet(uint8 FFCheck) { - uint8 c = getChar(); - - if ((FFCheck) && (c == 0xFF)) { - uint8 n = getChar(); - - if (n) { - stuffChar(n); - stuffChar(0xFF); - } - } - - return c; -} -//------------------------------------------------------------------------------ -static uint16 getBits(uint8 numBits, uint8 FFCheck) { - uint8 origBits = numBits; - uint16 ret = gBitBuf; - - if (numBits > 8) { - numBits -= 8; - - gBitBuf <<= gBitsLeft; - - gBitBuf |= getOctet(FFCheck); - - gBitBuf <<= (8 - gBitsLeft); - - ret = (ret & 0xFF00) | (gBitBuf >> 8); - } - - if (gBitsLeft < numBits) { - gBitBuf <<= gBitsLeft; - - gBitBuf |= getOctet(FFCheck); - - gBitBuf <<= (numBits - gBitsLeft); - - gBitsLeft = 8 - (numBits - gBitsLeft); - } else { - gBitsLeft = (uint8)(gBitsLeft - numBits); - gBitBuf <<= numBits; - } - - return ret >> (16 - origBits); -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint16 getBits1(uint8 numBits) { return getBits(numBits, 0); } -//------------------------------------------------------------------------------ -static PJPG_INLINE uint16 getBits2(uint8 numBits) { return getBits(numBits, 1); } -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 getBit(void) { - uint8 ret = 0; - if (gBitBuf & 0x8000) ret = 1; - - if (!gBitsLeft) { - gBitBuf |= getOctet(1); - - gBitsLeft += 8; - } - - gBitsLeft--; - gBitBuf <<= 1; - - return ret; -} -//------------------------------------------------------------------------------ -static uint16 getExtendTest(uint8 i) { - switch (i) { - case 0: - return 0; - case 1: - return 0x0001; - case 2: - return 0x0002; - case 3: - return 0x0004; - case 4: - return 0x0008; - case 5: - return 0x0010; - case 6: - return 0x0020; - case 7: - return 0x0040; - case 8: - return 0x0080; - case 9: - return 0x0100; - case 10: - return 0x0200; - case 11: - return 0x0400; - case 12: - return 0x0800; - case 13: - return 0x1000; - case 14: - return 0x2000; - case 15: - return 0x4000; - default: - return 0; - } -} -//------------------------------------------------------------------------------ -static int16 getExtendOffset(uint8 i) { - switch (i) { - case 0: - return 0; - case 1: - return ((-1) << 1) + 1; - case 2: - return ((-1) << 2) + 1; - case 3: - return ((-1) << 3) + 1; - case 4: - return ((-1) << 4) + 1; - case 5: - return ((-1) << 5) + 1; - case 6: - return ((-1) << 6) + 1; - case 7: - return ((-1) << 7) + 1; - case 8: - return ((-1) << 8) + 1; - case 9: - return ((-1) << 9) + 1; - case 10: - return ((-1) << 10) + 1; - case 11: - return ((-1) << 11) + 1; - case 12: - return ((-1) << 12) + 1; - case 13: - return ((-1) << 13) + 1; - case 14: - return ((-1) << 14) + 1; - case 15: - return ((-1) << 15) + 1; - default: - return 0; - } -}; -//------------------------------------------------------------------------------ -static PJPG_INLINE int16 huffExtend(uint16 x, uint8 s) { - return ((x < getExtendTest(s)) ? ((int16)x + getExtendOffset(s)) : (int16)x); -} -//------------------------------------------------------------------------------ -static PJPG_INLINE uint8 huffDecode(const HuffTable* pHuffTable, const uint8* pHuffVal) { - uint8 i = 0; - uint8 j; - uint16 code = getBit(); - - // This func only reads a bit at a time, which on modern CPU's is not terribly efficient. - // But on microcontrollers without strong integer shifting support this seems like a - // more reasonable approach. - for (;;) { - uint16 maxCode; - - if (i == 16) return 0; - - maxCode = pHuffTable->mMaxCode[i]; - if ((code <= maxCode) && (maxCode != 0xFFFF)) break; - - i++; - code <<= 1; - code |= getBit(); - } - - j = pHuffTable->mValPtr[i]; - j = (uint8)(j + (code - pHuffTable->mMinCode[i])); - - return pHuffVal[j]; -} -//------------------------------------------------------------------------------ -static void huffCreate(const uint8* pBits, HuffTable* pHuffTable) { - uint8 i = 0; - uint8 j = 0; - - uint16 code = 0; - - for (;;) { - uint8 num = pBits[i]; - - if (!num) { - pHuffTable->mMinCode[i] = 0x0000; - pHuffTable->mMaxCode[i] = 0xFFFF; - pHuffTable->mValPtr[i] = 0; - } else { - pHuffTable->mMinCode[i] = code; - pHuffTable->mMaxCode[i] = code + num - 1; - pHuffTable->mValPtr[i] = j; - - j = (uint8)(j + num); - - code = (uint16)(code + num); - } - - code <<= 1; - - i++; - if (i > 15) break; - } -} -//------------------------------------------------------------------------------ -static HuffTable* getHuffTable(uint8 index) { - // 0-1 = DC - // 2-3 = AC - switch (index) { - case 0: - return &gHuffTab0; - case 1: - return &gHuffTab1; - case 2: - return &gHuffTab2; - case 3: - return &gHuffTab3; - default: - return 0; - } -} -//------------------------------------------------------------------------------ -static uint8* getHuffVal(uint8 index) { - // 0-1 = DC - // 2-3 = AC - switch (index) { - case 0: - return gHuffVal0; - case 1: - return gHuffVal1; - case 2: - return gHuffVal2; - case 3: - return gHuffVal3; - default: - return 0; - } -} -//------------------------------------------------------------------------------ -static uint16 getMaxHuffCodes(uint8 index) { return (index < 2) ? 12 : 255; } -//------------------------------------------------------------------------------ -static uint8 readDHTMarker(void) { - uint8 bits[16]; - uint16 left = getBits1(16); - - if (left < 2) return PJPG_BAD_DHT_MARKER; - - left -= 2; - - while (left) { - uint8 i, tableIndex, index; - uint8* pHuffVal; - HuffTable* pHuffTable; - uint16 count, totalRead; - - index = (uint8)getBits1(8); - - if (((index & 0xF) > 1) || ((index & 0xF0) > 0x10)) return PJPG_BAD_DHT_INDEX; - - tableIndex = ((index >> 3) & 2) + (index & 1); - - pHuffTable = getHuffTable(tableIndex); - pHuffVal = getHuffVal(tableIndex); - - gValidHuffTables |= (1 << tableIndex); - - count = 0; - for (i = 0; i <= 15; i++) { - uint8 n = (uint8)getBits1(8); - bits[i] = n; - count = (uint16)(count + n); - } - - if (count > getMaxHuffCodes(tableIndex)) return PJPG_BAD_DHT_COUNTS; - - for (i = 0; i < count; i++) pHuffVal[i] = (uint8)getBits1(8); - - totalRead = 1 + 16 + count; - - if (left < totalRead) return PJPG_BAD_DHT_MARKER; - - left = (uint16)(left - totalRead); - - huffCreate(bits, pHuffTable); - } - - return 0; -} -//------------------------------------------------------------------------------ -static void createWinogradQuant(int16* pQuant); - -static uint8 readDQTMarker(void) { - uint16 left = getBits1(16); - - if (left < 2) return PJPG_BAD_DQT_MARKER; - - left -= 2; - - while (left) { - uint8 i; - uint8 n = (uint8)getBits1(8); - uint8 prec = n >> 4; - uint16 totalRead; - - n &= 0x0F; - - if (n > 1) return PJPG_BAD_DQT_TABLE; - - gValidQuantTables |= (n ? 2 : 1); - - // read quantization entries, in zag order - for (i = 0; i < 64; i++) { - uint16 temp = getBits1(8); - - if (prec) temp = (temp << 8) + getBits1(8); - - if (n) - gQuant1[i] = (int16)temp; - else - gQuant0[i] = (int16)temp; - } - - createWinogradQuant(n ? gQuant1 : gQuant0); - - totalRead = 64 + 1; - - if (prec) totalRead += 64; - - if (left < totalRead) return PJPG_BAD_DQT_LENGTH; - - left = (uint16)(left - totalRead); - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 readSOFMarker(void) { - uint8 i; - uint16 left = getBits1(16); - - if (getBits1(8) != 8) return PJPG_BAD_PRECISION; - - gImageYSize = getBits1(16); - - if ((!gImageYSize) || (gImageYSize > PJPG_MAX_HEIGHT)) return PJPG_BAD_HEIGHT; - - gImageXSize = getBits1(16); - - if ((!gImageXSize) || (gImageXSize > PJPG_MAX_WIDTH)) return PJPG_BAD_WIDTH; - - gCompsInFrame = (uint8)getBits1(8); - - if (gCompsInFrame > 3) return PJPG_TOO_MANY_COMPONENTS; - - if (left != (gCompsInFrame + gCompsInFrame + gCompsInFrame + 8)) return PJPG_BAD_SOF_LENGTH; - - for (i = 0; i < gCompsInFrame; i++) { - gCompIdent[i] = (uint8)getBits1(8); - gCompHSamp[i] = (uint8)getBits1(4); - gCompVSamp[i] = (uint8)getBits1(4); - gCompQuant[i] = (uint8)getBits1(8); - - if (gCompQuant[i] > 1) return PJPG_UNSUPPORTED_QUANT_TABLE; - } - - return 0; -} -//------------------------------------------------------------------------------ -// Used to skip unrecognized markers. -static uint8 skipVariableMarker(void) { - uint16 left = getBits1(16); - - if (left < 2) return PJPG_BAD_VARIABLE_MARKER; - - left -= 2; - - while (left) { - getBits1(8); - left--; - } - - return 0; -} -//------------------------------------------------------------------------------ -// Read a define restart interval (DRI) marker. -static uint8 readDRIMarker(void) { - if (getBits1(16) != 4) return PJPG_BAD_DRI_LENGTH; - - gRestartInterval = getBits1(16); - - return 0; -} -//------------------------------------------------------------------------------ -// Read a start of scan (SOS) marker. -static uint8 readSOSMarker(void) { - uint8 i; - uint16 left = getBits1(16); - uint8 spectral_start, spectral_end, successive_high, successive_low; - - gCompsInScan = (uint8)getBits1(8); - - left -= 3; - - if ((left != (gCompsInScan + gCompsInScan + 3)) || (gCompsInScan < 1) || (gCompsInScan > PJPG_MAXCOMPSINSCAN)) - return PJPG_BAD_SOS_LENGTH; - - for (i = 0; i < gCompsInScan; i++) { - uint8 cc = (uint8)getBits1(8); - uint8 c = (uint8)getBits1(8); - uint8 ci; - - left -= 2; - - for (ci = 0; ci < gCompsInFrame; ci++) - if (cc == gCompIdent[ci]) break; - - if (ci >= gCompsInFrame) return PJPG_BAD_SOS_COMP_ID; - - gCompList[i] = ci; - gCompDCTab[ci] = (c >> 4) & 15; - gCompACTab[ci] = (c & 15); - } - - spectral_start = (uint8)getBits1(8); - spectral_end = (uint8)getBits1(8); - successive_high = (uint8)getBits1(4); - successive_low = (uint8)getBits1(4); - - left -= 3; - - while (left) { - getBits1(8); - left--; - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 nextMarker(void) { - uint8 c; - uint8 bytes = 0; - - do { - do { - bytes++; - - c = (uint8)getBits1(8); - - } while (c != 0xFF); - - do { - c = (uint8)getBits1(8); - - } while (c == 0xFF); - - } while (c == 0); - - // If bytes > 0 here, there where extra bytes before the marker (not good). - - return c; -} -//------------------------------------------------------------------------------ -// Process markers. Returns when an SOFx, SOI, EOI, or SOS marker is -// encountered. -static uint8 processMarkers(uint8* pMarker) { - for (;;) { - uint8 c = nextMarker(); - - switch (c) { - case M_SOF0: - case M_SOF1: - case M_SOF2: - case M_SOF3: - case M_SOF5: - case M_SOF6: - case M_SOF7: - // case M_JPG: - case M_SOF9: - case M_SOF10: - case M_SOF11: - case M_SOF13: - case M_SOF14: - case M_SOF15: - case M_SOI: - case M_EOI: - case M_SOS: { - *pMarker = c; - return 0; - } - case M_DHT: { - readDHTMarker(); - break; - } - // Sorry, no arithmetic support at this time. Dumb patents! - case M_DAC: { - return PJPG_NO_ARITHMITIC_SUPPORT; - } - case M_DQT: { - readDQTMarker(); - break; - } - case M_DRI: { - readDRIMarker(); - break; - } - // case M_APP0: /* no need to read the JFIF marker */ - - case M_JPG: - case M_RST0: /* no parameters */ - case M_RST1: - case M_RST2: - case M_RST3: - case M_RST4: - case M_RST5: - case M_RST6: - case M_RST7: - case M_TEM: { - return PJPG_UNEXPECTED_MARKER; - } - default: /* must be DNL, DHP, EXP, APPn, JPGn, COM, or RESn or APP0 */ - { - skipVariableMarker(); - break; - } - } - } - // return 0; -} -//------------------------------------------------------------------------------ -// Finds the start of image (SOI) marker. -static uint8 locateSOIMarker(void) { - uint16 bytesleft; - - uint8 lastchar = (uint8)getBits1(8); - - uint8 thischar = (uint8)getBits1(8); - - /* ok if it's a normal JPEG file without a special header */ - - if ((lastchar == 0xFF) && (thischar == M_SOI)) return 0; - - bytesleft = 4096; // 512; - - for (;;) { - if (--bytesleft == 0) return PJPG_NOT_JPEG; - - lastchar = thischar; - - thischar = (uint8)getBits1(8); - - if (lastchar == 0xFF) { - if (thischar == M_SOI) - break; - else if (thischar == M_EOI) // getBits1 will keep returning M_EOI if we read past the end - return PJPG_NOT_JPEG; - } - } - - /* Check the next character after marker: if it's not 0xFF, it can't - be the start of the next marker, so the file is bad */ - - thischar = (uint8)((gBitBuf >> 8) & 0xFF); - - if (thischar != 0xFF) return PJPG_NOT_JPEG; - - return 0; -} -//------------------------------------------------------------------------------ -// Find a start of frame (SOF) marker. -static uint8 locateSOFMarker(void) { - uint8 c; - - uint8 status = locateSOIMarker(); - if (status) return status; - - status = processMarkers(&c); - if (status) return status; - - switch (c) { - case M_SOF2: { - // Progressive JPEG - not supported by picojpeg (would require too - // much memory, or too many IDCT's for embedded systems). - return PJPG_UNSUPPORTED_MODE; - } - case M_SOF0: /* baseline DCT */ - { - status = readSOFMarker(); - if (status) return status; - - break; - } - case M_SOF9: { - return PJPG_NO_ARITHMITIC_SUPPORT; - } - case M_SOF1: /* extended sequential DCT */ - default: { - return PJPG_UNSUPPORTED_MARKER; - } - } - - return 0; -} -//------------------------------------------------------------------------------ -// Find a start of scan (SOS) marker. -static uint8 locateSOSMarker(uint8* pFoundEOI) { - uint8 c; - uint8 status; - - *pFoundEOI = 0; - - status = processMarkers(&c); - if (status) return status; - - if (c == M_EOI) { - *pFoundEOI = 1; - return 0; - } else if (c != M_SOS) - return PJPG_UNEXPECTED_MARKER; - - return readSOSMarker(); -} -//------------------------------------------------------------------------------ -static uint8 init(void) { - gImageXSize = 0; - gImageYSize = 0; - gCompsInFrame = 0; - gRestartInterval = 0; - gCompsInScan = 0; - gValidHuffTables = 0; - gValidQuantTables = 0; - gTemFlag = 0; - gInBufOfs = 0; - gInBufLeft = 0; - gBitBuf = 0; - gBitsLeft = 8; - - getBits1(8); - getBits1(8); - - return 0; -} -//------------------------------------------------------------------------------ -// This method throws back into the stream any bytes that where read -// into the bit buffer during initial marker scanning. -static void fixInBuffer(void) { - /* In case any 0xFF's where pulled into the buffer during marker scanning */ - - if (gBitsLeft > 0) stuffChar((uint8)gBitBuf); - - stuffChar((uint8)(gBitBuf >> 8)); - - gBitsLeft = 8; - getBits2(8); - getBits2(8); -} -//------------------------------------------------------------------------------ -// Restart interval processing. -static uint8 processRestart(void) { - // Let's scan a little bit to find the marker, but not _too_ far. - // 1536 is a "fudge factor" that determines how much to scan. - uint16 i; - uint8 c = 0; - - for (i = 1536; i > 0; i--) - if (getChar() == 0xFF) break; - - if (i == 0) return PJPG_BAD_RESTART_MARKER; - - for (; i > 0; i--) - if ((c = getChar()) != 0xFF) break; - - if (i == 0) return PJPG_BAD_RESTART_MARKER; - - // Is it the expected marker? If not, something bad happened. - if (c != (gNextRestartNum + M_RST0)) return PJPG_BAD_RESTART_MARKER; - - // Reset each component's DC prediction values. - gLastDC[0] = 0; - gLastDC[1] = 0; - gLastDC[2] = 0; - - gRestartsLeft = gRestartInterval; - - gNextRestartNum = (gNextRestartNum + 1) & 7; - - // Get the bit buffer going again - - gBitsLeft = 8; - getBits2(8); - getBits2(8); - - return 0; -} -//------------------------------------------------------------------------------ -// FIXME: findEOI() is not actually called at the end of the image -// (it's optional, and probably not needed on embedded devices) -static uint8 findEOI(void) { - uint8 c; - uint8 status; - - // Prime the bit buffer - gBitsLeft = 8; - getBits1(8); - getBits1(8); - - // The next marker _should_ be EOI - status = processMarkers(&c); - if (status) - return status; - else if (gCallbackStatus) - return gCallbackStatus; - - // gTotalBytesRead -= in_buf_left; - if (c != M_EOI) return PJPG_UNEXPECTED_MARKER; - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 checkHuffTables(void) { - uint8 i; - - for (i = 0; i < gCompsInScan; i++) { - uint8 compDCTab = gCompDCTab[gCompList[i]]; - uint8 compACTab = gCompACTab[gCompList[i]] + 2; - - if (((gValidHuffTables & (1 << compDCTab)) == 0) || ((gValidHuffTables & (1 << compACTab)) == 0)) - return PJPG_UNDEFINED_HUFF_TABLE; - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 checkQuantTables(void) { - uint8 i; - - for (i = 0; i < gCompsInScan; i++) { - uint8 compQuantMask = gCompQuant[gCompList[i]] ? 2 : 1; - - if ((gValidQuantTables & compQuantMask) == 0) return PJPG_UNDEFINED_QUANT_TABLE; - } - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 initScan(void) { - uint8 foundEOI; - uint8 status = locateSOSMarker(&foundEOI); - if (status) return status; - if (foundEOI) return PJPG_UNEXPECTED_MARKER; - - status = checkHuffTables(); - if (status) return status; - - status = checkQuantTables(); - if (status) return status; - - gLastDC[0] = 0; - gLastDC[1] = 0; - gLastDC[2] = 0; - - if (gRestartInterval) { - gRestartsLeft = gRestartInterval; - gNextRestartNum = 0; - } - - fixInBuffer(); - - return 0; -} -//------------------------------------------------------------------------------ -static uint8 initFrame(void) { - if (gCompsInFrame == 1) { - if ((gCompHSamp[0] != 1) || (gCompVSamp[0] != 1)) return PJPG_UNSUPPORTED_SAMP_FACTORS; - - gScanType = PJPG_GRAYSCALE; - - gMaxBlocksPerMCU = 1; - gMCUOrg[0] = 0; - - gMaxMCUXSize = 8; - gMaxMCUYSize = 8; - } else if (gCompsInFrame == 3) { - if (((gCompHSamp[1] != 1) || (gCompVSamp[1] != 1)) || ((gCompHSamp[2] != 1) || (gCompVSamp[2] != 1))) - return PJPG_UNSUPPORTED_SAMP_FACTORS; - - if ((gCompHSamp[0] == 1) && (gCompVSamp[0] == 1)) { - gScanType = PJPG_YH1V1; - - gMaxBlocksPerMCU = 3; - gMCUOrg[0] = 0; - gMCUOrg[1] = 1; - gMCUOrg[2] = 2; - - gMaxMCUXSize = 8; - gMaxMCUYSize = 8; - } else if ((gCompHSamp[0] == 1) && (gCompVSamp[0] == 2)) { - gScanType = PJPG_YH1V2; - - gMaxBlocksPerMCU = 4; - gMCUOrg[0] = 0; - gMCUOrg[1] = 0; - gMCUOrg[2] = 1; - gMCUOrg[3] = 2; - - gMaxMCUXSize = 8; - gMaxMCUYSize = 16; - } else if ((gCompHSamp[0] == 2) && (gCompVSamp[0] == 1)) { - gScanType = PJPG_YH2V1; - - gMaxBlocksPerMCU = 4; - gMCUOrg[0] = 0; - gMCUOrg[1] = 0; - gMCUOrg[2] = 1; - gMCUOrg[3] = 2; - - gMaxMCUXSize = 16; - gMaxMCUYSize = 8; - } else if ((gCompHSamp[0] == 2) && (gCompVSamp[0] == 2)) { - gScanType = PJPG_YH2V2; - - gMaxBlocksPerMCU = 6; - gMCUOrg[0] = 0; - gMCUOrg[1] = 0; - gMCUOrg[2] = 0; - gMCUOrg[3] = 0; - gMCUOrg[4] = 1; - gMCUOrg[5] = 2; - - gMaxMCUXSize = 16; - gMaxMCUYSize = 16; - } else - return PJPG_UNSUPPORTED_SAMP_FACTORS; - } else - return PJPG_UNSUPPORTED_COLORSPACE; - - gMaxMCUSPerRow = (gImageXSize + (gMaxMCUXSize - 1)) >> ((gMaxMCUXSize == 8) ? 3 : 4); - gMaxMCUSPerCol = (gImageYSize + (gMaxMCUYSize - 1)) >> ((gMaxMCUYSize == 8) ? 3 : 4); - - // This can overflow on large JPEG's. - // gNumMCUSRemaining = gMaxMCUSPerRow * gMaxMCUSPerCol; - gNumMCUSRemainingX = gMaxMCUSPerRow; - gNumMCUSRemainingY = gMaxMCUSPerCol; - - return 0; -} -//---------------------------------------------------------------------------- -// Winograd IDCT: 5 multiplies per row/col, up to 80 muls for the 2D IDCT - -#define PJPG_DCT_SCALE_BITS 7 - -#define PJPG_DCT_SCALE (1U << PJPG_DCT_SCALE_BITS) - -#define PJPG_DESCALE(x) PJPG_ARITH_SHIFT_RIGHT_N_16(((x) + (1 << (PJPG_DCT_SCALE_BITS - 1))), PJPG_DCT_SCALE_BITS) - -#define PJPG_WFIX(x) ((x) * PJPG_DCT_SCALE + 0.5f) - -#define PJPG_WINOGRAD_QUANT_SCALE_BITS 10 - -const uint8 gWinogradQuant[] = { - 128, 178, 178, 167, 246, 167, 151, 232, 232, 151, 128, 209, 219, 209, 128, 101, 178, 197, 197, 178, 101, 69, - 139, 167, 177, 167, 139, 69, 35, 96, 131, 151, 151, 131, 96, 35, 49, 91, 118, 128, 118, 91, 49, 46, - 81, 101, 101, 81, 46, 42, 69, 79, 69, 42, 35, 54, 54, 35, 28, 37, 28, 19, 19, 10, -}; - -// Multiply quantization matrix by the Winograd IDCT scale factors -static void createWinogradQuant(int16* pQuant) { - uint8 i; - - for (i = 0; i < 64; i++) { - long x = pQuant[i]; - x *= gWinogradQuant[i]; - pQuant[i] = (int16)((x + (1 << (PJPG_WINOGRAD_QUANT_SCALE_BITS - PJPG_DCT_SCALE_BITS - 1))) >> - (PJPG_WINOGRAD_QUANT_SCALE_BITS - PJPG_DCT_SCALE_BITS)); - } -} - -// These multiply helper functions are the 4 types of signed multiplies needed by the Winograd IDCT. -// A smart C compiler will optimize them to use 16x8 = 24 bit muls, if not you may need to tweak -// these functions or drop to CPU specific inline assembly. - -// 1/cos(4*pi/16) -// 362, 256+106 -static PJPG_INLINE int16 imul_b1_b3(int16 w) { - long x = (w * 362L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -// 1/cos(6*pi/16) -// 669, 256+256+157 -static PJPG_INLINE int16 imul_b2(int16 w) { - long x = (w * 669L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -// 1/cos(2*pi/16) -// 277, 256+21 -static PJPG_INLINE int16 imul_b4(int16 w) { - long x = (w * 277L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -// 1/(cos(2*pi/16) + cos(6*pi/16)) -// 196, 196 -static PJPG_INLINE int16 imul_b5(int16 w) { - long x = (w * 196L); - x += 128L; - return (int16)(PJPG_ARITH_SHIFT_RIGHT_8_L(x)); -} - -static PJPG_INLINE uint8 clamp(int16 s) { - if ((uint16)s > 255U) { - if (s < 0) - return 0; - else if (s > 255) - return 255; - } - - return (uint8)s; -} - -static void idctRows(void) { - uint8 i; - int16* pSrc = gCoeffBuf; - - for (i = 0; i < 8; i++) { - if ((pSrc[1] | pSrc[2] | pSrc[3] | pSrc[4] | pSrc[5] | pSrc[6] | pSrc[7]) == 0) { - // Short circuit the 1D IDCT if only the DC component is non-zero - int16 src0 = *pSrc; - - *(pSrc + 1) = src0; - *(pSrc + 2) = src0; - *(pSrc + 3) = src0; - *(pSrc + 4) = src0; - *(pSrc + 5) = src0; - *(pSrc + 6) = src0; - *(pSrc + 7) = src0; - } else { - int16 src4 = *(pSrc + 5); - int16 src7 = *(pSrc + 3); - int16 x4 = src4 - src7; - int16 x7 = src4 + src7; - - int16 src5 = *(pSrc + 1); - int16 src6 = *(pSrc + 7); - int16 x5 = src5 + src6; - int16 x6 = src5 - src6; - - int16 tmp1 = imul_b5(x4 - x6); - int16 stg26 = imul_b4(x6) - tmp1; - - int16 x24 = tmp1 - imul_b2(x4); - - int16 x15 = x5 - x7; - int16 x17 = x5 + x7; - - int16 tmp2 = stg26 - x17; - int16 tmp3 = imul_b1_b3(x15) - tmp2; - int16 x44 = tmp3 + x24; - - int16 src0 = *(pSrc + 0); - int16 src1 = *(pSrc + 4); - int16 x30 = src0 + src1; - int16 x31 = src0 - src1; - - int16 src2 = *(pSrc + 2); - int16 src3 = *(pSrc + 6); - int16 x12 = src2 - src3; - int16 x13 = src2 + src3; - - int16 x32 = imul_b1_b3(x12) - x13; - - int16 x40 = x30 + x13; - int16 x43 = x30 - x13; - int16 x41 = x31 + x32; - int16 x42 = x31 - x32; - - *(pSrc + 0) = x40 + x17; - *(pSrc + 1) = x41 + tmp2; - *(pSrc + 2) = x42 + tmp3; - *(pSrc + 3) = x43 - x44; - *(pSrc + 4) = x43 + x44; - *(pSrc + 5) = x42 - tmp3; - *(pSrc + 6) = x41 - tmp2; - *(pSrc + 7) = x40 - x17; - } - - pSrc += 8; - } -} - -static void idctCols(void) { - uint8 i; - - int16* pSrc = gCoeffBuf; - - for (i = 0; i < 8; i++) { - if ((pSrc[1 * 8] | pSrc[2 * 8] | pSrc[3 * 8] | pSrc[4 * 8] | pSrc[5 * 8] | pSrc[6 * 8] | pSrc[7 * 8]) == 0) { - // Short circuit the 1D IDCT if only the DC component is non-zero - uint8 c = clamp(PJPG_DESCALE(*pSrc) + 128); - *(pSrc + 0 * 8) = c; - *(pSrc + 1 * 8) = c; - *(pSrc + 2 * 8) = c; - *(pSrc + 3 * 8) = c; - *(pSrc + 4 * 8) = c; - *(pSrc + 5 * 8) = c; - *(pSrc + 6 * 8) = c; - *(pSrc + 7 * 8) = c; - } else { - int16 src4 = *(pSrc + 5 * 8); - int16 src7 = *(pSrc + 3 * 8); - int16 x4 = src4 - src7; - int16 x7 = src4 + src7; - - int16 src5 = *(pSrc + 1 * 8); - int16 src6 = *(pSrc + 7 * 8); - int16 x5 = src5 + src6; - int16 x6 = src5 - src6; - - int16 tmp1 = imul_b5(x4 - x6); - int16 stg26 = imul_b4(x6) - tmp1; - - int16 x24 = tmp1 - imul_b2(x4); - - int16 x15 = x5 - x7; - int16 x17 = x5 + x7; - - int16 tmp2 = stg26 - x17; - int16 tmp3 = imul_b1_b3(x15) - tmp2; - int16 x44 = tmp3 + x24; - - int16 src0 = *(pSrc + 0 * 8); - int16 src1 = *(pSrc + 4 * 8); - int16 x30 = src0 + src1; - int16 x31 = src0 - src1; - - int16 src2 = *(pSrc + 2 * 8); - int16 src3 = *(pSrc + 6 * 8); - int16 x12 = src2 - src3; - int16 x13 = src2 + src3; - - int16 x32 = imul_b1_b3(x12) - x13; - - int16 x40 = x30 + x13; - int16 x43 = x30 - x13; - int16 x41 = x31 + x32; - int16 x42 = x31 - x32; - - // descale, convert to unsigned and clamp to 8-bit - *(pSrc + 0 * 8) = clamp(PJPG_DESCALE(x40 + x17) + 128); - *(pSrc + 1 * 8) = clamp(PJPG_DESCALE(x41 + tmp2) + 128); - *(pSrc + 2 * 8) = clamp(PJPG_DESCALE(x42 + tmp3) + 128); - *(pSrc + 3 * 8) = clamp(PJPG_DESCALE(x43 - x44) + 128); - *(pSrc + 4 * 8) = clamp(PJPG_DESCALE(x43 + x44) + 128); - *(pSrc + 5 * 8) = clamp(PJPG_DESCALE(x42 - tmp3) + 128); - *(pSrc + 6 * 8) = clamp(PJPG_DESCALE(x41 - tmp2) + 128); - *(pSrc + 7 * 8) = clamp(PJPG_DESCALE(x40 - x17) + 128); - } - - pSrc++; - } -} - -/*----------------------------------------------------------------------------*/ -static PJPG_INLINE uint8 addAndClamp(uint8 a, int16 b) { - b = a + b; - - if ((uint16)b > 255U) { - if (b < 0) - return 0; - else if (b > 255) - return 255; - } - - return (uint8)b; -} -/*----------------------------------------------------------------------------*/ -static PJPG_INLINE uint8 subAndClamp(uint8 a, int16 b) { - b = a - b; - - if ((uint16)b > 255U) { - if (b < 0) - return 0; - else if (b > 255) - return 255; - } - - return (uint8)b; -} -/*----------------------------------------------------------------------------*/ -// 103/256 -// R = Y + 1.402 (Cr-128) - -// 88/256, 183/256 -// G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) - -// 198/256 -// B = Y + 1.772 (Cb-128) -/*----------------------------------------------------------------------------*/ -// Cb upsample and accumulate, 4x4 to 8x8 -static void upsampleCb(uint8 srcOfs, uint8 dstOfs) { - // Cb - affects G and B - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 4; x++) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - pDstG[0] = subAndClamp(pDstG[0], cbG); - pDstG[1] = subAndClamp(pDstG[1], cbG); - pDstG[8] = subAndClamp(pDstG[8], cbG); - pDstG[9] = subAndClamp(pDstG[9], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - pDstB[0] = addAndClamp(pDstB[0], cbB); - pDstB[1] = addAndClamp(pDstB[1], cbB); - pDstB[8] = addAndClamp(pDstB[8], cbB); - pDstB[9] = addAndClamp(pDstB[9], cbB); - - pDstG += 2; - pDstB += 2; - } - - pSrc = pSrc - 4 + 8; - pDstG = pDstG - 8 + 16; - pDstB = pDstB - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// Cb upsample and accumulate, 4x8 to 8x8 -static void upsampleCbH(uint8 srcOfs, uint8 dstOfs) { - // Cb - affects G and B - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - for (y = 0; y < 8; y++) { - for (x = 0; x < 4; x++) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - pDstG[0] = subAndClamp(pDstG[0], cbG); - pDstG[1] = subAndClamp(pDstG[1], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - pDstB[0] = addAndClamp(pDstB[0], cbB); - pDstB[1] = addAndClamp(pDstB[1], cbB); - - pDstG += 2; - pDstB += 2; - } - - pSrc = pSrc - 4 + 8; - } -} -/*----------------------------------------------------------------------------*/ -// Cb upsample and accumulate, 8x4 to 8x8 -static void upsampleCbV(uint8 srcOfs, uint8 dstOfs) { - // Cb - affects G and B - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 8; x++) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - pDstG[0] = subAndClamp(pDstG[0], cbG); - pDstG[8] = subAndClamp(pDstG[8], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - pDstB[0] = addAndClamp(pDstB[0], cbB); - pDstB[8] = addAndClamp(pDstB[8], cbB); - - ++pDstG; - ++pDstB; - } - - pDstG = pDstG - 8 + 16; - pDstB = pDstB - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// 103/256 -// R = Y + 1.402 (Cr-128) - -// 88/256, 183/256 -// G = Y - 0.34414 (Cb-128) - 0.71414 (Cr-128) - -// 198/256 -// B = Y + 1.772 (Cb-128) -/*----------------------------------------------------------------------------*/ -// Cr upsample and accumulate, 4x4 to 8x8 -static void upsampleCr(uint8 srcOfs, uint8 dstOfs) { - // Cr - affects R and G - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 4; x++) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - pDstR[0] = addAndClamp(pDstR[0], crR); - pDstR[1] = addAndClamp(pDstR[1], crR); - pDstR[8] = addAndClamp(pDstR[8], crR); - pDstR[9] = addAndClamp(pDstR[9], crR); - - crG = ((cr * 183U) >> 8U) - 91; - pDstG[0] = subAndClamp(pDstG[0], crG); - pDstG[1] = subAndClamp(pDstG[1], crG); - pDstG[8] = subAndClamp(pDstG[8], crG); - pDstG[9] = subAndClamp(pDstG[9], crG); - - pDstR += 2; - pDstG += 2; - } - - pSrc = pSrc - 4 + 8; - pDstR = pDstR - 8 + 16; - pDstG = pDstG - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// Cr upsample and accumulate, 4x8 to 8x8 -static void upsampleCrH(uint8 srcOfs, uint8 dstOfs) { - // Cr - affects R and G - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - for (y = 0; y < 8; y++) { - for (x = 0; x < 4; x++) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - pDstR[0] = addAndClamp(pDstR[0], crR); - pDstR[1] = addAndClamp(pDstR[1], crR); - - crG = ((cr * 183U) >> 8U) - 91; - pDstG[0] = subAndClamp(pDstG[0], crG); - pDstG[1] = subAndClamp(pDstG[1], crG); - - pDstR += 2; - pDstG += 2; - } - - pSrc = pSrc - 4 + 8; - } -} -/*----------------------------------------------------------------------------*/ -// Cr upsample and accumulate, 8x4 to 8x8 -static void upsampleCrV(uint8 srcOfs, uint8 dstOfs) { - // Cr - affects R and G - uint8 x, y; - int16* pSrc = gCoeffBuf + srcOfs; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - for (y = 0; y < 4; y++) { - for (x = 0; x < 8; x++) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - pDstR[0] = addAndClamp(pDstR[0], crR); - pDstR[8] = addAndClamp(pDstR[8], crR); - - crG = ((cr * 183U) >> 8U) - 91; - pDstG[0] = subAndClamp(pDstG[0], crG); - pDstG[8] = subAndClamp(pDstG[8], crG); - - ++pDstR; - ++pDstG; - } - - pDstR = pDstR - 8 + 16; - pDstG = pDstG - 8 + 16; - } -} -/*----------------------------------------------------------------------------*/ -// Convert Y to RGB -static void copyY(uint8 dstOfs) { - uint8 i; - uint8* pRDst = gMCUBufR + dstOfs; - uint8* pGDst = gMCUBufG + dstOfs; - uint8* pBDst = gMCUBufB + dstOfs; - int16* pSrc = gCoeffBuf; - - for (i = 64; i > 0; i--) { - uint8 c = (uint8)*pSrc++; - - *pRDst++ = c; - *pGDst++ = c; - *pBDst++ = c; - } -} -/*----------------------------------------------------------------------------*/ -// Cb convert to RGB and accumulate -static void convertCb(uint8 dstOfs) { - uint8 i; - uint8* pDstG = gMCUBufG + dstOfs; - uint8* pDstB = gMCUBufB + dstOfs; - int16* pSrc = gCoeffBuf; - - for (i = 64; i > 0; i--) { - uint8 cb = (uint8)*pSrc++; - int16 cbG, cbB; - - cbG = ((cb * 88U) >> 8U) - 44U; - *pDstG++ = subAndClamp(pDstG[0], cbG); - - cbB = (cb + ((cb * 198U) >> 8U)) - 227U; - *pDstB++ = addAndClamp(pDstB[0], cbB); - } -} -/*----------------------------------------------------------------------------*/ -// Cr convert to RGB and accumulate -static void convertCr(uint8 dstOfs) { - uint8 i; - uint8* pDstR = gMCUBufR + dstOfs; - uint8* pDstG = gMCUBufG + dstOfs; - int16* pSrc = gCoeffBuf; - - for (i = 64; i > 0; i--) { - uint8 cr = (uint8)*pSrc++; - int16 crR, crG; - - crR = (cr + ((cr * 103U) >> 8U)) - 179; - *pDstR++ = addAndClamp(pDstR[0], crR); - - crG = ((cr * 183U) >> 8U) - 91; - *pDstG++ = subAndClamp(pDstG[0], crG); - } -} -/*----------------------------------------------------------------------------*/ -static void transformBlock(uint8 mcuBlock) { - idctRows(); - idctCols(); - - switch (gScanType) { - case PJPG_GRAYSCALE: { - // MCU size: 1, 1 block per MCU - copyY(0); - break; - } - case PJPG_YH1V1: { - // MCU size: 8x8, 3 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - convertCb(0); - break; - } - case 2: { - convertCr(0); - break; - } - } - - break; - } - case PJPG_YH1V2: { - // MCU size: 8x16, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - copyY(128); - break; - } - case 2: { - upsampleCbV(0, 0); - upsampleCbV(4 * 8, 128); - break; - } - case 3: { - upsampleCrV(0, 0); - upsampleCrV(4 * 8, 128); - break; - } - } - - break; - } - case PJPG_YH2V1: { - // MCU size: 16x8, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - copyY(64); - break; - } - case 2: { - upsampleCbH(0, 0); - upsampleCbH(4, 64); - break; - } - case 3: { - upsampleCrH(0, 0); - upsampleCrH(4, 64); - break; - } - } - - break; - } - case PJPG_YH2V2: { - // MCU size: 16x16, 6 blocks per MCU - switch (mcuBlock) { - case 0: { - copyY(0); - break; - } - case 1: { - copyY(64); - break; - } - case 2: { - copyY(128); - break; - } - case 3: { - copyY(192); - break; - } - case 4: { - upsampleCb(0, 0); - upsampleCb(4, 64); - upsampleCb(4 * 8, 128); - upsampleCb(4 + 4 * 8, 192); - break; - } - case 5: { - upsampleCr(0, 0); - upsampleCr(4, 64); - upsampleCr(4 * 8, 128); - upsampleCr(4 + 4 * 8, 192); - break; - } - } - - break; - } - } -} -//------------------------------------------------------------------------------ -static void transformBlockReduce(uint8 mcuBlock) { - uint8 c = clamp(PJPG_DESCALE(gCoeffBuf[0]) + 128); - int16 cbG, cbB, crR, crG; - - switch (gScanType) { - case PJPG_GRAYSCALE: { - // MCU size: 1, 1 block per MCU - gMCUBufR[0] = c; - break; - } - case PJPG_YH1V1: { - // MCU size: 8x8, 3 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - break; - } - case 2: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - break; - } - } - - break; - } - case PJPG_YH1V2: { - // MCU size: 8x16, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - gMCUBufR[128] = c; - gMCUBufG[128] = c; - gMCUBufB[128] = c; - break; - } - case 2: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - gMCUBufB[128] = addAndClamp(gMCUBufB[128], cbB); - - break; - } - case 3: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - gMCUBufR[128] = addAndClamp(gMCUBufR[128], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], crG); - - break; - } - } - break; - } - case PJPG_YH2V1: { - // MCU size: 16x8, 4 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - gMCUBufR[64] = c; - gMCUBufG[64] = c; - gMCUBufB[64] = c; - break; - } - case 2: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - gMCUBufB[64] = addAndClamp(gMCUBufB[64], cbB); - - break; - } - case 3: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - gMCUBufR[64] = addAndClamp(gMCUBufR[64], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], crG); - - break; - } - } - break; - } - case PJPG_YH2V2: { - // MCU size: 16x16, 6 blocks per MCU - switch (mcuBlock) { - case 0: { - gMCUBufR[0] = c; - gMCUBufG[0] = c; - gMCUBufB[0] = c; - break; - } - case 1: { - gMCUBufR[64] = c; - gMCUBufG[64] = c; - gMCUBufB[64] = c; - break; - } - case 2: { - gMCUBufR[128] = c; - gMCUBufG[128] = c; - gMCUBufB[128] = c; - break; - } - case 3: { - gMCUBufR[192] = c; - gMCUBufG[192] = c; - gMCUBufB[192] = c; - break; - } - case 4: { - cbG = ((c * 88U) >> 8U) - 44U; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], cbG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], cbG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], cbG); - gMCUBufG[192] = subAndClamp(gMCUBufG[192], cbG); - - cbB = (c + ((c * 198U) >> 8U)) - 227U; - gMCUBufB[0] = addAndClamp(gMCUBufB[0], cbB); - gMCUBufB[64] = addAndClamp(gMCUBufB[64], cbB); - gMCUBufB[128] = addAndClamp(gMCUBufB[128], cbB); - gMCUBufB[192] = addAndClamp(gMCUBufB[192], cbB); - - break; - } - case 5: { - crR = (c + ((c * 103U) >> 8U)) - 179; - gMCUBufR[0] = addAndClamp(gMCUBufR[0], crR); - gMCUBufR[64] = addAndClamp(gMCUBufR[64], crR); - gMCUBufR[128] = addAndClamp(gMCUBufR[128], crR); - gMCUBufR[192] = addAndClamp(gMCUBufR[192], crR); - - crG = ((c * 183U) >> 8U) - 91; - gMCUBufG[0] = subAndClamp(gMCUBufG[0], crG); - gMCUBufG[64] = subAndClamp(gMCUBufG[64], crG); - gMCUBufG[128] = subAndClamp(gMCUBufG[128], crG); - gMCUBufG[192] = subAndClamp(gMCUBufG[192], crG); - - break; - } - } - break; - } - } -} -//------------------------------------------------------------------------------ -static uint8 decodeNextMCU(void) { - uint8 status; - uint8 mcuBlock; - - if (gRestartInterval) { - if (gRestartsLeft == 0) { - status = processRestart(); - if (status) return status; - } - gRestartsLeft--; - } - - for (mcuBlock = 0; mcuBlock < gMaxBlocksPerMCU; mcuBlock++) { - uint8 componentID = gMCUOrg[mcuBlock]; - uint8 compQuant = gCompQuant[componentID]; - uint8 compDCTab = gCompDCTab[componentID]; - uint8 numExtraBits, compACTab, k; - const int16* pQ = compQuant ? gQuant1 : gQuant0; - uint16 r, dc; - - uint8 s = huffDecode(compDCTab ? &gHuffTab1 : &gHuffTab0, compDCTab ? gHuffVal1 : gHuffVal0); - - r = 0; - numExtraBits = s & 0xF; - if (numExtraBits) r = getBits2(numExtraBits); - dc = huffExtend(r, s); - - dc = dc + gLastDC[componentID]; - gLastDC[componentID] = dc; - - gCoeffBuf[0] = dc * pQ[0]; - - compACTab = gCompACTab[componentID]; - - if (gReduce) { - // Decode, but throw out the AC coefficients in reduce mode. - for (k = 1; k < 64; k++) { - s = huffDecode(compACTab ? &gHuffTab3 : &gHuffTab2, compACTab ? gHuffVal3 : gHuffVal2); - - numExtraBits = s & 0xF; - if (numExtraBits) getBits2(numExtraBits); - - r = s >> 4; - s &= 15; - - if (s) { - if (r) { - if ((k + r) > 63) return PJPG_DECODE_ERROR; - - k = (uint8)(k + r); - } - } else { - if (r == 15) { - if ((k + 16) > 64) return PJPG_DECODE_ERROR; - - k += (16 - 1); // - 1 because the loop counter is k - } else - break; - } - } - - transformBlockReduce(mcuBlock); - } else { - // Decode and dequantize AC coefficients - for (k = 1; k < 64; k++) { - uint16 extraBits; - - s = huffDecode(compACTab ? &gHuffTab3 : &gHuffTab2, compACTab ? gHuffVal3 : gHuffVal2); - - extraBits = 0; - numExtraBits = s & 0xF; - if (numExtraBits) extraBits = getBits2(numExtraBits); - - r = s >> 4; - s &= 15; - - if (s) { - int16 ac; - - if (r) { - if ((k + r) > 63) return PJPG_DECODE_ERROR; - - while (r) { - gCoeffBuf[ZAG[k++]] = 0; - r--; - } - } - - ac = huffExtend(extraBits, s); - - gCoeffBuf[ZAG[k]] = ac * pQ[k]; - } else { - if (r == 15) { - if ((k + 16) > 64) return PJPG_DECODE_ERROR; - - for (r = 16; r > 0; r--) gCoeffBuf[ZAG[k++]] = 0; - - k--; // - 1 because the loop counter is k - } else - break; - } - } - - while (k < 64) gCoeffBuf[ZAG[k++]] = 0; - - transformBlock(mcuBlock); - } - } - - return 0; -} -//------------------------------------------------------------------------------ -unsigned char pjpeg_decode_mcu(void) { - uint8 status; - - if (gCallbackStatus) return gCallbackStatus; - - if ((!gNumMCUSRemainingX) && (!gNumMCUSRemainingY)) return PJPG_NO_MORE_BLOCKS; - - status = decodeNextMCU(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - gNumMCUSRemainingX--; - if (!gNumMCUSRemainingX) { - gNumMCUSRemainingY--; - if (gNumMCUSRemainingY > 0) gNumMCUSRemainingX = gMaxMCUSPerRow; - } - - return 0; -} -//------------------------------------------------------------------------------ -unsigned char pjpeg_decode_init(pjpeg_image_info_t* pInfo, pjpeg_need_bytes_callback_t pNeed_bytes_callback, - void* pCallback_data, unsigned char reduce) { - uint8 status; - - pInfo->m_width = 0; - pInfo->m_height = 0; - pInfo->m_comps = 0; - pInfo->m_MCUSPerRow = 0; - pInfo->m_MCUSPerCol = 0; - pInfo->m_scanType = PJPG_GRAYSCALE; - pInfo->m_MCUWidth = 0; - pInfo->m_MCUHeight = 0; - pInfo->m_pMCUBufR = (unsigned char*)0; - pInfo->m_pMCUBufG = (unsigned char*)0; - pInfo->m_pMCUBufB = (unsigned char*)0; - - g_pNeedBytesCallback = pNeed_bytes_callback; - g_pCallback_data = pCallback_data; - gCallbackStatus = 0; - gReduce = reduce; - - status = init(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - status = locateSOFMarker(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - status = initFrame(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - status = initScan(); - if ((status) || (gCallbackStatus)) return gCallbackStatus ? gCallbackStatus : status; - - pInfo->m_width = gImageXSize; - pInfo->m_height = gImageYSize; - pInfo->m_comps = gCompsInFrame; - pInfo->m_scanType = gScanType; - pInfo->m_MCUSPerRow = gMaxMCUSPerRow; - pInfo->m_MCUSPerCol = gMaxMCUSPerCol; - pInfo->m_MCUWidth = gMaxMCUXSize; - pInfo->m_MCUHeight = gMaxMCUYSize; - pInfo->m_pMCUBufR = gMCUBufR; - pInfo->m_pMCUBufG = gMCUBufG; - pInfo->m_pMCUBufB = gMCUBufB; - - return 0; -} diff --git a/lib/picojpeg/picojpeg.h b/lib/picojpeg/picojpeg.h deleted file mode 100644 index 11345fb71..000000000 --- a/lib/picojpeg/picojpeg.h +++ /dev/null @@ -1,124 +0,0 @@ -//------------------------------------------------------------------------------ -// picojpeg - Public domain, Rich Geldreich -//------------------------------------------------------------------------------ -#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 diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 9a0e298b0..3f4e973d3 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -137,6 +137,9 @@ class CrossPointSettings { // Image rendering in EPUB reader enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT }; + // Home screen cover rendering mode + enum COVER_MODE { COVER_ENABLED = 0, COVER_TIMEOUT = 1, COVER_DISABLED_MODE = 2, COVER_MODE_COUNT }; + // Sleep screen settings uint8_t sleepScreen = DARK; // Sleep screen cover mode settings @@ -199,6 +202,8 @@ class CrossPointSettings { uint8_t showHiddenFiles = 0; // Image rendering mode in EPUB reader uint8_t imageRendering = IMAGES_DISPLAY; + // Home screen cover rendering mode + uint8_t coverMode = COVER_ENABLED; ~CrossPointSettings() = default; diff --git a/src/CrossPointState.h b/src/CrossPointState.h index 1de898382..7044e2bf0 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -12,6 +12,8 @@ class CrossPointState { uint8_t lastSleepImage = UINT8_MAX; // UINT8_MAX = unset sentinel uint8_t readerActivityLoadCount = 0; bool lastSleepFromReader = false; + bool pendingCoverGeneration = false; // set on reader exit, consumed by HomeActivity + std::string forceRenderCoverPath; // book path to force-render cover (no timeout); empty = none ~CrossPointState() = default; // Get singleton instance diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index a0458043a..8b0688a8c 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -302,6 +302,7 @@ bool JsonSettingsIO::saveRecentBooks(const RecentBooksStore& store, const char* obj["title"] = book.title; obj["author"] = book.author; obj["coverBmpPath"] = book.coverBmpPath; + obj["coverDisabled"] = book.coverDisabled; } String json; @@ -326,6 +327,7 @@ bool JsonSettingsIO::loadRecentBooks(RecentBooksStore& store, const char* json) book.title = obj["title"] | std::string(""); book.author = obj["author"] | std::string(""); book.coverBmpPath = obj["coverBmpPath"] | std::string(""); + book.coverDisabled = obj["coverDisabled"] | false; store.recentBooks.push_back(book); } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index f5a2c0483..bfe18b518 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -1,5 +1,6 @@ #include "RecentBooksStore.h" +#include #include #include #include @@ -25,12 +26,18 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title // Remove existing entry if present auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); + + // Preserve coverDisabled from the existing entry: generation failures and user preferences + // are tracked there, and must not be reset just because the book was re-opened. + const bool existingDisabled = + (it != recentBooks.end()) ? it->coverDisabled : (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE); + if (it != recentBooks.end()) { recentBooks.erase(it); } // Add to front - recentBooks.insert(recentBooks.begin(), {path, title, author, coverBmpPath}); + recentBooks.insert(recentBooks.begin(), {path, title, author, coverBmpPath, existingDisabled}); // Trim to max size if (recentBooks.size() > MAX_RECENT_BOOKS) { @@ -53,6 +60,15 @@ void RecentBooksStore::updateBook(const std::string& path, const std::string& ti } } +void RecentBooksStore::setCoverDisabled(const std::string& path, bool disabled) { + auto it = + std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); + if (it != recentBooks.end() && it->coverDisabled != disabled) { + it->coverDisabled = disabled; + saveToFile(); + } +} + bool RecentBooksStore::saveToFile() const { Storage.mkdir("/.crosspoint"); return JsonSettingsIO::saveRecentBooks(*this, RECENT_BOOKS_FILE_JSON); diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 5d98ce833..f53b29c08 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -7,6 +7,7 @@ struct RecentBook { std::string title; std::string author; std::string coverBmpPath; + bool coverDisabled = false; bool operator==(const RecentBook& other) const { return path == other.path; } }; @@ -37,6 +38,8 @@ class RecentBooksStore { void updateBook(const std::string& path, const std::string& title, const std::string& author, const std::string& coverBmpPath); + void setCoverDisabled(const std::string& path, bool disabled); + // Get the list of recent books (most recent first) const std::vector& getBooks() const { return recentBooks; } diff --git a/src/SettingsList.h b/src/SettingsList.h index cdbee372c..58116ff73 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -35,6 +35,9 @@ inline const std::vector& getSettingsList() { StrId::STR_CAT_DISPLAY), SettingInfo::Toggle(StrId::STR_SUNLIGHT_FADING_FIX, &CrossPointSettings::fadingFix, "fadingFix", StrId::STR_CAT_DISPLAY), + SettingInfo::Enum(StrId::STR_HOME_COVER, &CrossPointSettings::coverMode, + {StrId::STR_COVER_ENABLED, StrId::STR_COVER_TIMEOUT, StrId::STR_COVER_DISABLED}, "coverMode", + StrId::STR_CAT_DISPLAY), // --- Reader --- SettingInfo::Enum(StrId::STR_FONT_FAMILY, &CrossPointSettings::fontFamily, diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 810cb50bb..e8a10bba6 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -52,57 +52,86 @@ void HomeActivity::loadRecentBooks(int maxBooks) { void HomeActivity::loadRecentCovers(int coverHeight) { recentsLoading = true; - bool showingLoading = false; Rect popupRect; - int progress = 0; - for (RecentBook& book : recentBooks) { - if (!book.coverBmpPath.empty()) { + static constexpr uint32_t COVER_RENDER_TIMEOUT_MS = 3000; + + // Determine if a force-render was requested for the most recent book. + // Force render bypasses both the coverDisabled flag and any timeout deadline. + const bool isForcedBook = !recentBooks.empty() && (recentBooks[0].path == APP_STATE.forceRenderCoverPath); + + // Skip all cover generation if globally disabled, unless force-rendering a specific book + if (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE && !isForcedBook) { + APP_STATE.forceRenderCoverPath = ""; + recentsLoaded = true; + recentsLoading = false; + return; + } + + const bool useTimeout = (SETTINGS.coverMode == CrossPointSettings::COVER_TIMEOUT); + + // Only attempt cover generation for the most recently opened book (recentBooks[0]). + // Other books get their cover generated when the user opens them individually. + // This prevents blocking HOME with multiple simultaneous generations. + if (!recentBooks.empty()) { + RecentBook& book = recentBooks[0]; + if (!book.coverBmpPath.empty() && (!book.coverDisabled || isForcedBook)) { std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); - if (!Storage.exists(coverPath.c_str())) { + if (isForcedBook || !Storage.exists(coverPath.c_str())) { // If epub, try to load the metadata for title/author and cover if (FsHelpers::hasEpubExtension(book.path)) { + // Force-render: delete any stale BMP so generateThumbBmp regenerates it + if (isForcedBook) { + Storage.remove(coverPath.c_str()); + } Epub epub(book.path, "/.crosspoint"); // Skip loading css since we only need metadata here epub.load(false, true); // Try to generate thumbnail image for Continue Reading card - if (!showingLoading) { - showingLoading = true; - popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - } - GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); - bool success = epub.generateThumbBmp(coverHeight); - if (!success) { - RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); - book.coverBmpPath = ""; + popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + GUI.fillPopupProgress(renderer, popupRect, 50); + // Force-render uses no deadline regardless of global mode + const uint32_t deadline = (useTimeout && !isForcedBook) ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; + bool success = epub.generateThumbBmp(coverHeight, deadline); + if (success) { + RECENT_BOOKS.setCoverDisabled(book.path, false); + book.coverDisabled = false; + } else { + // Mark cover as disabled on any failure so HOME won't retry automatically. + // The user can force a retry via Reader Menu → "Generate cover" / "Enable cover". + RECENT_BOOKS.setCoverDisabled(book.path, true); + book.coverDisabled = true; } + // Force full re-render without restoring old buffer (which had the empty template). + // This prevents the cover appearing "behind" the old template on e-ink displays. coverRendered = false; + coverBufferStored = false; requestUpdate(); } else if (FsHelpers::hasXtcExtension(book.path)) { // Handle XTC file Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { - // Try to generate thumbnail image for Continue Reading card - if (!showingLoading) { - showingLoading = true; - popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - } - GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); + popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + GUI.fillPopupProgress(renderer, popupRect, 50); bool success = xtc.generateThumbBmp(coverHeight); - if (!success) { - RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); - book.coverBmpPath = ""; + if (success) { + RECENT_BOOKS.setCoverDisabled(book.path, false); + book.coverDisabled = false; + } else { + RECENT_BOOKS.setCoverDisabled(book.path, true); + book.coverDisabled = true; } coverRendered = false; + coverBufferStored = false; requestUpdate(); } } } } - progress++; } + APP_STATE.forceRenderCoverPath = ""; // consume force-render flag recentsLoaded = true; recentsLoading = false; } @@ -110,9 +139,11 @@ void HomeActivity::loadRecentCovers(int coverHeight) { void HomeActivity::onEnter() { Activity::onEnter(); - // Check if OPDS browser URL is configured hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; + coverRendered = false; + coverBufferStored = false; + selectorIndex = 0; const auto& metrics = UITheme::getInstance().getMetrics(); @@ -253,8 +284,17 @@ void HomeActivity::render(RenderLock&&) { firstRenderDone = true; requestUpdate(); } else if (!recentsLoaded && !recentsLoading) { - recentsLoading = true; - loadRecentCovers(metrics.homeCoverHeight); + // Only generate covers when returning from Reader, not on every HOME entry. + // Also trigger if a force-render was requested via Reader Menu. + recentsLoaded = true; + const bool hasPending = APP_STATE.pendingCoverGeneration; + const bool hasForce = !APP_STATE.forceRenderCoverPath.empty(); + if (hasPending || hasForce) { + APP_STATE.pendingCoverGeneration = false; + recentsLoaded = false; + recentsLoading = true; + loadRecentCovers(metrics.homeCoverHeight); + } } } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 16d403aaa..a266732ae 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -10,6 +10,8 @@ #include #include +#include + #include "CrossPointSettings.h" #include "CrossPointState.h" #include "EpubReaderChapterSelectionActivity.h" @@ -31,6 +33,12 @@ constexpr unsigned long skipChapterMs = 700; // pages per minute, first item is 1 to prevent division by zero if accessed const std::vector PAGE_TURN_LABELS = {1, 1, 3, 6, 12}; +const RecentBook* findRecentBook(const std::string& path) { + const auto& books = RECENT_BOOKS.getBooks(); + const auto it = std::find_if(books.begin(), books.end(), [&path](const RecentBook& rb) { return rb.path == path; }); + return (it != books.end()) ? &*it : nullptr; +} + int clampPercent(int percent) { if (percent < 0) { return 0; @@ -97,6 +105,7 @@ void EpubReaderActivity::onExit() { renderer.setOrientation(GfxRenderer::Orientation::Portrait); APP_STATE.readerActivityLoadCount = 0; + APP_STATE.pendingCoverGeneration = true; APP_STATE.saveToFile(); section.reset(); epub.reset(); @@ -145,9 +154,11 @@ void EpubReaderActivity::loop() { bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); + const RecentBook* recentBook = findRecentBook(epub->getPath()); + const bool bookCoverDisabled = recentBook && recentBook->coverDisabled; startActivityForResult(std::make_unique( renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, - SETTINGS.orientation, !currentPageFootnotes.empty()), + SETTINGS.orientation, !currentPageFootnotes.empty(), bookCoverDisabled), [this](const ActivityResult& result) { // Always apply orientation change even if the menu was cancelled const auto& menu = std::get(result.data); @@ -380,6 +391,34 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction requestUpdate(); break; } + case EpubReaderMenuActivity::MenuAction::COVER_ACTION: { + if (SETTINGS.coverMode == CrossPointSettings::COVER_TIMEOUT) { + // Force-render with no timeout regardless of current coverDisabled state + APP_STATE.forceRenderCoverPath = epub->getPath(); + } else { + // ENABLED or DISABLED: toggle per-book state + const RecentBook* toggleBook = findRecentBook(epub->getPath()); + const bool wasDisabled = toggleBook && toggleBook->coverDisabled; + if (!wasDisabled) { + // Disable: delete BMP and mark disabled + const std::string bmpPath = + UITheme::getCoverThumbPath(epub->getThumbBmpPath(), UITheme::getInstance().getMetrics().homeCoverHeight); + if (!bmpPath.empty() && Storage.exists(bmpPath.c_str())) { + Storage.remove(bmpPath.c_str()); + } + RECENT_BOOKS.setCoverDisabled(epub->getPath(), true); + // Clear any pending force-render so HOME won't regenerate the deleted BMP + if (APP_STATE.forceRenderCoverPath == epub->getPath()) { + APP_STATE.forceRenderCoverPath = ""; + } + } else { + // Enable: force-render (works even in DISABLED global mode) + APP_STATE.forceRenderCoverPath = epub->getPath(); + } + } + requestUpdate(); + break; + } case EpubReaderMenuActivity::MenuAction::SYNC: { if (KOREADER_STORE.hasCredentials()) { const int currentPage = section ? section->currentPage : 0; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 1d95d9b7a..334be2fa8 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -3,6 +3,7 @@ #include #include +#include "CrossPointSettings.h" #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" @@ -10,18 +11,19 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, const uint8_t currentOrientation, - const bool hasFootnotes) + const bool hasFootnotes, const bool bookCoverDisabled) : Activity("EpubReaderMenu", renderer, mappedInput), - menuItems(buildMenuItems(hasFootnotes)), + menuItems(buildMenuItems(hasFootnotes, bookCoverDisabled)), title(title), pendingOrientation(currentOrientation), currentPage(currentPage), totalPages(totalPages), bookProgressPercent(bookProgressPercent) {} -std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { +std::vector EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes, + bool bookCoverDisabled) { std::vector items; - items.reserve(10); + items.reserve(11); items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER}); if (hasFootnotes) { items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES}); @@ -34,6 +36,13 @@ std::vector EpubReaderMenuActivity::buildMenuI items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON}); items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS}); items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE}); + // Cover action: TIMEOUT always shows "Generate cover"; ENABLED/DISABLED toggle per-book state + if (SETTINGS.coverMode == CrossPointSettings::COVER_TIMEOUT) { + items.push_back({MenuAction::COVER_ACTION, StrId::STR_COVER_ACTION_GENERATE}); + } else { + const StrId coverLabel = bookCoverDisabled ? StrId::STR_COVER_ACTION_ENABLE : StrId::STR_COVER_ACTION_DISABLE; + items.push_back({MenuAction::COVER_ACTION, coverLabel}); + } return items; } diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index 9ddba93db..beed57219 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -21,12 +21,14 @@ class EpubReaderMenuActivity final : public Activity { DISPLAY_QR, GO_HOME, SYNC, - DELETE_CACHE + DELETE_CACHE, + COVER_ACTION }; explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, const int currentPage, const int totalPages, const int bookProgressPercent, - const uint8_t currentOrientation, const bool hasFootnotes); + const uint8_t currentOrientation, const bool hasFootnotes, + const bool bookCoverDisabled); void onEnter() override; void onExit() override; @@ -39,7 +41,7 @@ class EpubReaderMenuActivity final : public Activity { StrId labelId; }; - static std::vector buildMenuItems(bool hasFootnotes); + static std::vector buildMenuItems(bool hasFootnotes, bool bookCoverDisabled); // Fixed menu layout const std::vector menuItems; diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 2164a7f66..145442795 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "CrossPointSettings.h" #include "Epub.h" @@ -12,6 +13,7 @@ #include "XtcReaderActivity.h" #include "activities/util/BmpViewerActivity.h" #include "activities/util/FullScreenMessageActivity.h" +#include "components/UITheme.h" std::string ReaderActivity::extractFolderPath(const std::string& filePath) { const auto lastSlash = filePath.find_last_of('/'); @@ -114,27 +116,30 @@ void ReaderActivity::onEnter() { currentBookPath = initialBookPath; if (isBmpFile(initialBookPath)) { onGoToBmpViewer(initialBookPath); - } else if (isXtcFile(initialBookPath)) { - auto xtc = loadXtc(initialBookPath); - if (!xtc) { - onGoBack(); - return; - } - onGoToXtcReader(std::move(xtc)); - } else if (isTxtFile(initialBookPath)) { - auto txt = loadTxt(initialBookPath); - if (!txt) { - onGoBack(); - return; - } - onGoToTxtReader(std::move(txt)); } else { - auto epub = loadEpub(initialBookPath); - if (!epub) { - onGoBack(); - return; + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); + if (isXtcFile(initialBookPath)) { + auto xtc = loadXtc(initialBookPath); + if (!xtc) { + onGoBack(); + return; + } + onGoToXtcReader(std::move(xtc)); + } else if (isTxtFile(initialBookPath)) { + auto txt = loadTxt(initialBookPath); + if (!txt) { + onGoBack(); + return; + } + onGoToTxtReader(std::move(txt)); + } else { + auto epub = loadEpub(initialBookPath); + if (!epub) { + onGoBack(); + return; + } + onGoToEpubReader(std::move(epub)); } - onGoToEpubReader(std::move(epub)); } } diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 9c563eb13..815f362c6 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -379,7 +379,9 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: int bookWidth, bookX; bool hasCoverImage = false; - if (hasContinueReading && !recentBooks[0].coverBmpPath.empty()) { + const bool skipCover = hasContinueReading && recentBooks[0].coverDisabled; + + if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !skipCover) { // Try to get actual image dimensions from BMP header const std::string coverBmpPath = UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight); @@ -403,7 +405,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: bookWidth = maxWidth; } } else { - bookWidth = rect.width / 2; // Fallback + bookWidth = baseHeight * 2 / 3; // Fallback } } file.close(); @@ -412,25 +414,28 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (!hasCoverImage) { // No cover: use half screen size - bookWidth = rect.width / 2; + bookWidth = baseHeight * 2 / 3; + + // If buffer was restored from a previous full-width cover, clear the + // full slot so stale pixels don't leak behind the narrower placeholder. + if (bufferRestored) { + renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); + bufferRestored = false; + coverRendered = false; + coverBufferStored = false; + } } bookX = rect.x + (rect.width - bookWidth) / 2; const int bookY = rect.y; const int bookHeight = baseHeight; - // Bookmark dimensions (used in multiple places) - const int bookmarkWidth = bookWidth / 8; - const int bookmarkHeight = bookHeight / 5; - const int bookmarkX = bookX + bookWidth - bookmarkWidth - 10; - const int bookmarkY = bookY + 5; - // Draw book card regardless, fill with message based on `hasContinueReading` { // Draw cover image as background if available (inside the box) // Only load from SD on first render, then use stored buffer - if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) { + if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered && !skipCover) { const std::string coverBmpPath = UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight); @@ -447,17 +452,16 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // Draw border around the card renderer.drawRect(bookX, bookY, bookWidth, bookHeight); - // No bookmark ribbon when cover is shown - it would just cover the art + coverRendered = true; // Store the buffer with cover image for fast navigation coverBufferStored = storeCoverBuffer(); - coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer // First render: if selected, draw selection indicators now if (bookSelected) { LOG_DBG("THEME", "Drawing selection"); - renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); - renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); + renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2, true); + renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4, true); } } file.close(); @@ -465,118 +469,32 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: } if (!bufferRestored && !coverRendered) { - // No cover image: draw border or fill, plus bookmark as visual flair if (bookSelected) { renderer.fillRect(bookX, bookY, bookWidth, bookHeight); - } else { - renderer.drawRect(bookX, bookY, bookWidth, bookHeight); - } - - // Draw bookmark ribbon when no cover image (visual decoration) - if (hasContinueReading) { - const int notchDepth = bookmarkHeight / 3; - const int centerX = bookmarkX + bookmarkWidth / 2; - - const int xPoints[5] = { - bookmarkX, // top-left - bookmarkX + bookmarkWidth, // top-right - bookmarkX + bookmarkWidth, // bottom-right - centerX, // center notch point - bookmarkX // bottom-left - }; - const int yPoints[5] = { - bookmarkY, // top-left - bookmarkY, // top-right - bookmarkY + bookmarkHeight, // bottom-right - bookmarkY + bookmarkHeight - notchDepth, // center notch point - bookmarkY + bookmarkHeight // bottom-left - }; - - // Draw bookmark ribbon (inverted if selected) - renderer.fillPolygon(xPoints, yPoints, 5, !bookSelected); } } // If buffer was restored, draw selection indicators if needed if (bufferRestored && bookSelected && coverRendered) { - // Draw selection border (no bookmark inversion needed since cover has no bookmark) - renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); - renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); - } else if (!coverRendered && !bufferRestored) { - // Selection border already handled above in the no-cover case + renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2, true); + renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4, true); } } if (hasContinueReading) { - const std::string& lastBookTitle = recentBooks[0].title; - const std::string& lastBookAuthor = recentBooks[0].author; - - // Invert text colors based on selection state: - // - With cover: selected = white text on black box, unselected = black text on white box - // - Without cover: selected = white text on black card, unselected = black text on white card - - auto lines = renderer.wrappedText(UI_12_FONT_ID, lastBookTitle.c_str(), bookWidth - 40, 3); - - // Book title text - int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast(lines.size()); - if (!lastBookAuthor.empty()) { - totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; - } - - // Vertically center the title block within the card - int titleYStart = bookY + (bookHeight - totalTextHeight) / 2; - - const auto truncatedAuthor = lastBookAuthor.empty() - ? std::string{} - : renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40); - - // If cover image was rendered, draw box behind title and author - if (coverRendered) { - constexpr int boxPadding = 8; - // Calculate the max text width for the box - int maxTextWidth = 0; - for (const auto& line : lines) { - const int lineWidth = renderer.getTextWidth(UI_12_FONT_ID, line.c_str()); - if (lineWidth > maxTextWidth) { - maxTextWidth = lineWidth; - } - } - if (!truncatedAuthor.empty()) { - const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, truncatedAuthor.c_str()); - if (authorWidth > maxTextWidth) { - maxTextWidth = authorWidth; - } - } - - const int boxWidth = maxTextWidth + boxPadding * 2; - const int boxHeight = totalTextHeight + boxPadding * 2; - const int boxX = rect.x + (rect.width - boxWidth) / 2; - const int boxY = titleYStart - boxPadding; - - // Draw box (inverted when selected: black box instead of white) - renderer.fillRect(boxX, boxY, boxWidth, boxHeight, bookSelected); - // Draw border around the box (inverted when selected: white border instead of black) - renderer.drawRect(boxX, boxY, boxWidth, boxHeight, !bookSelected); - } - - for (const auto& line : lines) { - renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); - titleYStart += renderer.getLineHeight(UI_12_FONT_ID); - } + const char* continueText = tr(STR_CONTINUE_READING); + const int continueTextW = renderer.getTextWidth(UI_10_FONT_ID, continueText); - if (!truncatedAuthor.empty()) { - titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2; - renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, truncatedAuthor.c_str(), !bookSelected); + if (!hasCoverImage) { + BookCoverParams coverParams{recentBooks[0].title, recentBooks[0].author}; + drawClassicalBookCover(renderer, bookX, bookY, bookWidth, bookHeight, coverParams, bookSelected, continueTextW); + renderer.drawRect(bookX, bookY, bookWidth, bookHeight, 3, true); } - // "Continue Reading" label at the bottom - const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; - if (coverRendered) { - // Draw box behind "Continue Reading" text (inverted when selected: black box instead of white) - const char* continueText = tr(STR_CONTINUE_READING); - const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText); + const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2 - 2; + if (hasCoverImage) { constexpr int continuePadding = 6; - const int continueBoxWidth = continueTextWidth + continuePadding * 2; + const int continueBoxWidth = continueTextW + continuePadding * 2; const int continueBoxHeight = renderer.getLineHeight(UI_10_FONT_ID) + continuePadding; const int continueBoxX = rect.x + (rect.width - continueBoxWidth) / 2; const int continueBoxY = continueY - continuePadding / 2; @@ -584,14 +502,12 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected); renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected); } else { - renderer.drawCenteredText(UI_10_FONT_ID, continueY, tr(STR_CONTINUE_READING), !bookSelected); + renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected); } } else { - // No book to continue reading - const int y = - bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, y, "No open book"); - renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); + renderer.drawRect(bookX, bookY, bookWidth, bookHeight, 3, true); + const int textLineH = renderer.getLineHeight(UI_12_FONT_ID); + renderer.drawCenteredText(UI_12_FONT_ID, bookY + (bookHeight - textLineH) / 2, tr(STR_NO_OPEN_BOOK), true); } } @@ -773,3 +689,87 @@ void BaseTheme::drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const ch } renderer.drawText(UI_10_FONT_ID, textX, rect.y, label); } + +void BaseTheme::drawClassicalBookCover(GfxRenderer& renderer, int x, int y, int w, int h, const BookCoverParams& params, + bool inverted, int continueTextWidth) const { + renderer.fillRect(x + 1, y + 1, w - 2, h - 2, inverted); + + renderer.drawRect(x + 10, y + 10, 12, 12, !inverted); + renderer.drawRect(x + w - 22, y + 10, 12, 12, !inverted); + renderer.drawRect(x + 10, y + h - 22, 12, 12, !inverted); + renderer.drawRect(x + w - 22, y + h - 22, 12, 12, !inverted); + + const int innerL = x + 21; + const int innerR = x + w - 22; + const int innerT = y + 21; + const int innerB = y + h - 22; + const int innerCX = x + w / 2; + renderer.drawLine(innerL, innerT, innerR, innerT, !inverted); + renderer.drawLine(innerL, innerT, innerL, innerB, !inverted); + renderer.drawLine(innerR, innerT, innerR, innerB, !inverted); + const int textGapHalf = (continueTextWidth / 2) + 8; + renderer.drawLine(innerL, innerB, innerCX - textGapHalf, innerB, !inverted); + renderer.drawLine(innerCX + textGapHalf, innerB, innerR, innerB, !inverted); + + const int t = 2; + const int s = 30; + const int midY = y + h / 2; + + renderer.fillRect(x + 10, y + s, t, midY - y - s, !inverted); + renderer.fillRect(x + 10, y + s, s - 10 + t, t, !inverted); + renderer.fillRect(x + s, y + 10, t, s - 10, !inverted); + renderer.fillRect(x + s, y + 10, w - 2 * s, t, !inverted); + renderer.fillRect(x + w - 12, y + s, t, h - 2 * s, !inverted); + renderer.fillRect(x + w - 10 - (s - 10 + t), y + s, s - 10 + t, t, !inverted); + renderer.fillRect(x + w - 10 - (s - 10 + t), y + 10, t, s - 10, !inverted); + renderer.fillRect(x + w - 10 - (s - 10 + t), y + h - s - t, s - 10 + t, t, !inverted); + renderer.fillRect(x + w - 10 - (s - 10 + t), y + h - s, t, s - 10, !inverted); + renderer.fillRect(x + s, y + h - 12, w - 2 * s, t, !inverted); + renderer.fillRect(x + s, y + h - s, t, s - 10, !inverted); + renderer.fillRect(x + 10, y + h - s - t, s - 10 + t, t, !inverted); + renderer.fillRect(x + 10, midY, t, y + h - s - midY, !inverted); + + const int titleMaxW = w - 130; + auto titleLines = renderer.wrappedText(BOOKERLY_12_FONT_ID, params.title.c_str(), titleMaxW, 3); + + const int lineH = renderer.getLineHeight(BOOKERLY_12_FONT_ID); + const int gap = lineH / 2; + + std::vector authorLines; + int authorLineCount = 0; + if (!params.author.empty()) { + authorLines = renderer.wrappedText(BOOKERLY_12_FONT_ID, params.author.c_str(), titleMaxW, 2); + authorLineCount = static_cast(authorLines.size()); + } + + int totalBlockH = (static_cast(titleLines.size()) + authorLineCount) * lineH + 4 * gap + 1; + + const int center = y + (innerB - innerT) * 45 / 100; + const int blockY = center - totalBlockH / 2; + + renderer.fillRect(x + 42, blockY, w - 84, totalBlockH + 16, false); + renderer.drawRect(x + 42, blockY, w - 84, totalBlockH + 16, 2, true); + renderer.drawRect(x + 50, blockY + 8, w - 100, totalBlockH, 1, true); + + int centerX = x + w / 2; + int textY = blockY + 8 + gap; + + for (const auto& line : titleLines) { + int tw = renderer.getTextWidth(BOOKERLY_12_FONT_ID, line.c_str(), EpdFontFamily::BOLD); + renderer.drawText(BOOKERLY_12_FONT_ID, centerX - tw / 2, textY, line.c_str(), true, EpdFontFamily::BOLD); + textY += lineH; + } + + textY += gap; + if (!authorLines.empty()) { + renderer.drawLine(centerX - 40, textY, centerX + 40, textY, true); + textY += 1; + } + textY += gap; + + for (const auto& line : authorLines) { + int aw = renderer.getTextWidth(BOOKERLY_12_FONT_ID, line.c_str()); + renderer.drawText(BOOKERLY_12_FONT_ID, centerX - aw / 2, textY, line.c_str(), true); + textY += lineH; + } +} diff --git a/src/components/themes/BaseTheme.h b/src/components/themes/BaseTheme.h index 6878f5558..1da4c9a14 100644 --- a/src/components/themes/BaseTheme.h +++ b/src/components/themes/BaseTheme.h @@ -9,6 +9,11 @@ class GfxRenderer; struct RecentBook; +struct BookCoverParams { + std::string title; + std::string author; +}; + struct Rect { int x; int y; @@ -142,4 +147,6 @@ class BaseTheme { virtual void drawTextField(const GfxRenderer& renderer, Rect rect, const int textWidth) const; virtual void drawKeyboardKey(const GfxRenderer& renderer, Rect rect, const char* label, const bool isSelected) const; virtual bool showsFileIcons() const { return false; } + void drawClassicalBookCover(GfxRenderer& renderer, int x, int y, int w, int h, const BookCoverParams& params, + bool inverted = false, int continueTextWidth = 0) const; }; diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 68d8b2345..72f15d422 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -7,6 +7,7 @@ #include #include +#include "CrossPointSettings.h" #include "RecentBooksStore.h" #include "components/UITheme.h" #include "components/icons/cover.h" @@ -35,7 +36,8 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con std::string coverPath = recentBooks[i].coverBmpPath; bool hasCover = true; int tileX = Lyra3CoversMetrics::values.contentSidePadding + tileWidth * i; - if (coverPath.empty()) { + const bool skipCover = recentBooks[i].coverDisabled; + if (coverPath.empty() || skipCover) { hasCover = false; } else { const std::string coverBmpPath = @@ -60,6 +62,8 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con hasCover = false; } file.close(); + } else { + hasCover = false; } } // Draw either way @@ -76,8 +80,8 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con } } + coverRendered = true; coverBufferStored = storeCoverBuffer(); - coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer } for (int i = 0; i < std::min(static_cast(recentBooks.size()), Lyra3CoversMetrics::values.homeRecentBooksCount); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 58dabeab7..c297dbd20 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -423,7 +423,8 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: std::string coverPath = book.coverBmpPath; bool hasCover = true; int tileX = LyraMetrics::values.contentSidePadding; - if (coverPath.empty()) { + const bool skipCover = book.coverDisabled; + if (coverPath.empty() || skipCover) { hasCover = false; } else { const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); @@ -440,6 +441,8 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: hasCover = false; } file.close(); + } else { + hasCover = false; } } @@ -455,8 +458,8 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: renderer.drawIcon(CoverIcon, tileX + hPaddingInSelection + 24, tileY + hPaddingInSelection + 24, 32, 32); } + coverRendered = true; coverBufferStored = storeCoverBuffer(); - coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer } bool bookSelected = (selectorIndex == 0);