This commit is contained in:
pablohc 2026-01-27 18:14:44 +00:00 committed by GitHub
commit bdcbc44c22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 129 additions and 98 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<float>(THUMB_TARGET_WIDTH) / pageInfo.width;
float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height;
float scale = (scaleX < scaleY) ? scaleX : scaleY;
// Calculate proportional width for 400px height
const uint16_t targetWidth = static_cast<uint16_t>((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<uint16_t>(pageInfo.width * scale);
uint16_t thumbHeight = static_cast<uint16_t>(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<const uint8_t*>(&dibHeaderSize), 4);
int32_t widthVal = thumbWidth;
int32_t widthVal = targetWidth;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&widthVal), 4);
int32_t heightVal = -static_cast<int32_t>(thumbHeight); // Negative for top-down
int32_t heightVal = -static_cast<int32_t>(HOME_TARGET_HEIGHT); // Negative for top-down
thumbBmp.write(reinterpret_cast<const uint8_t*>(&heightVal), 4);
uint16_t planes = 1;
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
@ -451,8 +426,8 @@ bool Xtc::generateThumbBmp() const {
return false;
}
// Fixed-point scale factor (16.16)
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f / scale);
// Fixed-point scale factor (16.16) - scale to fit 400px height
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f * static_cast<float>(pageInfo.height) / HOME_TARGET_HEIGHT);
// Pre-calculate plane info for 2-bit mode
const size_t planeSize = (bitDepth == 2) ? ((static_cast<size_t>(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<uint32_t>(dstX) * scaleInv_fp) >> 16;
uint32_t srcXEnd = (static_cast<uint32_t>(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;
}

View File

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

View File

@ -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<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float boxRatio = static_cast<float>(bookWidth) / static_cast<float>(bookHeight);
if (imgRatio > boxRatio) {
coverX = bookX;
coverY = bookY + (bookHeight - static_cast<int>(bookWidth / imgRatio)) / 2;
} else {
coverX = bookX + (bookWidth - static_cast<int>(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);