nice
This commit is contained in:
@@ -573,6 +573,228 @@ bool Epub::generateThumbBmp() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Epub::getMicroThumbBmpPath() const { return cachePath + "/micro_thumb.bmp"; }
|
||||
|
||||
bool Epub::generateMicroThumbBmp() const {
|
||||
// Already generated, return true
|
||||
if (SdMan.exists(getMicroThumbBmpPath().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate micro thumb 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 micro thumbnail\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Generating micro thumb BMP from JPG cover image\n", millis());
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
// Check if temp JPEG already exists (from generateAllCovers), otherwise extract it
|
||||
bool needsCleanup = false;
|
||||
if (!SdMan.exists(coverJpgTempPath.c_str())) {
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
needsCleanup = true;
|
||||
}
|
||||
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FsFile microThumbBmp;
|
||||
if (!SdMan.openFileForWrite("EBP", getMicroThumbBmpPath(), microThumbBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
// Use very small target size for Recent Books list (45x60 pixels)
|
||||
// Generate 1-bit BMP for fast rendering
|
||||
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
||||
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
|
||||
coverJpg, microThumbBmp, MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT);
|
||||
coverJpg.close();
|
||||
microThumbBmp.close();
|
||||
|
||||
if (needsCleanup) {
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Serial.printf("[%lu] [EBP] Failed to generate micro thumb BMP from JPG cover image\n", millis());
|
||||
SdMan.remove(getMicroThumbBmpPath().c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated micro thumb 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 micro thumbnail\n", millis());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Epub::generateAllCovers(const std::function<void(int)>& progressCallback) const {
|
||||
// Check if all covers already exist - quick exit if nothing to do
|
||||
const bool hasThumb = SdMan.exists(getThumbBmpPath().c_str());
|
||||
const bool hasMicroThumb = SdMan.exists(getMicroThumbBmpPath().c_str());
|
||||
const bool hasCoverFit = SdMan.exists(getCoverBmpPath(false).c_str());
|
||||
const bool hasCoverCrop = SdMan.exists(getCoverBmpPath(true).c_str());
|
||||
|
||||
if (hasThumb && hasMicroThumb && hasCoverFit && hasCoverCrop) {
|
||||
Serial.printf("[%lu] [EBP] All covers already cached\n", millis());
|
||||
if (progressCallback) progressCallback(100);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot generate covers, cache not loaded\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only process JPG/JPEG covers
|
||||
if (coverImageHref.substr(coverImageHref.length() - 4) != ".jpg" &&
|
||||
coverImageHref.substr(coverImageHref.length() - 5) != ".jpeg") {
|
||||
Serial.printf("[%lu] [EBP] Cover image is not a JPG, skipping all cover generation\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Generating all covers (thumb:%d, micro:%d, fit:%d, crop:%d)\n", millis(), !hasThumb,
|
||||
!hasMicroThumb, !hasCoverFit, !hasCoverCrop);
|
||||
|
||||
// Extract JPEG once to temp file
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
{
|
||||
FsFile coverJpg;
|
||||
if (!SdMan.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
Serial.printf("[%lu] [EBP] Failed to create temp cover file\n", millis());
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
coverJpg.close();
|
||||
}
|
||||
|
||||
// Get JPEG dimensions once for FIT/CROP calculations
|
||||
int jpegWidth = 0, jpegHeight = 0;
|
||||
{
|
||||
FsFile coverJpg;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
JpegToBmpConverter::getJpegDimensions(coverJpg, jpegWidth, jpegHeight);
|
||||
coverJpg.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Progress tracking: 4 covers = 25% each
|
||||
// Helper to create sub-progress callback that maps 0-100% to a portion of overall progress
|
||||
auto makeSubProgress = [&progressCallback](int startPercent, int endPercent) {
|
||||
if (!progressCallback) return std::function<void(int)>(nullptr);
|
||||
return std::function<void(int)>([&progressCallback, startPercent, endPercent](int subPercent) {
|
||||
const int overallProgress = startPercent + (subPercent * (endPercent - startPercent)) / 100;
|
||||
progressCallback(overallProgress);
|
||||
});
|
||||
};
|
||||
|
||||
// Generate thumb (240x400, 1-bit) if missing - progress 0-25%
|
||||
if (!hasThumb) {
|
||||
FsFile coverJpg, thumbBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getThumbBmpPath(), thumbBmp)) {
|
||||
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, makeSubProgress(0, 25));
|
||||
coverJpg.close();
|
||||
thumbBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getThumbBmpPath().c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated thumb: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(25);
|
||||
|
||||
// Generate micro thumb (45x60, 1-bit) if missing - progress 25-50%
|
||||
if (!hasMicroThumb) {
|
||||
FsFile coverJpg, microThumbBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getMicroThumbBmpPath(), microThumbBmp)) {
|
||||
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
||||
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(
|
||||
coverJpg, microThumbBmp, MICRO_THUMB_TARGET_WIDTH, MICRO_THUMB_TARGET_HEIGHT, makeSubProgress(25, 50));
|
||||
coverJpg.close();
|
||||
microThumbBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getMicroThumbBmpPath().c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated micro thumb: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(50);
|
||||
|
||||
// Generate cover_fit (480xProportional, 2-bit) if missing - progress 50-75%
|
||||
if (!hasCoverFit && jpegWidth > 0 && jpegHeight > 0) {
|
||||
FsFile coverJpg, coverBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getCoverBmpPath(false), coverBmp)) {
|
||||
const int targetWidth = 480;
|
||||
const int targetHeight = (480 * jpegHeight) / jpegWidth;
|
||||
const bool success =
|
||||
JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth, targetHeight, makeSubProgress(50, 75));
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getCoverBmpPath(false).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated cover_fit: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(75);
|
||||
|
||||
// Generate cover_crop (Proportionalx800, 2-bit) if missing - progress 75-100%
|
||||
if (!hasCoverCrop && jpegWidth > 0 && jpegHeight > 0) {
|
||||
FsFile coverJpg, coverBmp;
|
||||
if (SdMan.openFileForRead("EBP", coverJpgTempPath, coverJpg) &&
|
||||
SdMan.openFileForWrite("EBP", getCoverBmpPath(true), coverBmp)) {
|
||||
const int targetHeight = 800;
|
||||
const int targetWidth = (800 * jpegWidth) / jpegHeight;
|
||||
const bool success =
|
||||
JpegToBmpConverter::jpegFileToBmpStreamWithSize(coverJpg, coverBmp, targetWidth, targetHeight, makeSubProgress(75, 100));
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
if (!success) {
|
||||
SdMan.remove(getCoverBmpPath(true).c_str());
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Generated cover_crop: %s\n", millis(), success ? "yes" : "no");
|
||||
}
|
||||
}
|
||||
if (progressCallback) progressCallback(100);
|
||||
|
||||
// Clean up temp JPEG
|
||||
SdMan.remove(coverJpgTempPath.c_str());
|
||||
Serial.printf("[%lu] [EBP] All cover generation complete\n", millis());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||
if (itemHref.empty()) {
|
||||
Serial.printf("[%lu] [EBP] Failed to read item, empty href\n", millis());
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -53,6 +54,9 @@ class Epub {
|
||||
bool generateCoverBmp(bool cropped = false) const;
|
||||
std::string getThumbBmpPath() const;
|
||||
bool generateThumbBmp() const;
|
||||
std::string getMicroThumbBmpPath() const;
|
||||
bool generateMicroThumbBmp() const;
|
||||
bool generateAllCovers(const std::function<void(int)>& progressCallback = nullptr) 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;
|
||||
|
||||
Reference in New Issue
Block a user