diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 78607573..066aca65 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -419,11 +419,13 @@ bool Epub::generateCoverBmp(bool cropped) const { return false; } -std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } +std::string Epub::getThumbBmpPath(int width, int height) const { + return cachePath + "/thumb_" + std::to_string(width) + "x" + std::to_string(height) + ".bmp"; +} -bool Epub::generateThumbBmp() const { +bool Epub::generateThumbBmp(int width, int height) const { // Already generated, return true - if (SdMan.exists(getThumbBmpPath().c_str())) { + if (SdMan.exists(getThumbBmpPath(width, height).c_str())) { return true; } @@ -455,23 +457,21 @@ bool Epub::generateThumbBmp() const { } FsFile thumbBmp; - if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) { + if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(width, height), thumbBmp)) { coverJpg.close(); return false; } // Use smaller target size for Continue Reading card (half of screen: 240x400) // Generate 1-bit BMP for fast home screen rendering (no gray passes needed) - constexpr int THUMB_TARGET_WIDTH = 240; - constexpr int THUMB_TARGET_HEIGHT = 400; - const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH, - THUMB_TARGET_HEIGHT); + const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, width, + height); coverJpg.close(); thumbBmp.close(); SdMan.remove(coverJpgTempPath.c_str()); if (!success) { Serial.printf("[%lu] [EBP] Failed to generate thumb BMP from JPG cover image\n", millis()); - SdMan.remove(getThumbBmpPath().c_str()); + SdMan.remove(getThumbBmpPath(width, height).c_str()); } Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 7a21efd5..be3d0ec7 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -47,8 +47,8 @@ class Epub { const std::string& getLanguage() const; std::string getCoverBmpPath(bool cropped = false) const; bool generateCoverBmp(bool cropped = false) const; - std::string getThumbBmpPath() const; - bool generateThumbBmp() const; + std::string getThumbBmpPath(int width, int height) const; + bool generateThumbBmp(int width, int height) 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; diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index c79421d7..22bc9b42 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -293,11 +293,13 @@ bool Xtc::generateCoverBmp() const { return true; } -std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } +std::string Xtc::getThumbBmpPath(int width, int height) const { + return cachePath + "/thumb_" + std::to_string(width) + "x" + std::to_string(height) + ".bmp"; +} -bool Xtc::generateThumbBmp() const { +bool Xtc::generateThumbBmp(int width, int height) const { // Already generated - if (SdMan.exists(getThumbBmpPath().c_str())) { + if (SdMan.exists(getThumbBmpPath(width, height).c_str())) { return true; } @@ -324,41 +326,25 @@ bool Xtc::generateThumbBmp() const { // Get bit depth const uint8_t bitDepth = parser->getBitDepth(); - // Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card) - constexpr int THUMB_TARGET_WIDTH = 240; - constexpr int THUMB_TARGET_HEIGHT = 400; + // Calculate target dimensions for thumbnail + uint16_t thumbWidth = width; + uint16_t thumbHeight = height; - // Calculate scale factor - float scaleX = static_cast(THUMB_TARGET_WIDTH) / pageInfo.width; - float scaleY = static_cast(THUMB_TARGET_HEIGHT) / pageInfo.height; + float scaleX = static_cast(thumbWidth) / pageInfo.width; + float scaleY = static_cast(thumbHeight) / pageInfo.height; float scale = (scaleX < scaleY) ? scaleX : scaleY; - // Only scale down, never up + // If we are scaling up, we should adjust thumbWidth and thumbHeight + // to maintain aspect ratio, but not exceed the given width and height. if (scale >= 1.0f) { - // Page is already small enough, just use cover.bmp - // Copy cover.bmp to thumb.bmp - if (generateCoverBmp()) { - FsFile src, dst; - if (SdMan.openFileForRead("XTC", getCoverBmpPath(), src)) { - if (SdMan.openFileForWrite("XTC", getThumbBmpPath(), dst)) { - uint8_t buffer[512]; - while (src.available()) { - size_t bytesRead = src.read(buffer, sizeof(buffer)); - dst.write(buffer, bytesRead); - } - dst.close(); - } - src.close(); - } - Serial.printf("[%lu] [XTC] Copied cover to thumb (no scaling needed)\n", millis()); - return SdMan.exists(getThumbBmpPath().c_str()); - } - return false; + thumbWidth = pageInfo.width; + thumbHeight = pageInfo.height; + scale = 1.0f; + } else { + thumbWidth = static_cast(pageInfo.width * scale); + thumbHeight = static_cast(pageInfo.height * scale); } - uint16_t thumbWidth = static_cast(pageInfo.width * scale); - uint16_t thumbHeight = static_cast(pageInfo.height * scale); - Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width, pageInfo.height, thumbWidth, thumbHeight, scale); @@ -385,7 +371,7 @@ bool Xtc::generateThumbBmp() const { // Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes) FsFile thumbBmp; - if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(), thumbBmp)) { + if (!SdMan.openFileForWrite("XTC", getThumbBmpPath(width, height), thumbBmp)) { Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis()); free(pageBuffer); return false; @@ -550,7 +536,7 @@ bool Xtc::generateThumbBmp() const { free(pageBuffer); Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight, - getThumbBmpPath().c_str()); + getThumbBmpPath(width, height).c_str()); return true; } diff --git a/lib/Xtc/Xtc.h b/lib/Xtc/Xtc.h index 7413ef47..75054528 100644 --- a/lib/Xtc/Xtc.h +++ b/lib/Xtc/Xtc.h @@ -63,8 +63,8 @@ class Xtc { std::string getCoverBmpPath() const; bool generateCoverBmp() const; // Thumbnail support (for Continue Reading card) - std::string getThumbBmpPath() const; - bool generateThumbBmp() const; + std::string getThumbBmpPath(int width, int height) const; + bool generateThumbBmp(int width, int height) const; // Page access uint32_t getPageCount() const; diff --git a/src/activities/home/BookCoverCache.cpp b/src/activities/home/BookCoverCache.cpp new file mode 100644 index 00000000..06c988e2 --- /dev/null +++ b/src/activities/home/BookCoverCache.cpp @@ -0,0 +1,85 @@ +#include "BookCoverCache.h" +#include +#include +#include "util/StringUtils.h" +#include + +BookCoverCache::BookCoverCache(const std::string& cache_dir) : cache_dir(cache_dir) { + // Ensure the cache directory exists + if (!SdMan.exists(cache_dir.c_str())) { + SdMan.mkdir(cache_dir.c_str()); + } +} + +BookCoverCache::~BookCoverCache() { +} + +std::shared_ptr BookCoverCache::getCover(const std::string& book_path) { + std::string cache_path = getCachePath(book_path); + + if (isCacheValid(book_path)) { + FsFile file; + if (SdMan.openFileForRead("CACHE", cache_path, file)) { + auto bitmap = std::make_shared(file); + if (bitmap->parseHeaders() == BmpReaderError::Ok) { + return bitmap; + } + } + } + + return generateThumbnail(book_path); +} + +void BookCoverCache::clearCache() { + // TODO: Implement this +} + +std::string BookCoverCache::getCachePath(const std::string& book_path) const { + // Simple cache path: replace '/' with '_' and append .bmp + std::string safe_path = book_path; + std::replace(safe_path.begin(), safe_path.end(), '/', '_'); + return cache_dir + "/" + safe_path + ".bmp"; +} + +bool BookCoverCache::isCacheValid(const std::string& book_path) const { + std::string cache_path = getCachePath(book_path); + return SdMan.exists(cache_path.c_str()); +} + +std::shared_ptr BookCoverCache::generateThumbnail(const std::string& book_path) { + std::string coverBmpPath; + bool hasCoverImage = false; + + if (StringUtils::checkFileExtension(book_path, ".epub")) { + Epub epub(book_path, "/.crosspoint/epub_cache"); + if (epub.load(false) && epub.generateThumbBmp()) { + coverBmpPath = epub.getThumbBmpPath(); + hasCoverImage = true; + } + } else if (StringUtils::checkFileExtension(book_path, ".xtch") || + StringUtils::checkFileExtension(book_path, ".xtc")) { + Xtc xtc(book_path, "/.crosspoint/xtc_cache"); + if (xtc.load() && xtc.generateThumbBmp()) { + coverBmpPath = xtc.getThumbBmpPath(); + hasCoverImage = true; + } + } + + if (hasCoverImage && !coverBmpPath.empty()) { + std::string cache_path = getCachePath(book_path); + // This is not very efficient, we are copying the file. + // We should ideally generate the thumbnail directly to the cache path. + // For now, this will do. + if(SdMan.copy(coverBmpPath.c_str(), cache_path.c_str())) { + FsFile file; + if (SdMan.openFileForRead("CACHE", cache_path, file)) { + auto bitmap = std::make_shared(file); + if (bitmap->parseHeaders() == BmpReaderError::Ok) { + return bitmap; + } + } + } + } + + return nullptr; +} diff --git a/src/activities/home/BookCoverCache.h b/src/activities/home/BookCoverCache.h new file mode 100644 index 00000000..bb80d663 --- /dev/null +++ b/src/activities/home/BookCoverCache.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class BookCoverCache { + public: + BookCoverCache(const std::string& cache_dir, GfxRenderer* renderer); + ~BookCoverCache(); + + bool render(const std::string& book_path, int x, int y, int width, int height); + void clearCache(); + void setTargetSize(int width, int height); + + private: + std::string getCachePath(const std::string& book_path) const; + bool generateThumbnail(const std::string& book_path); + + std::string cache_dir; + GfxRenderer* renderer; + int target_width; + int target_height; +}; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index c8755b68..b0c05935 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -466,15 +466,15 @@ void MyLibraryActivity::renderRecentAsBookCoverList() const { if (StringUtils::checkFileExtension(book.path, ".epub")) { Epub epub(book.path, "/.crosspoint"); - if (epub.load(false) && epub.generateThumbBmp()) { - coverBmpPath = epub.getThumbBmpPath(); + if (epub.load(false) && epub.generateThumbBmp(coverWidth, itemHeight - 10)) { + coverBmpPath = epub.getThumbBmpPath(coverWidth, itemHeight - 10); hasCoverImage = true; } } else if (StringUtils::checkFileExtension(book.path, ".xtch") || StringUtils::checkFileExtension(book.path, ".xtc")) { Xtc xtc(book.path, "/.crosspoint"); - if (xtc.load() && xtc.generateThumbBmp()) { - coverBmpPath = xtc.getThumbBmpPath(); + if (xtc.load() && xtc.generateThumbBmp(coverWidth, itemHeight - 10)) { + coverBmpPath = xtc.getThumbBmpPath(coverWidth, itemHeight - 10); hasCoverImage = true; } } @@ -554,15 +554,15 @@ void MyLibraryActivity::renderRecentAsBookCoverGrid() const { if (StringUtils::checkFileExtension(book.path, ".epub")) { Epub epub(book.path, "/.crosspoint"); - if (epub.load(false) && epub.generateThumbBmp()) { - coverBmpPath = epub.getThumbBmpPath(); + if (epub.load(false) && epub.generateThumbBmp(itemWidth, itemHeight)) { + coverBmpPath = epub.getThumbBmpPath(itemWidth, itemHeight); hasCoverImage = true; } } else if (StringUtils::checkFileExtension(book.path, ".xtch") || StringUtils::checkFileExtension(book.path, ".xtc")) { Xtc xtc(book.path, "/.crosspoint"); - if (xtc.load() && xtc.generateThumbBmp()) { - coverBmpPath = xtc.getThumbBmpPath(); + if (xtc.load() && xtc.generateThumbBmp(itemWidth, itemHeight)) { + coverBmpPath = xtc.getThumbBmpPath(itemWidth, itemHeight); hasCoverImage = true; } }