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 "Epub.h"
|
||||||
|
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
|
#include <HalDisplay.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <JpegToBmpConverter.h>
|
#include <JpegToBmpConverter.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
@@ -706,6 +707,171 @@ bool Epub::generateThumbBmp(int height) const {
|
|||||||
return false;
|
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 {
|
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||||
if (itemHref.empty()) {
|
if (itemHref.empty()) {
|
||||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class Epub {
|
|||||||
std::string getThumbBmpPath() const;
|
std::string getThumbBmpPath() const;
|
||||||
std::string getThumbBmpPath(int height) const;
|
std::string getThumbBmpPath(int height) const;
|
||||||
bool generateThumbBmp(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,
|
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <Epub/Page.h>
|
#include <Epub/Page.h>
|
||||||
#include <Epub/blocks/TextBlock.h>
|
#include <Epub/blocks/TextBlock.h>
|
||||||
#include <FsHelpers.h>
|
#include <FsHelpers.h>
|
||||||
|
#include <PlaceholderCoverGenerator.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalStorage.h>
|
#include <HalStorage.h>
|
||||||
#include <I18n.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
|
// Save current epub as last opened epub and add to recent books
|
||||||
APP_STATE.openEpubPath = epub->getPath();
|
APP_STATE.openEpubPath = epub->getPath();
|
||||||
APP_STATE.saveToFile();
|
APP_STATE.saveToFile();
|
||||||
|
|||||||
Reference in New Issue
Block a user