diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index cb0b1801..34e78293 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -1,6 +1,7 @@ #include "Epub.h" #include +#include #include #include #include @@ -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(&fileSize), 4); + uint32_t reserved = 0; + thumbBmp.write(reinterpret_cast(&reserved), 4); + thumbBmp.write(reinterpret_cast(&dataOffset), 4); + + uint32_t dibHeaderSize = 40; + thumbBmp.write(reinterpret_cast(&dibHeaderSize), 4); + int32_t bmpWidth = width; + thumbBmp.write(reinterpret_cast(&bmpWidth), 4); + int32_t bmpHeight = -height; + thumbBmp.write(reinterpret_cast(&bmpHeight), 4); + uint16_t planes = 1; + thumbBmp.write(reinterpret_cast(&planes), 2); + uint16_t bitsPerPixel = 1; + thumbBmp.write(reinterpret_cast(&bitsPerPixel), 2); + uint32_t compression = 0; + thumbBmp.write(reinterpret_cast(&compression), 4); + thumbBmp.write(reinterpret_cast(&imageSize), 4); + int32_t ppmX = 2835; + thumbBmp.write(reinterpret_cast(&ppmX), 4); + int32_t ppmY = 2835; + thumbBmp.write(reinterpret_cast(&ppmY), 4); + uint32_t colorsUsed = 2; + thumbBmp.write(reinterpret_cast(&colorsUsed), 4); + uint32_t colorsImportant = 2; + thumbBmp.write(reinterpret_cast(&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 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(~(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(HalDisplay::DISPLAY_WIDTH); + const int hwH = static_cast(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(&fileSize), 4); + uint32_t reserved = 0; + coverBmp.write(reinterpret_cast(&reserved), 4); + coverBmp.write(reinterpret_cast(&dataOffset), 4); + + uint32_t dibHeaderSize = 40; + coverBmp.write(reinterpret_cast(&dibHeaderSize), 4); + int32_t bmpWidth = width; + coverBmp.write(reinterpret_cast(&bmpWidth), 4); + int32_t bmpHeight = -height; + coverBmp.write(reinterpret_cast(&bmpHeight), 4); + uint16_t planes = 1; + coverBmp.write(reinterpret_cast(&planes), 2); + uint16_t bitsPerPixel = 1; + coverBmp.write(reinterpret_cast(&bitsPerPixel), 2); + uint32_t compression = 0; + coverBmp.write(reinterpret_cast(&compression), 4); + coverBmp.write(reinterpret_cast(&imageSize), 4); + int32_t ppmX = 2835; + coverBmp.write(reinterpret_cast(&ppmX), 4); + int32_t ppmY = 2835; + coverBmp.write(reinterpret_cast(&ppmY), 4); + uint32_t colorsUsed = 2; + coverBmp.write(reinterpret_cast(&colorsUsed), 4); + uint32_t colorsImportant = 2; + coverBmp.write(reinterpret_cast(&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 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(~(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"); diff --git a/lib/Epub/Epub.h b/lib/Epub/Epub.h index 9ffa8d37..15765e29 100644 --- a/lib/Epub/Epub.h +++ b/lib/Epub/Epub.h @@ -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; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 31a54e5d..9177cacd 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -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(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();