Cache cover

This commit is contained in:
jpirnay
2026-05-18 21:23:31 +02:00
parent af9e37f561
commit d4b2a24e2a
2 changed files with 132 additions and 165 deletions
+130 -165
View File
@@ -683,8 +683,9 @@ bool Epub::clearCache(const bool preserveThumbs) const {
f.close();
const std::string name(nameBuf);
// Keep thumbnail and cover BMPs — regenerating them is expensive.
if (FsHelpers::hasBmpExtension(name)) continue;
// Keep thumbnail and cover BMPs, and the cached raw cover image — all are
// expensive to regenerate (require ZIP decompression or format conversion).
if (FsHelpers::hasBmpExtension(name) || name == "cover.img") continue;
const std::string fullPath = cachePath + "/" + name;
if (!Storage.remove(fullPath.c_str())) {
@@ -766,14 +767,45 @@ std::string Epub::getCoverBmpPath(bool cropped) const {
return cachePath + "/" + coverFileName + ".bmp";
}
bool Epub::generateCoverBmp(bool cropped) const {
// Already generated, return true
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
return true;
std::string Epub::getCoverImageCachePath() const { return cachePath + "/cover.img"; }
bool Epub::ensureCoverImageCached() const {
const auto coverCachePath = getCoverImageCachePath();
if (Storage.exists(coverCachePath.c_str())) return true;
// Sidecar cover: a .jpg/.jpeg/.png/.bmp file alongside the EPUB takes priority
// over the embedded cover image (same resolution, no ZIP decompression needed).
const auto sep = filepath.find_last_of("/\\");
const auto dot = filepath.rfind('.');
if (dot != std::string::npos && (sep == std::string::npos || dot > sep)) {
const std::string base = filepath.substr(0, dot);
for (const char* ext : {".jpg", ".jpeg", ".png", ".bmp"}) {
const std::string candidate = base + ext;
if (Storage.exists(candidate.c_str())) {
LOG_DBG("EBP", "Using sidecar cover: %s", candidate.c_str());
FsFile src;
if (!Storage.openFileForRead("EBP", candidate, src)) break;
FsFile dst;
if (!Storage.openFileForWrite("EBP", coverCachePath, dst)) {
src.close();
break;
}
uint8_t buf[512];
int n;
while ((n = src.read(buf, sizeof(buf))) > 0) dst.write(buf, n);
src.close();
dst.close();
if (Storage.exists(coverCachePath.c_str())) {
LOG_DBG("EBP", "Sidecar cover cached: %s", coverCachePath.c_str());
return true;
}
break;
}
}
}
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
LOG_ERR("EBP", "Cannot extract cover image, cache not loaded");
return false;
}
@@ -783,42 +815,48 @@ bool Epub::generateCoverBmp(bool cropped) const {
return false;
}
const auto coverTempPath = getCachePath() + "/.cover.img";
FsFile coverFile;
if (!Storage.openFileForWrite("EBP", coverCachePath, coverFile)) return false;
FsFile coverImage;
if (!Storage.openFileForWrite("EBP", coverTempPath, coverImage)) {
return false;
}
if (!readItemContentsToStream(coverImageHref, coverImage, 1024)) {
if (!readItemContentsToStream(coverImageHref, coverFile, 1024)) {
LOG_ERR("EBP", "Failed to read cover image from EPUB: %s", coverImageHref.c_str());
coverImage.close();
Storage.remove(coverTempPath.c_str());
coverFile.close();
Storage.remove(coverCachePath.c_str());
return false;
}
coverImage.close();
coverFile.close();
if (!Storage.openFileForRead("EBP", coverTempPath, coverImage)) {
return false;
}
if (coverImage.size() == 0) {
if (!Storage.openFileForRead("EBP", coverCachePath, coverFile)) return false;
const bool empty = coverFile.size() == 0;
coverFile.close();
if (empty) {
LOG_ERR("EBP", "Cover image extracted as empty file: %s", coverImageHref.c_str());
coverImage.close();
Storage.remove(coverTempPath.c_str());
Storage.remove(coverCachePath.c_str());
return false;
}
LOG_DBG("EBP", "Cover image cached: %s", coverCachePath.c_str());
return true;
}
bool Epub::generateCoverBmp(bool cropped) const {
if (Storage.exists(getCoverBmpPath(cropped).c_str())) return true;
if (!ensureCoverImageCached()) return false;
const auto coverCachePath = getCoverImageCachePath();
FsFile coverImage;
if (!Storage.openFileForRead("EBP", coverCachePath, coverImage)) return false;
const auto detectedFormat = detectCoverImageFormat(coverImage);
if (detectedFormat == CoverImageFormat::Jpeg) {
LOG_DBG("EBP", "Generating BMP from JPEG cover image (%s mode)", cropped ? "cropped" : "fit");
} else if (detectedFormat == CoverImageFormat::Png) {
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
} else {
LOG_ERR("EBP", "Cover image has unsupported format: %s", coverImageHref.c_str());
LOG_ERR("EBP", "Cover image has unsupported format");
coverImage.close();
Storage.remove(coverTempPath.c_str());
return false;
}
@@ -837,7 +875,6 @@ bool Epub::generateCoverBmp(bool cropped) const {
coverImage.close();
coverBmp.close();
Storage.remove(coverTempPath.c_str());
if (!success) {
LOG_ERR("EBP", "Failed to generate BMP from cover image");
@@ -855,167 +892,95 @@ std::string Epub::getThumbBmpPath(int width, int height) const {
}
bool Epub::generateThumbBmp(int height) const {
// Already generated, return true
if (Storage.exists(getThumbBmpPath(height).c_str())) {
return true;
}
if (Storage.exists(getThumbBmpPath(height).c_str())) return true;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
if (!ensureCoverImageCached()) {
// Write an empty sentinel so we don't retry on every call
FsFile thumbBmp;
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
thumbBmp.close();
return false;
}
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) {
LOG_DBG("EBP", "No known cover image for thumbnail");
} else {
const auto coverTempPath = getCachePath() + "/.cover.img";
FsFile coverImage;
if (!Storage.openFileForWrite("EBP", coverTempPath, coverImage)) {
return false;
}
if (!readItemContentsToStream(coverImageHref, coverImage, 1024)) {
LOG_ERR("EBP", "Failed to read cover image for thumbnail: %s", coverImageHref.c_str());
coverImage.close();
Storage.remove(coverTempPath.c_str());
return false;
}
FsFile coverImage;
if (!Storage.openFileForRead("EBP", getCoverImageCachePath(), coverImage)) return false;
const auto detectedFormat = detectCoverImageFormat(coverImage);
if (detectedFormat == CoverImageFormat::Unknown) {
LOG_ERR("EBP", "Cached cover image is not a supported format");
coverImage.close();
if (!Storage.openFileForRead("EBP", coverTempPath, coverImage)) {
return false;
}
if (coverImage.size() == 0) {
LOG_ERR("EBP", "Cover image for thumbnail extracted as empty file: %s", coverImageHref.c_str());
coverImage.close();
Storage.remove(coverTempPath.c_str());
return false;
}
const auto detectedFormat = detectCoverImageFormat(coverImage);
if (detectedFormat == CoverImageFormat::Unknown) {
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail: %s", coverImageHref.c_str());
coverImage.close();
Storage.remove(coverTempPath.c_str());
return false;
}
FsFile thumbBmp;
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
coverImage.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)
int THUMB_TARGET_WIDTH = height * 0.6;
int THUMB_TARGET_HEIGHT = height;
bool success = false;
if (detectedFormat == CoverImageFormat::Jpeg) {
LOG_DBG("EBP", "Generating thumb BMP from JPEG cover image");
success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverImage, thumbBmp, THUMB_TARGET_WIDTH,
THUMB_TARGET_HEIGHT);
} else {
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverImage, thumbBmp, THUMB_TARGET_WIDTH,
THUMB_TARGET_HEIGHT);
}
coverImage.close();
thumbBmp.close();
Storage.remove(coverTempPath.c_str());
if (!success) {
LOG_ERR("EBP", "Failed to generate thumb BMP from cover image");
Storage.remove(getThumbBmpPath(height).c_str());
}
LOG_DBG("EBP", "Generated thumb BMP from cover image, success: %s", success ? "yes" : "no");
return success;
return false;
}
// Write an empty bmp file to avoid generation attempts in the future
FsFile thumbBmp;
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
coverImage.close();
return false;
}
const int thumbW = static_cast<int>(height * 0.6f);
bool success = false;
if (detectedFormat == CoverImageFormat::Jpeg) {
LOG_DBG("EBP", "Generating thumb BMP from JPEG cover image");
success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverImage, thumbBmp, thumbW, height);
} else {
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverImage, thumbBmp, thumbW, height);
}
coverImage.close();
thumbBmp.close();
return false;
if (!success) {
LOG_ERR("EBP", "Failed to generate thumb BMP from cover image");
Storage.remove(getThumbBmpPath(height).c_str());
}
LOG_DBG("EBP", "Generated thumb BMP from cover image, success: %s", success ? "yes" : "no");
return success;
}
bool Epub::generateThumbBmp(int width, int height) const {
if (Storage.exists(getThumbBmpPath(width, height).c_str())) return true;
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
if (!ensureCoverImageCached()) {
// Write an empty sentinel so we don't retry on every call
FsFile thumbBmp;
Storage.openFileForWrite("EBP", getThumbBmpPath(width, height), thumbBmp);
thumbBmp.close();
return false;
}
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
if (coverImageHref.empty()) {
LOG_DBG("EBP", "No known cover image for thumbnail");
} else {
const auto coverTempPath = getCachePath() + "/.cover";
FsFile coverTemp;
if (!Storage.openFileForWrite("EBP", coverTempPath, coverTemp)) return false;
if (!readItemContentsToStream(coverImageHref, coverTemp, 1024)) {
coverTemp.close();
Storage.remove(coverTempPath.c_str());
return false;
}
coverTemp.close();
FsFile coverImage;
if (!Storage.openFileForRead("EBP", getCoverImageCachePath(), coverImage)) return false;
if (!Storage.openFileForRead("EBP", coverTempPath, coverTemp)) {
Storage.remove(coverTempPath.c_str());
return false;
}
const auto detectedFormat = detectCoverImageFormat(coverTemp);
bool success = false;
FsFile thumbBmp;
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(width, height), thumbBmp)) {
coverTemp.close();
Storage.remove(coverTempPath.c_str());
return false;
}
if (detectedFormat == CoverImageFormat::Jpeg) {
LOG_DBG("EBP", "Generating %dx%d thumb BMP from JPEG cover image", width, height);
success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverTemp, thumbBmp, width, height);
} else if (detectedFormat == CoverImageFormat::Png) {
LOG_DBG("EBP", "Generating %dx%d thumb BMP from PNG cover image", width, height);
success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverTemp, thumbBmp, width, height);
} else {
LOG_DBG("EBP", "Cover image format unknown, attempting JPEG then PNG: %s", coverImageHref.c_str());
success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverTemp, thumbBmp, width, height);
if (!success) {
thumbBmp.close();
Storage.remove(getThumbBmpPath(width, height).c_str());
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(width, height), thumbBmp)) {
coverTemp.close();
Storage.remove(coverTempPath.c_str());
return false;
}
coverTemp.seek(0);
success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverTemp, thumbBmp, width, height);
}
}
coverTemp.close();
thumbBmp.close();
Storage.remove(coverTempPath.c_str());
if (!success) Storage.remove(getThumbBmpPath(width, height).c_str());
LOG_DBG("EBP", "Generated %dx%d thumb BMP from cover image, success: %s", width, height, success ? "yes" : "no");
return success;
const auto detectedFormat = detectCoverImageFormat(coverImage);
if (detectedFormat == CoverImageFormat::Unknown) {
LOG_ERR("EBP", "Cached cover image is not a supported format");
coverImage.close();
return false;
}
// Write empty sentinel to avoid repeated generation attempts
FsFile thumbBmp;
Storage.openFileForWrite("EBP", getThumbBmpPath(width, height), thumbBmp);
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(width, height), thumbBmp)) {
coverImage.close();
return false;
}
bool success = false;
if (detectedFormat == CoverImageFormat::Jpeg) {
LOG_DBG("EBP", "Generating %dx%d thumb BMP from JPEG cover image", width, height);
success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverImage, thumbBmp, width, height);
} else {
LOG_DBG("EBP", "Generating %dx%d thumb BMP from PNG cover image", width, height);
success = PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverImage, thumbBmp, width, height);
}
coverImage.close();
thumbBmp.close();
return false;
if (!success) Storage.remove(getThumbBmpPath(width, height).c_str());
LOG_DBG("EBP", "Generated %dx%d thumb BMP from cover image, success: %s", width, height, success ? "yes" : "no");
return success;
}
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
+2
View File
@@ -59,6 +59,8 @@ class Epub {
const std::string& getSeriesIndex() const;
const std::string& getDescription() const;
std::string getCoverBmpPath(bool cropped = false) const;
std::string getCoverImageCachePath() const;
bool ensureCoverImageCached() const;
bool generateCoverBmp(bool cropped = false) const;
std::string getThumbBmpPath() const;
std::string getThumbBmpPath(int height) const;