feat: restore book cover/thumbnail prerender on first open
- Add isValidThumbnailBmp(), generateInvalidFormatCoverBmp(), and generateInvalidFormatThumbBmp() methods to Epub class for validating BMP files and generating X-pattern marker images when cover extraction fails (e.g., progressive JPG). - Restore prerender block in EpubReaderActivity::onEnter() that checks for missing cover BMPs (fit + cropped) and thumbnail BMPs at each PRERENDER_THUMB_HEIGHTS size, showing a "Preparing book..." popup with progress. Falls back to PlaceholderCoverGenerator, then to invalid-format marker BMPs as last resort. Made-with: Cursor
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#include "Epub.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HalDisplay.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
@@ -706,6 +707,171 @@ bool Epub::generateThumbBmp(int height) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
LOG_DBG("EBP", "Thumbnail BMP is empty (no cover marker) at path: %s", bmpPath.c_str());
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
}
|
||||
file.close();
|
||||
return header[0] == 'B' && header[1] == 'M';
|
||||
}
|
||||
|
||||
bool Epub::generateInvalidFormatThumbBmp(int height) const {
|
||||
const int width = height * 0.6;
|
||||
const int rowBytes = ((width + 31) / 32) * 4;
|
||||
const int imageSize = rowBytes * height;
|
||||
const int fileSize = 14 + 40 + 8 + imageSize;
|
||||
const int dataOffset = 14 + 40 + 8;
|
||||
|
||||
FsFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
thumbBmp.write('B');
|
||||
thumbBmp.write('M');
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
|
||||
uint32_t reserved = 0;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
|
||||
|
||||
uint32_t dibHeaderSize = 40;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t bmpWidth = width;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
|
||||
int32_t bmpHeight = -height;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
|
||||
uint16_t planes = 1;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
uint16_t bitsPerPixel = 1;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
|
||||
uint32_t compression = 0;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
|
||||
int32_t ppmX = 2835;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
|
||||
int32_t ppmY = 2835;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
|
||||
uint32_t colorsUsed = 2;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
|
||||
uint32_t colorsImportant = 2;
|
||||
thumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
|
||||
|
||||
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
thumbBmp.write(black, 4);
|
||||
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00};
|
||||
thumbBmp.write(white, 4);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
std::vector<uint8_t> rowData(rowBytes, 0xFF);
|
||||
const int scaledY = (y * width) / height;
|
||||
const int thickness = 2;
|
||||
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<uint8_t>(~(1 << bitIndex));
|
||||
}
|
||||
}
|
||||
thumbBmp.write(rowData.data(), rowBytes);
|
||||
}
|
||||
|
||||
thumbBmp.close();
|
||||
LOG_DBG("EBP", "Generated invalid format thumbnail BMP");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::generateInvalidFormatCoverBmp(bool cropped) const {
|
||||
const int hwW = static_cast<int>(HalDisplay::DISPLAY_WIDTH);
|
||||
const int hwH = static_cast<int>(HalDisplay::DISPLAY_HEIGHT);
|
||||
const int width = std::min(hwW, hwH);
|
||||
const int height = std::max(hwW, hwH);
|
||||
const int rowBytes = ((width + 31) / 32) * 4;
|
||||
const int imageSize = rowBytes * height;
|
||||
const int fileSize = 14 + 40 + 8 + imageSize;
|
||||
const int dataOffset = 14 + 40 + 8;
|
||||
|
||||
FsFile coverBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
coverBmp.write('B');
|
||||
coverBmp.write('M');
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
|
||||
uint32_t reserved = 0;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
|
||||
|
||||
uint32_t dibHeaderSize = 40;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t bmpWidth = width;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpWidth), 4);
|
||||
int32_t bmpHeight = -height;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeight), 4);
|
||||
uint16_t planes = 1;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
uint16_t bitsPerPixel = 1;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
|
||||
uint32_t compression = 0;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
|
||||
int32_t ppmX = 2835;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
|
||||
int32_t ppmY = 2835;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
|
||||
uint32_t colorsUsed = 2;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
|
||||
uint32_t colorsImportant = 2;
|
||||
coverBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
|
||||
|
||||
uint8_t black[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
coverBmp.write(black, 4);
|
||||
uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00};
|
||||
coverBmp.write(white, 4);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
std::vector<uint8_t> rowData(rowBytes, 0xFF);
|
||||
const int scaledY = (y * width) / height;
|
||||
const int thickness = 6;
|
||||
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<uint8_t>(~(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");
|
||||
|
||||
@@ -56,6 +56,9 @@ class Epub {
|
||||
std::string getThumbBmpPath() const;
|
||||
std::string getThumbBmpPath(int height) const;
|
||||
bool generateThumbBmp(int height) const;
|
||||
bool generateInvalidFormatCoverBmp(bool cropped = false) const;
|
||||
bool generateInvalidFormatThumbBmp(int height) const;
|
||||
static bool isValidThumbnailBmp(const std::string& bmpPath);
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <Epub/Page.h>
|
||||
#include <Epub/blocks/TextBlock.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <PlaceholderCoverGenerator.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <I18n.h>
|
||||
@@ -115,6 +116,61 @@ void EpubReaderActivity::onEnter() {
|
||||
}
|
||||
}
|
||||
|
||||
// Prerender covers and thumbnails on first open so Home and Sleep screens are instant.
|
||||
{
|
||||
int totalSteps = 0;
|
||||
if (!Storage.exists(epub->getCoverBmpPath(false).c_str())) totalSteps++;
|
||||
if (!Storage.exists(epub->getCoverBmpPath(true).c_str())) totalSteps++;
|
||||
for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) {
|
||||
if (!Storage.exists(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]).c_str())) totalSteps++;
|
||||
}
|
||||
|
||||
if (totalSteps > 0) {
|
||||
Rect popupRect = GUI.drawPopup(renderer, "Preparing book...");
|
||||
int completedSteps = 0;
|
||||
|
||||
auto updateProgress = [&]() {
|
||||
completedSteps++;
|
||||
GUI.fillPopupProgress(renderer, popupRect, completedSteps * 100 / totalSteps);
|
||||
};
|
||||
|
||||
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) {
|
||||
epub->generateCoverBmp(false);
|
||||
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(false))) {
|
||||
if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(false), epub->getTitle(), epub->getAuthor(),
|
||||
480, 800)) {
|
||||
epub->generateInvalidFormatCoverBmp(false);
|
||||
}
|
||||
}
|
||||
updateProgress();
|
||||
}
|
||||
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) {
|
||||
epub->generateCoverBmp(true);
|
||||
if (!Epub::isValidThumbnailBmp(epub->getCoverBmpPath(true))) {
|
||||
if (!PlaceholderCoverGenerator::generate(epub->getCoverBmpPath(true), epub->getTitle(), epub->getAuthor(),
|
||||
480, 800)) {
|
||||
epub->generateInvalidFormatCoverBmp(true);
|
||||
}
|
||||
}
|
||||
updateProgress();
|
||||
}
|
||||
for (int i = 0; i < PRERENDER_THUMB_HEIGHTS_COUNT; i++) {
|
||||
if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) {
|
||||
epub->generateThumbBmp(PRERENDER_THUMB_HEIGHTS[i]);
|
||||
if (!Epub::isValidThumbnailBmp(epub->getThumbBmpPath(PRERENDER_THUMB_HEIGHTS[i]))) {
|
||||
const int thumbHeight = PRERENDER_THUMB_HEIGHTS[i];
|
||||
const int thumbWidth = static_cast<int>(thumbHeight * 0.6);
|
||||
if (!PlaceholderCoverGenerator::generate(epub->getThumbBmpPath(thumbHeight), epub->getTitle(),
|
||||
epub->getAuthor(), thumbWidth, thumbHeight)) {
|
||||
epub->generateInvalidFormatThumbBmp(thumbHeight);
|
||||
}
|
||||
}
|
||||
updateProgress();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save current epub as last opened epub and add to recent books
|
||||
APP_STATE.openEpubPath = epub->getPath();
|
||||
APP_STATE.saveToFile();
|
||||
|
||||
Reference in New Issue
Block a user