mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Merge 436dc9e593 into da4d3b5ea5
This commit is contained in:
commit
bdcbc44c22
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user