diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 7559e3b3..3214d2e5 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -428,28 +428,28 @@ bool Epub::generateCoverBmp(bool cropped) const { return false; } -std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } +std::string Epub::getCoverHomeBmpPath() const { return cachePath + "/cover_home.bmp"; } -bool Epub::generateThumbBmp() const { +bool Epub::generateCoverHomeBmp() const { // Already generated, return true - if (SdMan.exists(getThumbBmpPath().c_str())) { + if (SdMan.exists(getCoverHomeBmpPath().c_str())) { return true; } if (!bookMetadataCache || !bookMetadataCache->isLoaded()) { - Serial.printf("[%lu] [EBP] Cannot generate thumb BMP, cache not loaded\n", millis()); + Serial.printf("[%lu] [EBP] Cannot generate home BMP, cache not loaded\n", millis()); return false; } const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref; if (coverImageHref.empty()) { - Serial.printf("[%lu] [EBP] No known cover image for thumbnail\n", millis()); + Serial.printf("[%lu] [EBP] No known cover image for home screen\n", millis()); return false; } if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") { - Serial.printf("[%lu] [EBP] Generating thumb BMP from JPG cover image\n", millis()); + Serial.printf("[%lu] [EBP] Generating home BMP from JPG cover image\n", millis()); const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; FsFile coverJpg; @@ -463,30 +463,50 @@ bool Epub::generateThumbBmp() const { return false; } - FsFile thumbBmp; - if (!SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) { + FsFile homeBmp; + if (!SdMan.openFileForWrite("EBP", getCoverHomeBmpPath(), homeBmp)) { coverJpg.close(); return false; } - // Use smaller target size for Continue Reading card (half of screen: 240x400) + + // For home screen, use 400px height with proportional width for optimal performance // 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); + constexpr int HOME_TARGET_HEIGHT = 400; + + FsFile tempJpg; + if (!SdMan.openFileForRead("EBP", coverJpgTempPath, tempJpg)) { + coverJpg.close(); + return false; + } + + // First get JPEG dimensions to calculate proper width + int jpegWidth, jpegHeight; + if (!JpegToBmpConverter::getJpegDimensions(tempJpg, &jpegWidth, &jpegHeight)) { + Serial.printf("[%lu] [EBP] Failed to get JPEG dimensions for home cover\n", millis()); + coverJpg.close(); + tempJpg.close(); + return false; + } + tempJpg.close(); + + // Calculate proportional width for 400px height + const int targetWidth = (400 * jpegWidth) / jpegHeight; + + const bool success = + JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, homeBmp, targetWidth, HOME_TARGET_HEIGHT); coverJpg.close(); - thumbBmp.close(); + homeBmp.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()); + Serial.printf("[%lu] [EBP] Failed to generate home BMP from JPG cover image\n", millis()); + SdMan.remove(getCoverHomeBmpPath().c_str()); } - Serial.printf("[%lu] [EBP] Generated thumb BMP from JPG cover image, success: %s\n", millis(), + Serial.printf("[%lu] [EBP] Generated home BMP from JPG cover image, success: %s\n", millis(), success ? "yes" : "no"); return success; } else { - Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping thumbnail\n", millis()); + Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping home screen\n", millis()); } return false; diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 7a21efd5..4c8f2b69 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -47,8 +47,9 @@ 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; + // Home screen support (optimized 400px height covers for Continue Reading card) + std::string getCoverHomeBmpPath() const; + bool generateCoverHomeBmp() 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/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 84ac1d58..6e4fee83 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -569,3 +569,32 @@ bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print int targetMaxHeight) { return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true); } + +// Get JPEG dimensions without full conversion +bool JpegToBmpConverter::getJpegDimensions(FsFile& jpegFile, int* width, int* height) { + // Reset file position to beginning + if (!jpegFile.seek(0)) { + Serial.printf("[%lu] [JPG] Failed to seek to beginning of JPEG file\n", millis()); + return false; + } + + // Initialize JPEG decoder + pjpeg_image_info_t imageInfo = {}; + JpegReadContext context{jpegFile, {}, 0, 0}; + + const int decodeStatus = pjpeg_decode_init(&imageInfo, jpegReadCallback, &context, false); + if (decodeStatus != 0) { + Serial.printf("[%lu] [JPG] pjpeg_decode_init failed with status %d\n", millis(), decodeStatus); + return false; + } + + // Get dimensions from image info + *width = imageInfo.m_width; + *height = imageInfo.m_height; + + Serial.printf("[%lu] [JPG] Read JPEG dimensions: %dx%d\n", millis(), *width, *height); + + // Reset file position after reading + jpegFile.seek(0); + return true; +} diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.h b/lib/JpegToBmpConverter/JpegToBmpConverter.h index 9b92bb6d..90505f99 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.h +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.h @@ -16,4 +16,6 @@ class JpegToBmpConverter { 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); + // Extract JPEG dimensions without loading full image + static bool getJpegDimensions(FsFile& jpegFile, int* width, int* height); }; diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 7850d934..0d18672d 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -301,11 +301,11 @@ bool Xtc::generateCoverBmp() const { return true; } -std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; } +std::string Xtc::getCoverHomeBmpPath() const { return cachePath + "/cover_home.bmp"; } -bool Xtc::generateThumbBmp() const { +bool Xtc::generateCoverHomeBmp() const { // Already generated - if (SdMan.exists(getThumbBmpPath().c_str())) { + if (SdMan.exists(getCoverHomeBmpPath().c_str())) { return true; } @@ -332,43 +332,18 @@ 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; + // For home screen, use 400px height with proportional width for optimal performance + // Generate 1-bit BMP for fast home screen rendering (no gray passes needed) + constexpr int HOME_TARGET_HEIGHT = 400; - // Calculate scale factor - float scaleX = static_cast(THUMB_TARGET_WIDTH) / pageInfo.width; - float scaleY = static_cast(THUMB_TARGET_HEIGHT) / pageInfo.height; - float scale = (scaleX < scaleY) ? scaleX : scaleY; + // Calculate proportional width for 400px height + const uint16_t targetWidth = static_cast((HOME_TARGET_HEIGHT * pageInfo.width) / pageInfo.height); - // Only scale down, never up - 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; - } - - uint16_t thumbWidth = static_cast(pageInfo.width * scale); - uint16_t thumbHeight = static_cast(pageInfo.height * scale); + Serial.printf("[%lu] [XTC] Generating home BMP: %dx%d -> %dx%d\n", millis(), pageInfo.width, pageInfo.height, + targetWidth, HOME_TARGET_HEIGHT); Serial.printf("[%lu] [XTC] Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width, - pageInfo.height, thumbWidth, thumbHeight, scale); + pageInfo.height, targetWidth, HOME_TARGET_HEIGHT); // Allocate buffer for page data size_t bitmapSize; @@ -393,15 +368,15 @@ 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", getCoverHomeBmpPath(), thumbBmp)) { Serial.printf("[%lu] [XTC] Failed to create thumb BMP file\n", millis()); free(pageBuffer); return false; } // Write 1-bit BMP header for fast home screen rendering - const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes - const uint32_t imageSize = rowSize * thumbHeight; + const uint32_t rowSize = (targetWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes + const uint32_t imageSize = rowSize * HOME_TARGET_HEIGHT; const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette // File header @@ -416,9 +391,9 @@ bool Xtc::generateThumbBmp() const { // DIB header uint32_t dibHeaderSize = 40; thumbBmp.write(reinterpret_cast(&dibHeaderSize), 4); - int32_t widthVal = thumbWidth; + int32_t widthVal = targetWidth; thumbBmp.write(reinterpret_cast(&widthVal), 4); - int32_t heightVal = -static_cast(thumbHeight); // Negative for top-down + int32_t heightVal = -static_cast(HOME_TARGET_HEIGHT); // Negative for top-down thumbBmp.write(reinterpret_cast(&heightVal), 4); uint16_t planes = 1; thumbBmp.write(reinterpret_cast(&planes), 2); @@ -451,8 +426,8 @@ bool Xtc::generateThumbBmp() const { return false; } - // Fixed-point scale factor (16.16) - uint32_t scaleInv_fp = static_cast(65536.0f / scale); + // Fixed-point scale factor (16.16) - scale to fit 400px height + uint32_t scaleInv_fp = static_cast(65536.0f * static_cast(pageInfo.height) / HOME_TARGET_HEIGHT); // Pre-calculate plane info for 2-bit mode const size_t planeSize = (bitDepth == 2) ? ((static_cast(pageInfo.width) * pageInfo.height + 7) / 8) : 0; @@ -461,7 +436,7 @@ bool Xtc::generateThumbBmp() const { const size_t colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0; const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0; - for (uint16_t dstY = 0; dstY < thumbHeight; dstY++) { + for (uint16_t dstY = 0; dstY < HOME_TARGET_HEIGHT; dstY++) { memset(rowBuffer, 0xFF, rowSize); // Start with all white (bit 1) // Calculate source Y range with bounds checking @@ -472,7 +447,7 @@ bool Xtc::generateThumbBmp() const { if (srcYEnd <= srcYStart) srcYEnd = srcYStart + 1; if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height; - for (uint16_t dstX = 0; dstX < thumbWidth; dstX++) { + for (uint16_t dstX = 0; dstX < targetWidth; dstX++) { // Calculate source X range with bounds checking uint32_t srcXStart = (static_cast(dstX) * scaleInv_fp) >> 16; uint32_t srcXEnd = (static_cast(dstX + 1) * scaleInv_fp) >> 16; @@ -557,8 +532,8 @@ bool Xtc::generateThumbBmp() const { thumbBmp.close(); free(pageBuffer); - Serial.printf("[%lu] [XTC] Generated thumb BMP (%dx%d): %s\n", millis(), thumbWidth, thumbHeight, - getThumbBmpPath().c_str()); + Serial.printf("[%lu] [XTC] Generated home BMP (%dx%d): %s\n", millis(), targetWidth, HOME_TARGET_HEIGHT, + getCoverHomeBmpPath().c_str()); return true; } diff --git a/lib/Xtc/Xtc.h b/lib/Xtc/Xtc.h index c8d9a040..c9545060 100644 --- a/lib/Xtc/Xtc.h +++ b/lib/Xtc/Xtc.h @@ -63,9 +63,9 @@ class Xtc { // Cover image support (for sleep screen) std::string getCoverBmpPath() const; bool generateCoverBmp() const; - // Thumbnail support (for Continue Reading card) - std::string getThumbBmpPath() const; - bool generateThumbBmp() const; + // Home screen support (optimized 400px height covers for Continue Reading card) + std::string getCoverHomeBmpPath() const; + bool generateCoverHomeBmp() const; // Page access uint32_t getPageCount() const; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 58b29505..1b1ea652 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -59,8 +59,8 @@ void HomeActivity::onEnter() { lastBookAuthor = std::string(epub.getAuthor()); } // Try to generate thumbnail image for Continue Reading card - if (epub.generateThumbBmp()) { - coverBmpPath = epub.getThumbBmpPath(); + if (epub.generateCoverHomeBmp()) { + coverBmpPath = epub.getCoverHomeBmpPath(); hasCoverImage = true; } } else if (StringUtils::checkFileExtension(lastBookTitle, ".xtch") || @@ -75,8 +75,8 @@ void HomeActivity::onEnter() { lastBookAuthor = std::string(xtc.getAuthor()); } // Try to generate thumbnail image for Continue Reading card - if (xtc.generateThumbBmp()) { - coverBmpPath = xtc.getThumbBmpPath(); + if (xtc.generateCoverHomeBmp()) { + coverBmpPath = xtc.getCoverHomeBmpPath(); hasCoverImage = true; } } @@ -223,10 +223,32 @@ void HomeActivity::render() { constexpr int bottomMargin = 60; // --- Top "book" card for the current title (selectorIndex == 0) --- - const int bookWidth = pageWidth / 2; - const int bookHeight = pageHeight / 2; - const int bookX = (pageWidth - bookWidth) / 2; + // Load cover image to get its dimensions + int coverWidth = 0; + int coverHeight = 0; + if (hasContinueReading && hasCoverImage && !coverBmpPath.empty()) { + FsFile coverFile; + if (SdMan.openFileForRead("HOME", coverBmpPath, coverFile)) { + Bitmap testBitmap(coverFile); + if (testBitmap.parseHeaders() == BmpReaderError::Ok) { + coverWidth = testBitmap.getWidth(); + coverHeight = testBitmap.getHeight(); + } + coverFile.close(); + } + } + + // Calculate card dimensions based on cover image + // Use 400px height as specified, with proportional width + constexpr int CARD_HEIGHT = 400; + const int cardWidth = (coverWidth > 0 && coverHeight > 0) + ? (CARD_HEIGHT * coverWidth) / coverHeight + : 240; // Fallback to 240px width if no image (maintain aspect ratio) + + const int bookX = (pageWidth - cardWidth) / 2; constexpr int bookY = 30; + const int bookWidth = cardWidth; + const int bookHeight = CARD_HEIGHT; const bool bookSelected = hasContinueReading && selectorIndex == 0; // Bookmark dimensions (used in multiple places) @@ -245,27 +267,9 @@ void HomeActivity::render() { if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { - // Calculate position to center image within the book card - int coverX, coverY; - - if (bitmap.getWidth() > bookWidth || bitmap.getHeight() > bookHeight) { - const float imgRatio = static_cast(bitmap.getWidth()) / static_cast(bitmap.getHeight()); - const float boxRatio = static_cast(bookWidth) / static_cast(bookHeight); - - if (imgRatio > boxRatio) { - coverX = bookX; - coverY = bookY + (bookHeight - static_cast(bookWidth / imgRatio)) / 2; - } else { - coverX = bookX + (bookWidth - static_cast(bookHeight * imgRatio)) / 2; - coverY = bookY; - } - } else { - coverX = bookX + (bookWidth - bitmap.getWidth()) / 2; - coverY = bookY + (bookHeight - bitmap.getHeight()) / 2; - } - - // Draw the cover image centered within the book card - renderer.drawBitmap(bitmap, coverX, coverY, bookWidth, bookHeight); + // Since the book card already has the exact same size as the image, + // we can draw it at the same position with the same dimensions + renderer.drawBitmap(bitmap, bookX, bookY, bookWidth, bookHeight); // Draw border around the card renderer.drawRect(bookX, bookY, bookWidth, bookHeight);