From c6c6bd33e80938df7a707141002fde753b2a7940 Mon Sep 17 00:00:00 2001 From: pablohc Date: Tue, 24 Mar 2026 20:30:01 +0100 Subject: [PATCH 01/16] feat: add home cover settings with timeout and per-book control - Settings > Display > Cover: Enabled / Timeout (3s) / Disabled enum - Cover generation only triggers on return from Reader (pendingCoverGeneration flag) - ZIP/JPEG/PNG deadline propagated through full stack to prevent WDT resets - Lyra/Lyra3Covers: show placeholder icon when BMP file is missing - addBook() preserves cleared coverBmpPath to prevent timeout retry on reopen - ReaderActivity shows loading popup before epub/xtc/txt load - RecentBook.coverDisabled field added with JSON serialization (Paso 4) Co-Authored-By: Claude Sonnet 4.6 --- lib/Epub/Epub.cpp | 28 ++++++++--- lib/Epub/Epub.h | 4 +- lib/I18n/translations/english.yaml | 4 ++ lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 11 +++-- lib/JpegToBmpConverter/JpegToBmpConverter.h | 5 +- lib/PngToBmpConverter/PngToBmpConverter.cpp | 12 +++-- lib/PngToBmpConverter/PngToBmpConverter.h | 5 +- lib/Xtc/Xtc.cpp | 2 +- lib/Xtc/Xtc.h | 2 +- lib/ZipFile/ZipFile.cpp | 14 +++++- lib/ZipFile/ZipFile.h | 2 +- src/CrossPointSettings.h | 5 ++ src/CrossPointState.h | 1 + src/JsonSettingsIO.cpp | 2 + src/RecentBooksStore.cpp | 16 ++++++- src/RecentBooksStore.h | 3 ++ src/SettingsList.h | 3 ++ src/activities/home/HomeActivity.cpp | 47 +++++++++++++++---- src/activities/reader/EpubReaderActivity.cpp | 1 + src/activities/reader/ReaderActivity.cpp | 5 ++ src/activities/reader/XtcReaderActivity.cpp | 1 + .../themes/lyra/Lyra3CoversTheme.cpp | 2 + src/components/themes/lyra/LyraTheme.cpp | 2 + 23 files changed, 143 insertions(+), 34 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index cb0b18017..d48f127ce 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; } @@ -684,7 +696,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); + PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT, + deadline); coverPng.close(); thumbBmp.close(); Storage.remove(coverPngTempPath.c_str()); @@ -723,14 +736,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..c76c50bb4 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -227,6 +227,10 @@ 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_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..9caefe746 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -200,7 +200,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un // 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 @@ -360,6 +360,11 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm const int mcuPixelWidth = imageInfo.m_MCUWidth; for (int mcuY = 0; mcuY < imageInfo.m_MCUSPerCol; mcuY++) { + if (deadline != 0 && millis() > deadline) { + LOG_ERR("JPG", "Decode deadline exceeded at MCU row %d", mcuY); + return false; + } + // Clear the MCU row buffer memset(mcuRowBuffer, 0, mcuRowPixels); @@ -570,6 +575,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..bf1443c74 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.h +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.h @@ -9,12 +9,13 @@ 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..d5cc1e692 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 && millis() > deadline) { + 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/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 53d32cac3..73f5ff6da 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -304,7 +304,7 @@ bool Xtc::generateCoverBmp() const { std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; } std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; } -bool Xtc::generateThumbBmp(int height) const { +bool Xtc::generateThumbBmp(int height, uint32_t /*deadline*/) const { // Already generated if (Storage.exists(getThumbBmpPath(height).c_str())) { return true; diff --git a/lib/Xtc/Xtc.h b/lib/Xtc/Xtc.h index 9b75f5869..73f4cbb38 100644 --- a/lib/Xtc/Xtc.h +++ b/lib/Xtc/Xtc.h @@ -66,7 +66,7 @@ class Xtc { // Thumbnail support (for Continue Reading card) std::string getThumbBmpPath() const; std::string getThumbBmpPath(int height) const; - bool generateThumbBmp(int height) const; + bool generateThumbBmp(int height, uint32_t deadline = 0) const; // Page access uint32_t getPageCount() const; diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 58fa64d5a..90711b481 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; @@ -526,6 +526,14 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch size_t remaining = inflatedDataSize; while (remaining > 0) { + if (deadline != 0 && millis() > deadline) { + 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"); @@ -588,6 +596,10 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch size_t totalProduced = 0; while (true) { + if (deadline != 0 && millis() > deadline) { + 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/src/CrossPointSettings.h b/src/CrossPointSettings.h index 9a0e298b0..02b6a5b85 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_TIMEOUT; ~CrossPointSettings() = default; diff --git a/src/CrossPointState.h b/src/CrossPointState.h index 1de898382..ee2e658d4 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -12,6 +12,7 @@ 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 ~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..f9da4aab9 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -25,12 +25,17 @@ 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; }); + + // If the existing entry has an empty coverBmpPath, it was deliberately cleared (e.g. after a + // timeout failure) to prevent retrying. Preserve that empty state so HOME does not retry. + const std::string resolvedCover = (it != recentBooks.end() && it->coverBmpPath.empty()) ? "" : coverBmpPath; + 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, resolvedCover}); // Trim to max size if (recentBooks.size() > MAX_RECENT_BOOKS) { @@ -53,6 +58,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; + 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..be28a86d4 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..d65ebf708 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -55,8 +55,22 @@ void HomeActivity::loadRecentCovers(int coverHeight) { bool showingLoading = false; Rect popupRect; - int progress = 0; - for (RecentBook& book : recentBooks) { + static constexpr uint32_t COVER_RENDER_TIMEOUT_MS = 3000; + + // Skip all cover generation if globally disabled + if (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE) { + 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()) { std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); if (!Storage.exists(coverPath.c_str())) { @@ -71,36 +85,42 @@ void HomeActivity::loadRecentCovers(int coverHeight) { showingLoading = true; popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); } - GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); - bool success = epub.generateThumbBmp(coverHeight); + GUI.fillPopupProgress(renderer, popupRect, 50); + const uint32_t deadline = useTimeout ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; + bool success = epub.generateThumbBmp(coverHeight, deadline); if (!success) { + // Clear cover path on any failure (including timeout) so HOME won't retry. + // coverBmpPath is restored next time the user opens the book (via addBook). RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); book.coverBmpPath = ""; } + // 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())); - bool success = xtc.generateThumbBmp(coverHeight); + GUI.fillPopupProgress(renderer, popupRect, 50); + const uint32_t deadline = useTimeout ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; + bool success = xtc.generateThumbBmp(coverHeight, deadline); if (!success) { RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); book.coverBmpPath = ""; } coverRendered = false; + coverBufferStored = false; requestUpdate(); } } } } - progress++; } recentsLoaded = true; @@ -253,8 +273,15 @@ 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 + recentsLoaded = true; + if (APP_STATE.pendingCoverGeneration && + SETTINGS.coverMode != CrossPointSettings::COVER_DISABLED_MODE) { + 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..32a2290be 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -97,6 +97,7 @@ void EpubReaderActivity::onExit() { renderer.setOrientation(GfxRenderer::Orientation::Portrait); APP_STATE.readerActivityLoadCount = 0; + APP_STATE.pendingCoverGeneration = true; APP_STATE.saveToFile(); section.reset(); epub.reset(); diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 2164a7f66..d9f0aa280 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('/'); @@ -115,6 +117,7 @@ void ReaderActivity::onEnter() { if (isBmpFile(initialBookPath)) { onGoToBmpViewer(initialBookPath); } else if (isXtcFile(initialBookPath)) { + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); auto xtc = loadXtc(initialBookPath); if (!xtc) { onGoBack(); @@ -122,6 +125,7 @@ void ReaderActivity::onEnter() { } onGoToXtcReader(std::move(xtc)); } else if (isTxtFile(initialBookPath)) { + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); auto txt = loadTxt(initialBookPath); if (!txt) { onGoBack(); @@ -129,6 +133,7 @@ void ReaderActivity::onEnter() { } onGoToTxtReader(std::move(txt)); } else { + GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); auto epub = loadEpub(initialBookPath); if (!epub) { onGoBack(); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 84cc51da2..f9a0f575f 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -50,6 +50,7 @@ void XtcReaderActivity::onExit() { Activity::onExit(); APP_STATE.readerActivityLoadCount = 0; + APP_STATE.pendingCoverGeneration = true; APP_STATE.saveToFile(); xtc.reset(); } diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 68d8b2345..44a2f5ed7 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -60,6 +60,8 @@ void Lyra3CoversTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, con hasCover = false; } file.close(); + } else { + hasCover = false; } } // Draw either way diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 58dabeab7..07c7f09aa 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -440,6 +440,8 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: hasCover = false; } file.close(); + } else { + hasCover = false; } } From d79bc5cc1882ca6885fa7e4d84ee0168b8c51015 Mon Sep 17 00:00:00 2001 From: pablohc Date: Tue, 24 Mar 2026 22:10:14 +0100 Subject: [PATCH 02/16] feat: coverDisabled state machine, per-book cover menu action - HomeActivity: isForcedBook bypasses coverDisabled guard and DISABLED global mode; DISABLED early return now allows force renders; add setCoverDisabled(false) on successful generation - BaseTheme/LyraTheme/Lyra3CoversTheme: skipCover now uses only coverDisabled (no global mode check), enabling per-book ENABLED to override global DISABLED - EpubReaderActivity/XtcReaderActivity: set coverDisabled=true on exit when global mode is DISABLED - EpubReaderMenuActivity: add COVER_ACTION to menu; TIMEOUT shows "Generate cover", ENABLED/DISABLED toggle "Enable/Disable cover" - EpubReaderActivity: pass bookCoverDisabled to menu, handle COVER_ACTION (force render or toggle per-book state + delete BMP) Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + lib/I18n/translations/english.yaml | 3 ++ src/CrossPointState.h | 1 + src/RecentBooksStore.cpp | 8 ++-- src/activities/home/HomeActivity.cpp | 47 ++++++++++++------- src/activities/reader/EpubReaderActivity.cpp | 43 ++++++++++++++++- .../reader/EpubReaderMenuActivity.cpp | 17 +++++-- .../reader/EpubReaderMenuActivity.h | 8 ++-- src/activities/reader/XtcReaderActivity.cpp | 3 ++ src/components/themes/BaseTheme.cpp | 6 ++- .../themes/lyra/Lyra3CoversTheme.cpp | 4 +- src/components/themes/lyra/LyraTheme.cpp | 3 +- 12 files changed, 112 insertions(+), 32 deletions(-) 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/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index c76c50bb4..eba56cc7c 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -231,6 +231,9 @@ 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/src/CrossPointState.h b/src/CrossPointState.h index ee2e658d4..7044e2bf0 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -13,6 +13,7 @@ class CrossPointState { 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/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index f9da4aab9..422d20894 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -26,16 +26,16 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title auto it = std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); - // If the existing entry has an empty coverBmpPath, it was deliberately cleared (e.g. after a - // timeout failure) to prevent retrying. Preserve that empty state so HOME does not retry. - const std::string resolvedCover = (it != recentBooks.end() && it->coverBmpPath.empty()) ? "" : coverBmpPath; + // 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 : false; if (it != recentBooks.end()) { recentBooks.erase(it); } // Add to front - recentBooks.insert(recentBooks.begin(), {path, title, author, resolvedCover}); + recentBooks.insert(recentBooks.begin(), {path, title, author, coverBmpPath, existingDisabled}); // Trim to max size if (recentBooks.size() > MAX_RECENT_BOOKS) { diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index d65ebf708..15214fcb9 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -57,8 +57,13 @@ void HomeActivity::loadRecentCovers(int coverHeight) { static constexpr uint32_t COVER_RENDER_TIMEOUT_MS = 3000; - // Skip all cover generation if globally disabled - if (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE) { + // 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; @@ -71,7 +76,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { // This prevents blocking HOME with multiple simultaneous generations. if (!recentBooks.empty()) { RecentBook& book = recentBooks[0]; - if (!book.coverBmpPath.empty()) { + if (!book.coverBmpPath.empty() && (!book.coverDisabled || isForcedBook)) { std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); if (!Storage.exists(coverPath.c_str())) { // If epub, try to load the metadata for title/author and cover @@ -86,13 +91,17 @@ void HomeActivity::loadRecentCovers(int coverHeight) { popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); } GUI.fillPopupProgress(renderer, popupRect, 50); - const uint32_t deadline = useTimeout ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; + // 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) { - // Clear cover path on any failure (including timeout) so HOME won't retry. - // coverBmpPath is restored next time the user opens the book (via addBook). - RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); - book.coverBmpPath = ""; + 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. @@ -108,11 +117,14 @@ void HomeActivity::loadRecentCovers(int coverHeight) { popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); } GUI.fillPopupProgress(renderer, popupRect, 50); - const uint32_t deadline = useTimeout ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; + const uint32_t deadline = (useTimeout && !isForcedBook) ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; bool success = xtc.generateThumbBmp(coverHeight, deadline); - 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; @@ -123,6 +135,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { } } + APP_STATE.forceRenderCoverPath = ""; // consume force-render flag recentsLoaded = true; recentsLoading = false; } @@ -273,10 +286,12 @@ void HomeActivity::render(RenderLock&&) { firstRenderDone = true; requestUpdate(); } else if (!recentsLoaded && !recentsLoading) { - // Only generate covers when returning from Reader, not on every HOME entry + // 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; - if (APP_STATE.pendingCoverGeneration && - SETTINGS.coverMode != CrossPointSettings::COVER_DISABLED_MODE) { + const bool hasPending = APP_STATE.pendingCoverGeneration; + const bool hasForce = !APP_STATE.forceRenderCoverPath.empty(); + if (hasPending || hasForce) { APP_STATE.pendingCoverGeneration = false; recentsLoaded = false; recentsLoading = true; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 32a2290be..8b2fd777d 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -97,6 +97,9 @@ void EpubReaderActivity::onExit() { renderer.setOrientation(GfxRenderer::Orientation::Portrait); APP_STATE.readerActivityLoadCount = 0; + if (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE) { + RECENT_BOOKS.setCoverDisabled(epub->getPath(), true); + } APP_STATE.pendingCoverGeneration = true; APP_STATE.saveToFile(); section.reset(); @@ -146,9 +149,16 @@ void EpubReaderActivity::loop() { bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); + bool bookCoverDisabled = false; + for (const auto& rb : RECENT_BOOKS.getBooks()) { + if (rb.path == epub->getPath()) { + bookCoverDisabled = rb.coverDisabled; + break; + } + } 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); @@ -381,6 +391,37 @@ 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 + RECENT_BOOKS.setCoverDisabled(epub->getPath(), false); + APP_STATE.forceRenderCoverPath = epub->getPath(); + } else { + // ENABLED or DISABLED: toggle per-book state + bool wasDisabled = false; + for (const auto& rb : RECENT_BOOKS.getBooks()) { + if (rb.path == epub->getPath()) { + wasDisabled = rb.coverDisabled; + break; + } + } + 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); + } else { + // Enable: force-render (works even in DISABLED global mode) + RECENT_BOOKS.setCoverDisabled(epub->getPath(), false); + 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/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index f9a0f575f..20647085d 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -50,6 +50,9 @@ void XtcReaderActivity::onExit() { Activity::onExit(); APP_STATE.readerActivityLoadCount = 0; + if (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE) { + RECENT_BOOKS.setCoverDisabled(xtc->getPath(), true); + } APP_STATE.pendingCoverGeneration = true; APP_STATE.saveToFile(); xtc.reset(); diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 9c563eb13..a0b04a3d3 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); @@ -430,7 +432,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // 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); diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index 44a2f5ed7..aca05d985 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 = diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 07c7f09aa..bc9bd094d 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); From 79ebb57b1becced6dd7e940556516707b376b4df Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Mar 2026 00:06:55 +0100 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20Classic=20theme=20=E2=80=94=20hid?= =?UTF-8?q?e=20title/author=20overlay=20when=20cover=20present,=20move=20b?= =?UTF-8?q?ookmark=20to=20spine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hide title + author text overlay when hasCoverImage is true; cover speaks for itself without redundant text on top of the art - Update "Continue Reading" box condition from coverRendered to hasCoverImage for consistency (also covers edge case where buffer store fails but BMP is already drawn) - Move bookmark ribbon from top-right to top-left (spine side), flush with top edge, 20px margin from the spine Co-Authored-By: Claude Sonnet 4.6 --- lib/Epub/Epub.cpp | 5 +- lib/Xtc/Xtc.cpp | 2 +- lib/Xtc/Xtc.h | 2 +- src/SettingsList.h | 4 +- src/activities/home/HomeActivity.cpp | 2 +- src/components/themes/BaseTheme.cpp | 79 +++++++++------------------- 6 files changed, 31 insertions(+), 63 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index d48f127ce..fe4963bd1 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -695,9 +695,8 @@ bool Epub::generateThumbBmp(int height, uint32_t deadline) 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, - deadline); + const bool success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, + THUMB_TARGET_HEIGHT, deadline); coverPng.close(); thumbBmp.close(); Storage.remove(coverPngTempPath.c_str()); diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 73f5ff6da..53d32cac3 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -304,7 +304,7 @@ bool Xtc::generateCoverBmp() const { std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; } std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; } -bool Xtc::generateThumbBmp(int height, uint32_t /*deadline*/) const { +bool Xtc::generateThumbBmp(int height) const { // Already generated if (Storage.exists(getThumbBmpPath(height).c_str())) { return true; diff --git a/lib/Xtc/Xtc.h b/lib/Xtc/Xtc.h index 73f4cbb38..9b75f5869 100644 --- a/lib/Xtc/Xtc.h +++ b/lib/Xtc/Xtc.h @@ -66,7 +66,7 @@ class Xtc { // Thumbnail support (for Continue Reading card) std::string getThumbBmpPath() const; std::string getThumbBmpPath(int height) const; - bool generateThumbBmp(int height, uint32_t deadline = 0) const; + bool generateThumbBmp(int height) const; // Page access uint32_t getPageCount() const; diff --git a/src/SettingsList.h b/src/SettingsList.h index be28a86d4..58116ff73 100644 --- a/src/SettingsList.h +++ b/src/SettingsList.h @@ -36,8 +36,8 @@ inline const std::vector& getSettingsList() { 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), + {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 15214fcb9..bfd598804 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -118,7 +118,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { } GUI.fillPopupProgress(renderer, popupRect, 50); const uint32_t deadline = (useTimeout && !isForcedBook) ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; - bool success = xtc.generateThumbBmp(coverHeight, deadline); + bool success = xtc.generateThumbBmp(coverHeight); if (success) { RECENT_BOOKS.setCoverDisabled(book.path, false); book.coverDisabled = false; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index a0b04a3d3..d52b546b2 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -424,8 +424,8 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // 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; + const int bookmarkX = bookX + 20; // top-left, 20px from the spine edge + const int bookmarkY = bookY; // flush with the top of the card // Draw book card regardless, fill with message based on `hasContinueReading` { @@ -510,71 +510,40 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: } if (hasContinueReading) { - const std::string& lastBookTitle = recentBooks[0].title; - const std::string& lastBookAuthor = recentBooks[0].author; + // Only show title + author when there is no cover image (alt-text placeholder). + // When the cover is available, it speaks for itself — the overlay would obscure the art. + if (!hasCoverImage) { + 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); - 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; - } + 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; + int titleYStart = bookY + (bookHeight - totalTextHeight) / 2; - const auto truncatedAuthor = lastBookAuthor.empty() - ? std::string{} - : renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40); + 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; - } + renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); + titleYStart += renderer.getLineHeight(UI_12_FONT_ID); } + if (!truncatedAuthor.empty()) { - const int authorWidth = renderer.getTextWidth(UI_10_FONT_ID, truncatedAuthor.c_str()); - if (authorWidth > maxTextWidth) { - maxTextWidth = authorWidth; - } + titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2; + renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, truncatedAuthor.c_str(), !bookSelected); } - - 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); - } - - if (!truncatedAuthor.empty()) { - titleYStart += renderer.getLineHeight(UI_10_FONT_ID) / 2; - renderer.drawCenteredText(UI_10_FONT_ID, titleYStart, truncatedAuthor.c_str(), !bookSelected); } - // "Continue Reading" label at the bottom + // "Continue Reading" label at the bottom — always visible. + // When a cover is present, draw it inside a box so it stands out over the image. 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) + if (hasCoverImage) { const char* continueText = tr(STR_CONTINUE_READING); const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText); constexpr int continuePadding = 6; From 1042f74a29eb465427a95826aef4360387c24e5f Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Mar 2026 01:39:07 +0100 Subject: [PATCH 04/16] =?UTF-8?q?fix:=20resolve=20cppcheck=20warnings=20?= =?UTF-8?q?=E2=80=94=20unused=20vars=20and=20raw=20loops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/activities/home/HomeActivity.cpp | 12 ++-------- src/activities/reader/EpubReaderActivity.cpp | 24 ++++++++------------ 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index bfd598804..dded153c3 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -52,7 +52,6 @@ void HomeActivity::loadRecentBooks(int maxBooks) { void HomeActivity::loadRecentCovers(int coverHeight) { recentsLoading = true; - bool showingLoading = false; Rect popupRect; static constexpr uint32_t COVER_RENDER_TIMEOUT_MS = 3000; @@ -86,10 +85,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { 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)); - } + 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; @@ -112,12 +108,8 @@ void HomeActivity::loadRecentCovers(int coverHeight) { // Handle XTC file Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { - if (!showingLoading) { - showingLoading = true; - popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - } + popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); GUI.fillPopupProgress(renderer, popupRect, 50); - const uint32_t deadline = (useTimeout && !isForcedBook) ? (millis() + COVER_RENDER_TIMEOUT_MS) : 0; bool success = xtc.generateThumbBmp(coverHeight); if (success) { RECENT_BOOKS.setCoverDisabled(book.path, false); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 8b2fd777d..73e8b7686 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" @@ -149,13 +151,10 @@ void EpubReaderActivity::loop() { bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); - bool bookCoverDisabled = false; - for (const auto& rb : RECENT_BOOKS.getBooks()) { - if (rb.path == epub->getPath()) { - bookCoverDisabled = rb.coverDisabled; - break; - } - } + const auto& recentBooksList = RECENT_BOOKS.getBooks(); + const auto recentIt = std::find_if(recentBooksList.begin(), recentBooksList.end(), + [this](const RecentBook& rb) { return rb.path == epub->getPath(); }); + const bool bookCoverDisabled = (recentIt != recentBooksList.end()) && recentIt->coverDisabled; startActivityForResult(std::make_unique( renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation, !currentPageFootnotes.empty(), bookCoverDisabled), @@ -398,13 +397,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction APP_STATE.forceRenderCoverPath = epub->getPath(); } else { // ENABLED or DISABLED: toggle per-book state - bool wasDisabled = false; - for (const auto& rb : RECENT_BOOKS.getBooks()) { - if (rb.path == epub->getPath()) { - wasDisabled = rb.coverDisabled; - break; - } - } + const auto& toggleBooksList = RECENT_BOOKS.getBooks(); + const auto toggleIt = std::find_if(toggleBooksList.begin(), toggleBooksList.end(), + [this](const RecentBook& rb) { return rb.path == epub->getPath(); }); + const bool wasDisabled = (toggleIt != toggleBooksList.end()) && toggleIt->coverDisabled; if (!wasDisabled) { // Disable: delete BMP and mark disabled const std::string bmpPath = From 475a575371383e9bfa1d4e9b35e908646c83b203 Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Mar 2026 01:51:05 +0100 Subject: [PATCH 05/16] =?UTF-8?q?fix:=20address=20CodeRabbit=20review=20?= =?UTF-8?q?=E2=80=94=20millis=20rollover,=20onExit=20write-back,=20flash?= =?UTF-8?q?=20churn?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use wrap-safe deadline check in JPEG/PNG/ZipFile deadline pipeline: `static_cast(millis() - deadline) >= 0` - Remove COVER_DISABLED_MODE write-back from EpubReaderActivity::onExit() to preserve per-book cover enables set via Reader Menu - Revert XtcReaderActivity::onExit() to master state (no changes from this PR) - Guard RecentBooksStore::setCoverDisabled() to skip flash write when value unchanged Co-Authored-By: Claude Sonnet 4.6 --- lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 2 +- lib/PngToBmpConverter/PngToBmpConverter.cpp | 2 +- lib/ZipFile/ZipFile.cpp | 4 ++-- src/RecentBooksStore.cpp | 2 +- src/activities/reader/EpubReaderActivity.cpp | 3 --- src/activities/reader/XtcReaderActivity.cpp | 4 ---- 6 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 9caefe746..561939616 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -360,7 +360,7 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm const int mcuPixelWidth = imageInfo.m_MCUWidth; for (int mcuY = 0; mcuY < imageInfo.m_MCUSPerCol; mcuY++) { - if (deadline != 0 && millis() > deadline) { + if (deadline != 0 && static_cast(millis() - deadline) >= 0) { LOG_ERR("JPG", "Decode deadline exceeded at MCU row %d", mcuY); return false; } diff --git a/lib/PngToBmpConverter/PngToBmpConverter.cpp b/lib/PngToBmpConverter/PngToBmpConverter.cpp index d5cc1e692..95915ab38 100644 --- a/lib/PngToBmpConverter/PngToBmpConverter.cpp +++ b/lib/PngToBmpConverter/PngToBmpConverter.cpp @@ -667,7 +667,7 @@ bool PngToBmpConverter::pngFileToBmpStreamInternal(FsFile& pngFile, Print& bmpOu // Process each scanline for (uint32_t y = 0; y < height; y++) { - if (deadline != 0 && millis() > deadline) { + if (deadline != 0 && static_cast(millis() - deadline) >= 0) { LOG_ERR("PNG", "Decode deadline exceeded at scanline %u", y); success = false; break; diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 90711b481..765279e67 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -526,7 +526,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch size_t remaining = inflatedDataSize; while (remaining > 0) { - if (deadline != 0 && millis() > deadline) { + if (deadline != 0 && static_cast(millis() - deadline) >= 0) { LOG_ERR("ZIP", "Read deadline exceeded (stored)"); free(buffer); if (!wasOpen) { @@ -596,7 +596,7 @@ bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t ch size_t totalProduced = 0; while (true) { - if (deadline != 0 && millis() > deadline) { + if (deadline != 0 && static_cast(millis() - deadline) >= 0) { LOG_ERR("ZIP", "Decompress deadline exceeded after %zu bytes", totalProduced); break; } diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 422d20894..b88bedec8 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -61,7 +61,7 @@ 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()) { + if (it != recentBooks.end() && it->coverDisabled != disabled) { it->coverDisabled = disabled; saveToFile(); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 73e8b7686..6ea90e96c 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -99,9 +99,6 @@ void EpubReaderActivity::onExit() { renderer.setOrientation(GfxRenderer::Orientation::Portrait); APP_STATE.readerActivityLoadCount = 0; - if (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE) { - RECENT_BOOKS.setCoverDisabled(epub->getPath(), true); - } APP_STATE.pendingCoverGeneration = true; APP_STATE.saveToFile(); section.reset(); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 20647085d..84cc51da2 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -50,10 +50,6 @@ void XtcReaderActivity::onExit() { Activity::onExit(); APP_STATE.readerActivityLoadCount = 0; - if (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE) { - RECENT_BOOKS.setCoverDisabled(xtc->getPath(), true); - } - APP_STATE.pendingCoverGeneration = true; APP_STATE.saveToFile(); xtc.reset(); } From f55a501843ef623584b3a9d96b758aef1a15eaf0 Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Mar 2026 02:09:24 +0100 Subject: [PATCH 06/16] fix: force-render bypasses Storage.exists check and clears stale BMP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isForcedBook now skips both the outer Storage.exists guard and deletes the stale BMP before calling generateThumbBmp (which has its own internal exists-check that would otherwise return early). Only applies to EPUB — XTC cover generation is unaffected. Co-Authored-By: Claude Sonnet 4.6 --- src/activities/home/HomeActivity.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index dded153c3..68a78d644 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -77,9 +77,13 @@ void HomeActivity::loadRecentCovers(int coverHeight) { 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); From 490226b07964d7052e5fb260822391e09ca2e28c Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Mar 2026 02:12:32 +0100 Subject: [PATCH 07/16] fix: clear forceRenderCoverPath when user disables cover in Reader Menu If the user enables then disables cover in the same reader session, forceRenderCoverPath could still be set when HOME runs, causing it to regenerate the BMP the user just deleted. Clear the flag on disable. Co-Authored-By: Claude Sonnet 4.6 --- src/activities/reader/EpubReaderActivity.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6ea90e96c..5b699165c 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -406,6 +406,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction 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) RECENT_BOOKS.setCoverDisabled(epub->getPath(), false); From be20a6a47fd5e17afa0db8622ebacf91e6eaf647 Mon Sep 17 00:00:00 2001 From: pablohc Date: Wed, 25 Mar 2026 02:25:27 +0100 Subject: [PATCH 08/16] fix: ZipFile::readFileToStream resource leak and unchecked write - Close archive on early returns when this function opened it (loadFileStatSlim failure and negative fileOffset) - Check out.write() return in stored-entry path, matching the existing check in the deflated path (line 614) Co-Authored-By: Claude Sonnet 4.6 --- lib/ZipFile/ZipFile.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 765279e67..4d4188dd2 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -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; } @@ -544,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; } From cb834e9f0ba8268a0ec0d8d9a6a86e92dfb6c904 Mon Sep 17 00:00:00 2001 From: pablohc Date: Thu, 26 Mar 2026 15:00:29 +0100 Subject: [PATCH 09/16] refactor: deduplicate RecentBook lookup and loading popup calls Extract repeated find_if over RECENT_BOOKS into a findRecentBook() helper in EpubReaderActivity.cpp; hoist the single GUI.drawPopup call in ReaderActivity::onEnter to cover all non-BMP branches. Co-Authored-By: Claude Sonnet 4.6 --- src/activities/reader/EpubReaderActivity.cpp | 18 +++++---- src/activities/reader/ReaderActivity.cpp | 42 ++++++++++---------- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 5b699165c..ba9df3c56 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -33,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; @@ -148,10 +154,8 @@ void EpubReaderActivity::loop() { bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int bookProgressPercent = clampPercent(static_cast(bookProgress + 0.5f)); - const auto& recentBooksList = RECENT_BOOKS.getBooks(); - const auto recentIt = std::find_if(recentBooksList.begin(), recentBooksList.end(), - [this](const RecentBook& rb) { return rb.path == epub->getPath(); }); - const bool bookCoverDisabled = (recentIt != recentBooksList.end()) && recentIt->coverDisabled; + 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(), bookCoverDisabled), @@ -394,10 +398,8 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction APP_STATE.forceRenderCoverPath = epub->getPath(); } else { // ENABLED or DISABLED: toggle per-book state - const auto& toggleBooksList = RECENT_BOOKS.getBooks(); - const auto toggleIt = std::find_if(toggleBooksList.begin(), toggleBooksList.end(), - [this](const RecentBook& rb) { return rb.path == epub->getPath(); }); - const bool wasDisabled = (toggleIt != toggleBooksList.end()) && toggleIt->coverDisabled; + const RecentBook* toggleBook = findRecentBook(epub->getPath()); + const bool wasDisabled = toggleBook && toggleBook->coverDisabled; if (!wasDisabled) { // Disable: delete BMP and mark disabled const std::string bmpPath = diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index d9f0aa280..145442795 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -116,30 +116,30 @@ void ReaderActivity::onEnter() { currentBookPath = initialBookPath; if (isBmpFile(initialBookPath)) { onGoToBmpViewer(initialBookPath); - } else if (isXtcFile(initialBookPath)) { - GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - auto xtc = loadXtc(initialBookPath); - if (!xtc) { - onGoBack(); - return; - } - onGoToXtcReader(std::move(xtc)); - } else if (isTxtFile(initialBookPath)) { - GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - auto txt = loadTxt(initialBookPath); - if (!txt) { - onGoBack(); - return; - } - onGoToTxtReader(std::move(txt)); } else { GUI.drawPopup(renderer, tr(STR_LOADING_POPUP)); - auto epub = loadEpub(initialBookPath); - if (!epub) { - onGoBack(); - return; + 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)); } } From ad90fd3eb15930c264f7681f5309eb2291e3da11 Mon Sep 17 00:00:00 2001 From: pablohc Date: Thu, 26 Mar 2026 15:59:09 +0100 Subject: [PATCH 10/16] feat: set COVER_ENABLED as default for home cover setting Co-Authored-By: Claude Sonnet 4.6 --- src/CrossPointSettings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 02b6a5b85..3f4e973d3 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -203,7 +203,7 @@ class CrossPointSettings { // Image rendering mode in EPUB reader uint8_t imageRendering = IMAGES_DISPLAY; // Home screen cover rendering mode - uint8_t coverMode = COVER_TIMEOUT; + uint8_t coverMode = COVER_ENABLED; ~CrossPointSettings() = default; From b2319832a58fdc9d9ffe842003de7624ba658569 Mon Sep 17 00:00:00 2001 From: pablohc Date: Fri, 27 Mar 2026 19:42:34 +0100 Subject: [PATCH 11/16] fix: initialize per-book cover state from global mode on first open --- src/RecentBooksStore.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index b88bedec8..bfe18b518 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -1,5 +1,6 @@ #include "RecentBooksStore.h" +#include #include #include #include @@ -28,7 +29,8 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title // 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 : false; + const bool existingDisabled = + (it != recentBooks.end()) ? it->coverDisabled : (SETTINGS.coverMode == CrossPointSettings::COVER_DISABLED_MODE); if (it != recentBooks.end()) { recentBooks.erase(it); From 3cb0a2a3affb40c468f7a6d85381d8010b6433f3 Mon Sep 17 00:00:00 2001 From: pablohc Date: Fri, 27 Mar 2026 21:34:54 +0100 Subject: [PATCH 12/16] feat: add classical book cover placeholder for Classic/BaseTheme --- src/components/themes/BaseTheme.cpp | 185 +++++++++++++++------------- src/components/themes/BaseTheme.h | 7 ++ 2 files changed, 109 insertions(+), 83 deletions(-) diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index d52b546b2..a2a40ef2c 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -405,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(); @@ -414,19 +414,13 @@ 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; } 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 + 20; // top-left, 20px from the spine edge - const int bookmarkY = bookY; // flush with the top of the card - // Draw book card regardless, fill with message based on `hasContinueReading` { // Draw cover image as background if available (inside the box) @@ -449,8 +443,6 @@ 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 - // Store the buffer with cover image for fast navigation coverBufferStored = storeCoverBuffer(); coverRendered = coverBufferStored; // Only consider it rendered if we successfully stored the buffer @@ -458,8 +450,8 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // 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(); @@ -467,87 +459,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) { - // Only show title + author when there is no cover image (alt-text placeholder). - // When the cover is available, it speaks for itself — the overlay would obscure the art. - if (!hasCoverImage) { - const std::string& lastBookTitle = recentBooks[0].title; - const std::string& lastBookAuthor = recentBooks[0].author; + const char* continueText = tr(STR_CONTINUE_READING); + const int continueTextW = renderer.getTextWidth(UI_10_FONT_ID, continueText); - auto lines = renderer.wrappedText(UI_12_FONT_ID, lastBookTitle.c_str(), bookWidth - 40, 3); - - int totalTextHeight = renderer.getLineHeight(UI_12_FONT_ID) * static_cast(lines.size()); - if (!lastBookAuthor.empty()) { - totalTextHeight += renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; - } - - int titleYStart = bookY + (bookHeight - totalTextHeight) / 2; - - const auto truncatedAuthor = lastBookAuthor.empty() - ? std::string{} - : renderer.truncatedText(UI_10_FONT_ID, lastBookAuthor.c_str(), bookWidth - 40); - - for (const auto& line : lines) { - renderer.drawCenteredText(UI_12_FONT_ID, titleYStart, line.c_str(), !bookSelected); - titleYStart += renderer.getLineHeight(UI_12_FONT_ID); - } - - 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 — always visible. - // When a cover is present, draw it inside a box so it stands out over the image. - const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; + const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2 - 2; if (hasCoverImage) { - const char* continueText = tr(STR_CONTINUE_READING); - const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText); 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; @@ -555,14 +492,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); } } @@ -744,3 +679,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; }; From 98193a998f06f844ae73dd6fe5b9c89d1589a40c Mon Sep 17 00:00:00 2001 From: pablohc Date: Sat, 28 Mar 2026 03:02:32 +0100 Subject: [PATCH 13/16] fix: decouple coverRendered from cache write and clear stale pixels on no-cover books coverRendered is now set immediately after drawing, before storeCoverBuffer(). A failed cache write no longer causes re-rendering every frame. When bufferRestored && !hasCoverImage in BaseTheme, the full rect is cleared so prior full-width cover pixels don't leak behind the narrower placeholder. Applied to BaseTheme, LyraTheme, and Lyra3CoversTheme. --- src/components/themes/BaseTheme.cpp | 10 +++++++++- src/components/themes/lyra/Lyra3CoversTheme.cpp | 2 +- src/components/themes/lyra/LyraTheme.cpp | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index a2a40ef2c..e6359c2c2 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -415,6 +415,13 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (!hasCoverImage) { // No cover: use half screen size 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; + } } bookX = rect.x + (rect.width - bookWidth) / 2; @@ -443,9 +450,10 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // Draw border around the card renderer.drawRect(bookX, bookY, bookWidth, bookHeight); + 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) { diff --git a/src/components/themes/lyra/Lyra3CoversTheme.cpp b/src/components/themes/lyra/Lyra3CoversTheme.cpp index aca05d985..72f15d422 100644 --- a/src/components/themes/lyra/Lyra3CoversTheme.cpp +++ b/src/components/themes/lyra/Lyra3CoversTheme.cpp @@ -80,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 bc9bd094d..c297dbd20 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -458,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); From 33f0e6b0cb8f92fe313985d4c7a36b0df537e9a2 Mon Sep 17 00:00:00 2001 From: pablohc Date: Sat, 28 Mar 2026 10:09:46 +0100 Subject: [PATCH 14/16] fix: reset coverRendered and coverBufferStored when clearing stale buffer on no-cover path --- src/components/themes/BaseTheme.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index e6359c2c2..815f362c6 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -421,6 +421,8 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (bufferRestored) { renderer.fillRect(rect.x, rect.y, rect.width, rect.height, false); bufferRestored = false; + coverRendered = false; + coverBufferStored = false; } } From ae40cfd69ade2e0f4bb095edcad7f35603d2d34d Mon Sep 17 00:00:00 2001 From: pablohc Date: Sat, 28 Mar 2026 10:18:06 +0100 Subject: [PATCH 15/16] fix: reset coverRendered on HomeActivity::onEnter and delay coverDisabled persistence until HOME confirms success --- src/activities/home/HomeActivity.cpp | 4 +++- src/activities/reader/EpubReaderActivity.cpp | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 68a78d644..e8a10bba6 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -139,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(); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index ba9df3c56..a266732ae 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -394,7 +394,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction case EpubReaderMenuActivity::MenuAction::COVER_ACTION: { if (SETTINGS.coverMode == CrossPointSettings::COVER_TIMEOUT) { // Force-render with no timeout regardless of current coverDisabled state - RECENT_BOOKS.setCoverDisabled(epub->getPath(), false); APP_STATE.forceRenderCoverPath = epub->getPath(); } else { // ENABLED or DISABLED: toggle per-book state @@ -414,7 +413,6 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction } } else { // Enable: force-render (works even in DISABLED global mode) - RECENT_BOOKS.setCoverDisabled(epub->getPath(), false); APP_STATE.forceRenderCoverPath = epub->getPath(); } } From ba31b05cff53dae898820071f7d34e0cd5ab119d Mon Sep 17 00:00:00 2001 From: jpirnay Date: Sat, 28 Mar 2026 11:34:07 +0100 Subject: [PATCH 16/16] Replace picojpeg with jpegdec; thread deadline through callback - Delete lib/picojpeg/ (picojpeg.c + picojpeg.h) - Rewrite JpegToBmpConverter to use JPEGDEC (callback-based decoder) instead of picojpeg; adds progressive JPEG support via patch_jpegdec.py - Preserve the deadline parameter introduced in this branch: store it in BmpConvertCtx and check it in the JPEGDEC draw callback (equivalent to the per-MCU-row check in the picojpeg version) Co-Authored-By: Claude Sonnet 4.6 --- lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 630 +++-- lib/JpegToBmpConverter/JpegToBmpConverter.h | 2 - lib/picojpeg/picojpeg.c | 2087 ----------------- lib/picojpeg/picojpeg.h | 124 - 4 files changed, 307 insertions(+), 2536 deletions(-) delete mode 100644 lib/picojpeg/picojpeg.c delete mode 100644 lib/picojpeg/picojpeg.h diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 561939616..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; +}; + +// 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 (!context || !context->file) { - return PJPG_STREAM_READ_ERROR; + 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); +} + +// Flush one scaled output row from Y-axis accumulators and advance currentOutY +static void flushScaledRow(BmpConvertCtx* ctx) { + memset(ctx->bmpRow, 0, ctx->bytesPerRow); - if (context->bufferFilled == 0) { - // EOF or error - *pBytes_actually_read = 0; - return 0; // Success (EOF is normal) + if (USE_8BIT_OUTPUT && !ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; + ctx->bmpRow[x] = adjustPixel(gray); + } + } else if (ctx->oneBit) { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = (ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0; + const uint8_t bit = ctx->atkinson1BitDitherer ? ctx->atkinson1BitDitherer->processPixel(gray, x) + : quantize1bit(gray, x, ctx->currentOutY); + ctx->bmpRow[x / 8] |= (bit << (7 - (x % 8))); + } + if (ctx->atkinson1BitDitherer) ctx->atkinson1BitDitherer->nextRow(); + } else { + for (int x = 0; x < ctx->outWidth; x++) { + const uint8_t gray = adjustPixel((ctx->rowCount[x] > 0) ? (ctx->rowAccum[x] / ctx->rowCount[x]) : 0); + uint8_t twoBit; + if (ctx->atkinsonDitherer) { + twoBit = ctx->atkinsonDitherer->processPixel(gray, x); + } else if (ctx->fsDitherer) { + twoBit = ctx->fsDitherer->processPixel(gray, x); + } else { + twoBit = quantize(gray, x, ctx->currentOutY); + } + ctx->bmpRow[(x * 2) / 8] |= (twoBit << (6 - ((x * 2) % 8))); } + if (ctx->atkinsonDitherer) + ctx->atkinsonDitherer->nextRow(); + else if (ctx->fsDitherer) + ctx->fsDitherer->nextRow(); + } + + ctx->bmpOut->write(ctx->bmpRow, ctx->bytesPerRow); + ctx->currentOutY++; +} + +// JPEGDEC draw callback — receives one MCU-width × MCU-height block at a time, +// in left-to-right, top-to-bottom order (baseline JPEG). +// Accumulates columns into mcuBuf; once the last column arrives (completing the MCU +// row), applies scaling + dithering and writes packed BMP rows to bmpOut. +int bmpDrawCallback(JPEGDRAW* pDraw) { + auto* ctx = reinterpret_cast(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; } - // 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; + 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); + } + + // 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; + } - memcpy(pBuf, context->buffer + context->bufferPos, toRead); - context->bufferPos += toRead; - *pBytes_actually_read = static_cast(toRead); + // 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 0; // Success + 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, 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; + } + + s_jpegFile = &jpegFile; - // 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); + 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,290 +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) + jpeg->setPixelType(EIGHT_BIT_GRAYSCALE); + jpeg->setUserPointer(&ctx); - 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) - } - - // 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++) { - if (deadline != 0 && static_cast(millis() - deadline) >= 0) { - LOG_ERR("JPG", "Decode deadline exceeded at MCU row %d", mcuY); - return false; - } - - // 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; - } - - // 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; - } + rc = jpeg->decode(0, 0, 0); - // 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"); diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.h b/lib/JpegToBmpConverter/JpegToBmpConverter.h index bf1443c74..13257fdd6 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.h +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.h @@ -6,8 +6,6 @@ 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, 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