This commit is contained in:
cottongin
2026-01-24 02:01:53 -05:00
parent 2952d7554c
commit 5c3828efe8
14 changed files with 908 additions and 24 deletions

View File

@@ -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());

View File

@@ -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;