diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 9a2fbc07..974b0f44 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -1,11 +1,14 @@ #include "Epub.h" #include +#include #include #include #include #include +#include + #include "Epub/parsers/ContainerParser.h" #include "Epub/parsers/ContentOpfParser.h" #include "Epub/parsers/TocNavParser.h" @@ -440,9 +443,18 @@ std::string Epub::getCoverBmpPath(bool cropped) const { } bool Epub::generateCoverBmp(bool cropped) const { + bool invalid = false; // Already generated, return true if (Storage.exists(getCoverBmpPath(cropped).c_str())) { - return true; + // is this a valid cover or just an empty file we created to mark generation attempts? + invalid = !isValidThumbnailBmp(getCoverBmpPath(cropped)); + if (invalid) { + // Remove the old invalid cover so we can attempt to generate a new one + Storage.remove(getCoverBmpPath(cropped).c_str()); + LOG_DBG("EBP", "Previous cover generation attempt failed for %s mode, retrying", cropped ? "cropped" : "fit"); + } else { + return true; + } } if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { @@ -451,13 +463,33 @@ bool Epub::generateCoverBmp(bool cropped) const { } const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref; + std::string effectiveCoverImageHref = coverImageHref; if (coverImageHref.empty()) { + // Fallback: try common cover filenames + std::vector coverCandidates = getCoverCandidates(); + for (const auto& candidate : coverCandidates) { + effectiveCoverImageHref = candidate; + // Try to read a small amount to check if exists + uint8_t* test = readItemContentsToBytes(candidate, nullptr, false); + if (test) { + free(test); + break; + } else { + effectiveCoverImageHref.clear(); + } + } + } + if (effectiveCoverImageHref.empty()) { LOG_ERR("EBP", "No known cover image"); return false; } - if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || - coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") { + // Check for JPG/JPEG extensions (case insensitive) + std::string lowerHref = effectiveCoverImageHref; + std::transform(lowerHref.begin(), lowerHref.end(), lowerHref.begin(), ::tolower); + bool isJpg = + lowerHref.substr(lowerHref.length() - 4) == ".jpg" || lowerHref.substr(lowerHref.length() - 5) == ".jpeg"; + if (isJpg) { LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit"); const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; @@ -465,7 +497,7 @@ bool Epub::generateCoverBmp(bool cropped) const { if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { return false; } - readItemContentsToStream(coverImageHref, coverJpg, 1024); + readItemContentsToStream(effectiveCoverImageHref, coverJpg, 1024); coverJpg.close(); if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { @@ -499,9 +531,18 @@ std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT]. std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; } bool Epub::generateThumbBmp(int height) const { + bool invalid = false; // Already generated, return true if (Storage.exists(getThumbBmpPath(height).c_str())) { - return true; + // is this a valid thumbnail or just an empty file we created to mark generation attempts? + invalid = !isValidThumbnailBmp(getThumbBmpPath(height)); + if (invalid) { + // Remove the old invalid thumbnail so we can attempt to generate a new one + Storage.remove(getThumbBmpPath(height).c_str()); + LOG_DBG("EBP", "Previous thumbnail generation attempt failed for height %d, retrying", height); + } else { + return true; + } } if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { @@ -510,52 +551,246 @@ bool Epub::generateThumbBmp(int height) const { } const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref; + std::string effectiveCoverImageHref = coverImageHref; if (coverImageHref.empty()) { + // Fallback: try common cover filenames + std::vector coverCandidates = getCoverCandidates(); + for (const auto& candidate : coverCandidates) { + effectiveCoverImageHref = candidate; + // Try to read a small amount to check if exists + uint8_t* test = readItemContentsToBytes(candidate, nullptr, false); + if (test) { + free(test); + break; + } else { + effectiveCoverImageHref.clear(); + } + } + } + if (effectiveCoverImageHref.empty()) { LOG_DBG("EBP", "No known cover image for thumbnail"); - } else if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || - coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") { - LOG_DBG("EBP", "Generating thumb BMP from JPG cover image"); - const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; - - FsFile coverJpg; - if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { - return false; - } - readItemContentsToStream(coverImageHref, coverJpg, 1024); - coverJpg.close(); - - if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { - return false; - } - - FsFile thumbBmp; - if (!Storage.openFileForWrite("EBP", getThumbBmpPath(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) - 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); - coverJpg.close(); - thumbBmp.close(); - Storage.remove(coverJpgTempPath.c_str()); - - if (!success) { - LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image"); - Storage.remove(getThumbBmpPath(height).c_str()); - } - LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no"); - return success; } else { - LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail"); + // Check for JPG/JPEG extensions (case insensitive) + std::string lowerHref = effectiveCoverImageHref; + std::transform(lowerHref.begin(), lowerHref.end(), lowerHref.begin(), ::tolower); + bool isJpg = + lowerHref.substr(lowerHref.length() - 4) == ".jpg" || lowerHref.substr(lowerHref.length() - 5) == ".jpeg"; + if (isJpg) { + LOG_DBG("EBP", "Generating thumb BMP from JPG cover image"); + const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; + + FsFile coverJpg; + if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) { + return false; + } + readItemContentsToStream(effectiveCoverImageHref, coverJpg, 1024); + coverJpg.close(); + + if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) { + return false; + } + + FsFile thumbBmp; + if (!Storage.openFileForWrite("EBP", getThumbBmpPath(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) + 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); + coverJpg.close(); + thumbBmp.close(); + Storage.remove(coverJpgTempPath.c_str()); + + if (!success) { + LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image"); + Storage.remove(getThumbBmpPath(height).c_str()); + } + LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no"); + return success; + } else { + LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail"); + } } return false; } +bool Epub::generateInvalidFormatThumbBmp(int height) const { + // Create a simple 1-bit BMP with an X pattern to indicate invalid format. + // This BMP is a valid 1-bit file used as a marker to prevent repeated + // generation attempts when conversion fails (e.g., progressive JPG). + const int width = height * 0.6; // Same aspect ratio as normal thumbnails + const int rowBytes = ((width + 31) / 32) * 4; // 1-bit rows padded to 4-byte boundary + const int imageSize = rowBytes * height; + const int fileSize = 14 + 40 + 8 + imageSize; // Header + DIB + palette + data + const int dataOffset = 14 + 40 + 8; + + FsFile thumbBmp; + if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) { + return false; + } + + // BMP file header (14 bytes) + thumbBmp.write('B'); + thumbBmp.write('M'); + thumbBmp.write(reinterpret_cast(&fileSize), 4); + uint32_t reserved = 0; + thumbBmp.write(reinterpret_cast(&reserved), 4); + thumbBmp.write(reinterpret_cast(&dataOffset), 4); + + // DIB header (BITMAPINFOHEADER - 40 bytes) + uint32_t dibHeaderSize = 40; + thumbBmp.write(reinterpret_cast(&dibHeaderSize), 4); + int32_t bmpWidth = width; + thumbBmp.write(reinterpret_cast(&bmpWidth), 4); + int32_t bmpHeight = -height; // Negative for top-down + thumbBmp.write(reinterpret_cast(&bmpHeight), 4); + uint16_t planes = 1; + thumbBmp.write(reinterpret_cast(&planes), 2); + uint16_t bitsPerPixel = 1; + thumbBmp.write(reinterpret_cast(&bitsPerPixel), 2); + uint32_t compression = 0; + thumbBmp.write(reinterpret_cast(&compression), 4); + thumbBmp.write(reinterpret_cast(&imageSize), 4); + int32_t ppmX = 2835; // 72 DPI + thumbBmp.write(reinterpret_cast(&ppmX), 4); + int32_t ppmY = 2835; + thumbBmp.write(reinterpret_cast(&ppmY), 4); + uint32_t colorsUsed = 2; + thumbBmp.write(reinterpret_cast(&colorsUsed), 4); + uint32_t colorsImportant = 2; + thumbBmp.write(reinterpret_cast(&colorsImportant), 4); + + // Color palette (2 colors for 1-bit) + uint8_t black[4] = {0x00, 0x00, 0x00, 0x00}; // Color 0: Black + thumbBmp.write(black, 4); + uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00}; // Color 1: White + thumbBmp.write(white, 4); + + // Generate X pattern bitmap data + // In BMP, 0 = black (first color in palette), 1 = white + // We'll draw black pixels on white background + for (int y = 0; y < height; y++) { + std::vector rowData(rowBytes, 0xFF); // Initialize to all white (1s) + + // Map this row to a horizontal position for diagonals + const int scaledY = (y * width) / height; + const int thickness = 2; // thickness of diagonal lines in pixels + + for (int x = 0; x < width; x++) { + bool drawPixel = false; + // Main diagonal (top-left to bottom-right) + if (std::abs(x - scaledY) <= thickness) drawPixel = true; + // Other diagonal (top-right to bottom-left) + if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true; + + if (drawPixel) { + const int byteIndex = x / 8; + const int bitIndex = 7 - (x % 8); // MSB first + rowData[byteIndex] &= static_cast(~(1 << bitIndex)); + } + } + + // Write the row data + thumbBmp.write(rowData.data(), rowBytes); + } + + thumbBmp.close(); + LOG_DBG("EBP", "Generated invalid format thumbnail BMP"); + return true; +} + +bool Epub::generateInvalidFormatCoverBmp(bool cropped) const { + // Create a simple 1-bit BMP with an X pattern to indicate invalid format. + // This BMP is intentionally a valid image that visually indicates a + // malformed/unsupported cover image instead of leaving an empty marker + // file that would cause repeated generation attempts. + // Derive logical portrait dimensions from the display hardware constants + // EInkDisplay reports native panel orientation as 800x480; use min/max + const int hwW = HalDisplay::DISPLAY_WIDTH; + const int hwH = HalDisplay::DISPLAY_HEIGHT; + const int width = std::min(hwW, hwH); // logical portrait width (480) + const int height = std::max(hwW, hwH); // logical portrait height (800) + const int rowBytes = ((width + 31) / 32) * 4; // 1-bit rows padded to 4-byte boundary + const int imageSize = rowBytes * height; + const int fileSize = 14 + 40 + 8 + imageSize; // Header + DIB + palette + data + const int dataOffset = 14 + 40 + 8; + + FsFile coverBmp; + if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) { + return false; + } + + // BMP file header (14 bytes) + coverBmp.write('B'); + coverBmp.write('M'); + coverBmp.write(reinterpret_cast(&fileSize), 4); + uint32_t reserved = 0; + coverBmp.write(reinterpret_cast(&reserved), 4); + coverBmp.write(reinterpret_cast(&dataOffset), 4); + + // DIB header (BITMAPINFOHEADER - 40 bytes) + uint32_t dibHeaderSize = 40; + coverBmp.write(reinterpret_cast(&dibHeaderSize), 4); + int32_t bmpWidth = width; + coverBmp.write(reinterpret_cast(&bmpWidth), 4); + int32_t bmpHeight = -height; // Negative for top-down + coverBmp.write(reinterpret_cast(&bmpHeight), 4); + uint16_t planes = 1; + coverBmp.write(reinterpret_cast(&planes), 2); + uint16_t bitsPerPixel = 1; + coverBmp.write(reinterpret_cast(&bitsPerPixel), 2); + uint32_t compression = 0; + coverBmp.write(reinterpret_cast(&compression), 4); + coverBmp.write(reinterpret_cast(&imageSize), 4); + int32_t ppmX = 2835; // 72 DPI + coverBmp.write(reinterpret_cast(&ppmX), 4); + int32_t ppmY = 2835; + coverBmp.write(reinterpret_cast(&ppmY), 4); + uint32_t colorsUsed = 2; + coverBmp.write(reinterpret_cast(&colorsUsed), 4); + uint32_t colorsImportant = 2; + coverBmp.write(reinterpret_cast(&colorsImportant), 4); + + // Color palette (2 colors for 1-bit) + uint8_t black[4] = {0x00, 0x00, 0x00, 0x00}; // Color 0: Black + coverBmp.write(black, 4); + uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00}; // Color 1: White + coverBmp.write(white, 4); + + // Generate X pattern bitmap data + // In BMP, 0 = black (first color in palette), 1 = white + // We'll draw black pixels on white background + for (int y = 0; y < height; y++) { + std::vector rowData(rowBytes, 0xFF); // Initialize to all white (1s) + + const int scaledY = (y * width) / height; + const int thickness = 6; // thicker lines for full-cover visibility + + for (int x = 0; x < width; x++) { + bool drawPixel = false; + if (std::abs(x - scaledY) <= thickness) drawPixel = true; + if (std::abs(x - (width - 1 - scaledY)) <= thickness) drawPixel = true; + + if (drawPixel) { + const int byteIndex = x / 8; + const int bitIndex = 7 - (x % 8); + rowData[byteIndex] &= static_cast(~(1 << bitIndex)); + } + } + + coverBmp.write(rowData.data(), rowBytes); + } + + coverBmp.close(); + LOG_DBG("EBP", "Generated invalid format cover BMP"); + return true; +} + uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const { if (itemHref.empty()) { LOG_DBG("EBP", "Failed to read item, empty href"); @@ -703,3 +938,45 @@ float Epub::calculateProgress(const int currentSpineIndex, const float currentSp const float totalProgress = static_cast(prevChapterSize) + sectionProgSize; return totalProgress / static_cast(bookSize); } + +bool Epub::isValidThumbnailBmp(const std::string& bmpPath) { + if (!Storage.exists(bmpPath.c_str())) { + return false; + } + FsFile file = Storage.open(bmpPath.c_str()); + if (!file) { + LOG_ERR("EBP", "Failed to open thumbnail BMP at path: %s", bmpPath.c_str()); + return false; + } + size_t fileSize = file.size(); + if (fileSize == 0) { + // Empty file is a marker for "no cover available" + LOG_DBG("EBP", "Thumbnail BMP is empty (no cover marker) at path: %s", bmpPath.c_str()); + file.close(); + return false; + } + // BMP header starts with 'B' 'M' + uint8_t header[2]; + size_t bytesRead = file.read(header, 2); + if (bytesRead != 2) { + LOG_ERR("EBP", "Failed to read thumbnail BMP header at path: %s", bmpPath.c_str()); + file.close(); + return false; + } + LOG_DBG("EBP", "Thumbnail BMP header: %c%c", header[0], header[1]); + file.close(); + return header[0] == 'B' && header[1] == 'M'; +} + +std::vector Epub::getCoverCandidates() const { + std::vector coverDirectories = {".", "images", "Images", "OEBPS", "OEBPS/images", "OEBPS/Images"}; + std::vector coverExtensions = {".jpg", ".jpeg"}; // add ".png" when PNG cover support is implemented + std::vector coverCandidates; + for (const auto& ext : coverExtensions) { + for (const auto& dir : coverDirectories) { + std::string candidate = (dir == ".") ? "cover" + ext : dir + "/cover" + ext; + coverCandidates.push_back(candidate); + } + } + return coverCandidates; +} diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index cde9d210..ae9e8b4a 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -52,10 +52,23 @@ class Epub { const std::string& getAuthor() const; const std::string& getLanguage() const; std::string getCoverBmpPath(bool cropped = false) const; + // Generate a 1-bit BMP cover image from the EPUB cover image. + // Returns true on success. On conversion failure, callers may use + // `generateInvalidFormatCoverBmp` to create a valid marker BMP. bool generateCoverBmp(bool cropped = false) const; + // Create a valid 1-bit BMP that visually indicates an invalid/unsupported + // cover format (an X pattern). This prevents repeated generation attempts + // by providing a valid BMP file that `isValidThumbnailBmp` accepts. + bool generateInvalidFormatCoverBmp(bool cropped = false) const; std::string getThumbBmpPath() const; std::string getThumbBmpPath(int height) const; + // Generate a thumbnail BMP at the requested `height`. Returns true on + // successful conversion. If conversion fails, `generateInvalidFormatThumbBmp` + // can be used to write a valid marker image that prevents retries. bool generateThumbBmp(int height) const; + // Create a valid 1-bit thumbnail BMP with an X marker indicating an + // invalid/unsupported cover image instead of leaving an empty marker file. + bool generateInvalidFormatThumbBmp(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; @@ -72,4 +85,9 @@ class Epub { size_t getBookSize() const; float calculateProgress(int currentSpineIndex, float currentSpineRead) const; CssParser* getCssParser() const { return cssParser.get(); } + + static bool isValidThumbnailBmp(const std::string& bmpPath); + + private: + std::vector getCoverCandidates() const; }; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 7bb92c3d..426f23f7 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -642,11 +642,14 @@ void SleepActivity::renderCoverSleepScreen() const { if (!lastEpub.generateCoverBmp(cropped)) { LOG_DBG("SLP", "EPUB cover generation failed, trying placeholder"); - PlaceholderCoverGenerator::generate(lastEpub.getCoverBmpPath(cropped), lastEpub.getTitle(), - lastEpub.getAuthor(), 480, 800); + if (!PlaceholderCoverGenerator::generate(lastEpub.getCoverBmpPath(cropped), lastEpub.getTitle(), + lastEpub.getAuthor(), 480, 800)) { + LOG_DBG("SLP", "Placeholder generation failed, creating X-pattern marker"); + lastEpub.generateInvalidFormatCoverBmp(cropped); + } } - if (!Storage.exists(lastEpub.getCoverBmpPath(cropped).c_str())) { + if (!Epub::isValidThumbnailBmp(lastEpub.getCoverBmpPath(cropped))) { LOG_ERR("SLP", "Failed to generate cover bmp"); return (this->*renderNoCoverSleepScreen)(); } diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 1e42117f..290c5c29 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -65,7 +65,7 @@ void HomeActivity::loadRecentCovers(int coverHeight) { for (RecentBook& book : recentBooks) { if (!book.coverBmpPath.empty()) { std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); - if (!Storage.exists(coverPath.c_str())) { + if (!Epub::isValidThumbnailBmp(coverPath)) { if (!showingLoading) { showingLoading = true; popupRect = GUI.drawPopup(renderer, "Loading..."); @@ -74,21 +74,46 @@ void HomeActivity::loadRecentCovers(int coverHeight) { bool success = false; - // Try format-specific thumbnail generation first + // Try format-specific thumbnail generation first (Real Cover) if (StringUtils::checkFileExtension(book.path, ".epub")) { Epub epub(book.path, "/.crosspoint"); - epub.load(false, true); + // Try fast cache-only load first; only build cache if missing + if (!epub.load(false, true)) { + // Cache missing — build it (may take longer) + epub.load(true, true); + } success = epub.generateThumbBmp(coverHeight); + if (success) { + const std::string thumbPath = epub.getThumbBmpPath(coverHeight); + RECENT_BOOKS.updateBook(book.path, book.title, book.author, thumbPath); + book.coverBmpPath = thumbPath; + } else { + // Fallback: generate a placeholder thumbnail with title/author + const int thumbWidth = static_cast(coverHeight * 0.6); + success = PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight); + if (!success) { + // Last resort: X-pattern marker to prevent repeated generation attempts + epub.generateInvalidFormatThumbBmp(coverHeight); + } + } } else if (StringUtils::checkFileExtension(book.path, ".xtch") || StringUtils::checkFileExtension(book.path, ".xtc")) { Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { success = xtc.generateThumbBmp(coverHeight); + if (success) { + const std::string thumbPath = xtc.getThumbBmpPath(coverHeight); + RECENT_BOOKS.updateBook(book.path, book.title, book.author, thumbPath); + book.coverBmpPath = thumbPath; + } } - } - - // Fallback: generate a placeholder thumbnail with title/author - if (!success) { + if (!success) { + // Fallback: generate a placeholder thumbnail with title/author + const int thumbWidth = static_cast(coverHeight * 0.6); + PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight); + } + } else { + // Unknown format: generate a placeholder thumbnail const int thumbWidth = static_cast(coverHeight * 0.6); PlaceholderCoverGenerator::generate(coverPath, book.title, book.author, thumbWidth, coverHeight); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 610330e2..18e0d581 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -131,32 +131,41 @@ void EpubReaderActivity::onEnter() { GUI.fillPopupProgress(renderer, popupRect, completedSteps * 100 / totalSteps); }; - if (!Storage.exists(epub->getCoverBmpPath(false).c_str())) { + if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) { epub->generateCoverBmp(false); // Fallback: generate placeholder if real cover extraction failed - if (!Storage.exists(epub->getCoverBmpPath(false).c_str())) { - PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(false), epub->getTitle(), epub->getAuthor(), 480, - 800); + if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) { + if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(false), epub->getTitle(), epub->getAuthor(), + 480, 800)) { + // Last resort: X-pattern marker + epub->generateInvalidFormatCoverBmp(false); + } } updateProgress(); } - if (!Storage.exists(epub->getCoverBmpPath(true).c_str())) { + if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) { epub->generateCoverBmp(true); - if (!Storage.exists(epub->getCoverBmpPath(true).c_str())) { - PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(true), epub->getTitle(), epub->getAuthor(), 480, - 800); + if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) { + if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(true), epub->getTitle(), epub->getAuthor(), + 480, 800)) { + // Last resort: X-pattern marker + epub->generateInvalidFormatCoverBmp(true); + } } updateProgress(); } for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) { - if (!Storage.exists(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]).c_str())) { + if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) { epub->generateThumbBmp(PRERENDER_THUMB_HEIGHTS[i]); // Fallback: generate placeholder thumbnail - if (!Storage.exists(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]).c_str())) { + if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) { const int thumbHeight = PRERENDER_THUMB_HEIGHTS[i]; const int thumbWidth = static_cast(thumbHeight * 0.6); - PlaceholderCoverGenerator::generate(epub->getThumbBmpPath(thumbHeight), epub->getTitle(), - epub->getAuthor(), thumbWidth, thumbHeight); + if (!PlaceholderCoverGenerator::generate(epub->getThumbBmpPath(thumbHeight), epub->getTitle(), + epub->getAuthor(), thumbWidth, thumbHeight)) { + // Last resort: X-pattern marker + epub->generateInvalidFormatThumbBmp(thumbHeight); + } } updateProgress(); } diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 2e3ad4cd..dc4f4ea8 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -274,11 +274,10 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: for (int i = 0; i < std::min(static_cast(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); i++) { std::string coverPath = recentBooks[i].coverBmpPath; - bool hasCover = true; int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; - if (coverPath.empty()) { - hasCover = false; - } else { + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, + tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight); + if (!coverPath.empty()) { const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); // First time: load cover from SD and render @@ -292,20 +291,12 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / static_cast(LyraMetrics::values.homeCoverHeight); float cropX = 1.0f - (tileRatio / ratio); - renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); - } else { - hasCover = false; } file.close(); } } - - if (!hasCover) { - renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, - tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight); - } } coverBufferStored = storeCoverBuffer();