new thumb cache

This commit is contained in:
Eliz Kilic 2026-01-24 00:11:10 +00:00
parent 3ec8c1f23a
commit fc44b8f9ed
7 changed files with 153 additions and 55 deletions

View File

@ -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");

View File

@ -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;

View File

@ -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<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
float scaleX = static_cast<float>(thumbWidth) / pageInfo.width;
float scaleY = static_cast<float>(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<uint16_t>(pageInfo.width * scale);
thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
}
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
uint16_t thumbHeight = static_cast<uint16_t>(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;
}

View File

@ -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;

View File

@ -0,0 +1,85 @@
#include "BookCoverCache.h"
#include <Epub.h>
#include <Xtc.h>
#include "util/StringUtils.h"
#include <sys/stat.h>
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<Bitmap> 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<Bitmap>(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<Bitmap> 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<Bitmap>(file);
if (bitmap->parseHeaders() == BmpReaderError::Ok) {
return bitmap;
}
}
}
}
return nullptr;
}

View File

@ -0,0 +1,27 @@
#pragma once
#include <string>
#include <memory>
#include <vector>
#include <Bitmap.h>
#include <SDCardManager.h>
#include <GfxRenderer.h>
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;
};

View File

@ -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;
}
}