nice
This commit is contained in:
267
lib/Xtc/Xtc.cpp
267
lib/Xtc/Xtc.cpp
@@ -554,6 +554,273 @@ bool Xtc::generateThumbBmp() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Xtc::getMicroThumbBmpPath() const { return cachePath + "/micro_thumb.bmp"; }
|
||||
|
||||
bool Xtc::generateMicroThumbBmp() const {
|
||||
// Already generated
|
||||
if (SdMan.exists(getMicroThumbBmpPath().c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!loaded || !parser) {
|
||||
Serial.printf("[%lu] [XTC] Cannot generate micro thumb BMP, file not loaded\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parser->getPageCount() == 0) {
|
||||
Serial.printf("[%lu] [XTC] No pages in XTC file\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup cache directory
|
||||
setupCacheDir();
|
||||
|
||||
// Get first page info for cover
|
||||
xtc::PageInfo pageInfo;
|
||||
if (!parser->getPageInfo(0, pageInfo)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to get first page info\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get bit depth
|
||||
const uint8_t bitDepth = parser->getBitDepth();
|
||||
|
||||
// Calculate target dimensions for micro thumbnail (45x60 for Recent Books list)
|
||||
constexpr int MICRO_THUMB_TARGET_WIDTH = 45;
|
||||
constexpr int MICRO_THUMB_TARGET_HEIGHT = 60;
|
||||
|
||||
// Calculate scale factor to fit within target dimensions
|
||||
float scaleX = static_cast<float>(MICRO_THUMB_TARGET_WIDTH) / pageInfo.width;
|
||||
float scaleY = static_cast<float>(MICRO_THUMB_TARGET_HEIGHT) / pageInfo.height;
|
||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
|
||||
uint16_t microThumbWidth = static_cast<uint16_t>(pageInfo.width * scale);
|
||||
uint16_t microThumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
|
||||
|
||||
// Ensure minimum size
|
||||
if (microThumbWidth < 1) microThumbWidth = 1;
|
||||
if (microThumbHeight < 1) microThumbHeight = 1;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generating micro thumb BMP: %dx%d -> %dx%d (scale: %.3f)\n", millis(), pageInfo.width,
|
||||
pageInfo.height, microThumbWidth, microThumbHeight, scale);
|
||||
|
||||
// Allocate buffer for page data
|
||||
size_t bitmapSize;
|
||||
if (bitDepth == 2) {
|
||||
bitmapSize = ((static_cast<size_t>(pageInfo.width) * pageInfo.height + 7) / 8) * 2;
|
||||
} else {
|
||||
bitmapSize = ((pageInfo.width + 7) / 8) * pageInfo.height;
|
||||
}
|
||||
uint8_t* pageBuffer = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||
if (!pageBuffer) {
|
||||
Serial.printf("[%lu] [XTC] Failed to allocate page buffer (%lu bytes)\n", millis(), bitmapSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load first page (cover)
|
||||
size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize);
|
||||
if (bytesRead == 0) {
|
||||
Serial.printf("[%lu] [XTC] Failed to load cover page for micro thumb\n", millis());
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create micro thumbnail BMP file - use 1-bit format
|
||||
FsFile microThumbBmp;
|
||||
if (!SdMan.openFileForWrite("XTC", getMicroThumbBmpPath(), microThumbBmp)) {
|
||||
Serial.printf("[%lu] [XTC] Failed to create micro thumb BMP file\n", millis());
|
||||
free(pageBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write 1-bit BMP header
|
||||
const uint32_t rowSize = (microThumbWidth + 31) / 32 * 4; // 1 bit per pixel, aligned to 4 bytes
|
||||
const uint32_t imageSize = rowSize * microThumbHeight;
|
||||
const uint32_t fileSize = 14 + 40 + 8 + imageSize; // 8 bytes for 2-color palette
|
||||
|
||||
// File header
|
||||
microThumbBmp.write('B');
|
||||
microThumbBmp.write('M');
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&fileSize), 4);
|
||||
uint32_t reserved = 0;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&reserved), 4);
|
||||
uint32_t dataOffset = 14 + 40 + 8;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&dataOffset), 4);
|
||||
|
||||
// DIB header
|
||||
uint32_t dibHeaderSize = 40;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&dibHeaderSize), 4);
|
||||
int32_t widthVal = microThumbWidth;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&widthVal), 4);
|
||||
int32_t heightVal = -static_cast<int32_t>(microThumbHeight); // Negative for top-down
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&heightVal), 4);
|
||||
uint16_t planes = 1;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&planes), 2);
|
||||
uint16_t bitsPerPixel = 1;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&bitsPerPixel), 2);
|
||||
uint32_t compression = 0;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&compression), 4);
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&imageSize), 4);
|
||||
int32_t ppmX = 2835;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmX), 4);
|
||||
int32_t ppmY = 2835;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&ppmY), 4);
|
||||
uint32_t colorsUsed = 2;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsUsed), 4);
|
||||
uint32_t colorsImportant = 2;
|
||||
microThumbBmp.write(reinterpret_cast<const uint8_t*>(&colorsImportant), 4);
|
||||
|
||||
// Color palette
|
||||
uint8_t palette[8] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Color 0: Black
|
||||
0xFF, 0xFF, 0xFF, 0x00 // Color 1: White
|
||||
};
|
||||
microThumbBmp.write(palette, 8);
|
||||
|
||||
// Allocate row buffer
|
||||
uint8_t* rowBuffer = static_cast<uint8_t*>(malloc(rowSize));
|
||||
if (!rowBuffer) {
|
||||
free(pageBuffer);
|
||||
microThumbBmp.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fixed-point scale factor (16.16)
|
||||
uint32_t scaleInv_fp = static_cast<uint32_t>(65536.0f / scale);
|
||||
|
||||
// Pre-calculate plane info for 2-bit mode
|
||||
const size_t planeSize = (bitDepth == 2) ? ((static_cast<size_t>(pageInfo.width) * pageInfo.height + 7) / 8) : 0;
|
||||
const uint8_t* plane1 = (bitDepth == 2) ? pageBuffer : nullptr;
|
||||
const uint8_t* plane2 = (bitDepth == 2) ? pageBuffer + planeSize : nullptr;
|
||||
const size_t colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0;
|
||||
const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0;
|
||||
|
||||
for (uint16_t dstY = 0; dstY < microThumbHeight; dstY++) {
|
||||
memset(rowBuffer, 0xFF, rowSize); // Start with all white
|
||||
|
||||
uint32_t srcYStart = (static_cast<uint32_t>(dstY) * scaleInv_fp) >> 16;
|
||||
uint32_t srcYEnd = (static_cast<uint32_t>(dstY + 1) * scaleInv_fp) >> 16;
|
||||
if (srcYStart >= pageInfo.height) srcYStart = pageInfo.height - 1;
|
||||
if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height;
|
||||
if (srcYEnd <= srcYStart) srcYEnd = srcYStart + 1;
|
||||
if (srcYEnd > pageInfo.height) srcYEnd = pageInfo.height;
|
||||
|
||||
for (uint16_t dstX = 0; dstX < microThumbWidth; dstX++) {
|
||||
uint32_t srcXStart = (static_cast<uint32_t>(dstX) * scaleInv_fp) >> 16;
|
||||
uint32_t srcXEnd = (static_cast<uint32_t>(dstX + 1) * scaleInv_fp) >> 16;
|
||||
if (srcXStart >= pageInfo.width) srcXStart = pageInfo.width - 1;
|
||||
if (srcXEnd > pageInfo.width) srcXEnd = pageInfo.width;
|
||||
if (srcXEnd <= srcXStart) srcXEnd = srcXStart + 1;
|
||||
if (srcXEnd > pageInfo.width) srcXEnd = pageInfo.width;
|
||||
|
||||
// Area averaging
|
||||
uint32_t graySum = 0;
|
||||
uint32_t totalCount = 0;
|
||||
|
||||
for (uint32_t srcY = srcYStart; srcY < srcYEnd && srcY < pageInfo.height; srcY++) {
|
||||
for (uint32_t srcX = srcXStart; srcX < srcXEnd && srcX < pageInfo.width; srcX++) {
|
||||
uint8_t grayValue = 255;
|
||||
|
||||
if (bitDepth == 2) {
|
||||
if (srcX < pageInfo.width) {
|
||||
const size_t colIndex = pageInfo.width - 1 - srcX;
|
||||
const size_t byteInCol = srcY / 8;
|
||||
const size_t bitInByte = 7 - (srcY % 8);
|
||||
const size_t byteOffset = colIndex * colBytes + byteInCol;
|
||||
if (byteOffset < planeSize) {
|
||||
const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1;
|
||||
const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1;
|
||||
const uint8_t pixelValue = (bit1 << 1) | bit2;
|
||||
grayValue = (3 - pixelValue) * 85;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const size_t byteIdx = srcY * srcRowBytes + srcX / 8;
|
||||
const size_t bitIdx = 7 - (srcX % 8);
|
||||
if (byteIdx < bitmapSize) {
|
||||
const uint8_t pixelBit = (pageBuffer[byteIdx] >> bitIdx) & 1;
|
||||
grayValue = pixelBit ? 255 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
graySum += grayValue;
|
||||
totalCount++;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t avgGray = (totalCount > 0) ? static_cast<uint8_t>(graySum / totalCount) : 255;
|
||||
|
||||
// Hash-based noise dithering
|
||||
uint32_t hash = static_cast<uint32_t>(dstX) * 374761393u + static_cast<uint32_t>(dstY) * 668265263u;
|
||||
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
||||
const int threshold = static_cast<int>(hash >> 24);
|
||||
const int adjustedThreshold = 128 + ((threshold - 128) / 2);
|
||||
|
||||
uint8_t oneBit = (avgGray >= adjustedThreshold) ? 1 : 0;
|
||||
|
||||
const size_t byteIndex = dstX / 8;
|
||||
const size_t bitOffset = 7 - (dstX % 8);
|
||||
if (byteIndex < rowSize) {
|
||||
if (oneBit) {
|
||||
rowBuffer[byteIndex] |= (1 << bitOffset);
|
||||
} else {
|
||||
rowBuffer[byteIndex] &= ~(1 << bitOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
microThumbBmp.write(rowBuffer, rowSize);
|
||||
}
|
||||
|
||||
free(rowBuffer);
|
||||
microThumbBmp.close();
|
||||
free(pageBuffer);
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generated micro thumb BMP (%dx%d): %s\n", millis(), microThumbWidth, microThumbHeight,
|
||||
getMicroThumbBmpPath().c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Xtc::generateAllCovers(const std::function<void(int)>& progressCallback) const {
|
||||
// Check if all covers already exist
|
||||
const bool hasCover = SdMan.exists(getCoverBmpPath().c_str());
|
||||
const bool hasThumb = SdMan.exists(getThumbBmpPath().c_str());
|
||||
const bool hasMicroThumb = SdMan.exists(getMicroThumbBmpPath().c_str());
|
||||
|
||||
if (hasCover && hasThumb && hasMicroThumb) {
|
||||
Serial.printf("[%lu] [XTC] All covers already cached\n", millis());
|
||||
if (progressCallback) progressCallback(100);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!loaded || !parser) {
|
||||
Serial.printf("[%lu] [XTC] Cannot generate covers, file not loaded\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [XTC] Generating all covers (cover:%d, thumb:%d, micro:%d)\n", millis(), !hasCover, !hasThumb,
|
||||
!hasMicroThumb);
|
||||
|
||||
// Generate each cover type that's missing with progress updates
|
||||
if (!hasCover) {
|
||||
generateCoverBmp();
|
||||
}
|
||||
if (progressCallback) progressCallback(33);
|
||||
|
||||
if (!hasThumb) {
|
||||
generateThumbBmp();
|
||||
}
|
||||
if (progressCallback) progressCallback(66);
|
||||
|
||||
if (!hasMicroThumb) {
|
||||
generateMicroThumbBmp();
|
||||
}
|
||||
if (progressCallback) progressCallback(100);
|
||||
|
||||
Serial.printf("[%lu] [XTC] All cover generation complete\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t Xtc::getPageCount() const {
|
||||
if (!loaded || !parser) {
|
||||
return 0;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -65,6 +66,11 @@ class Xtc {
|
||||
// Thumbnail support (for Continue Reading card)
|
||||
std::string getThumbBmpPath() const;
|
||||
bool generateThumbBmp() const;
|
||||
// Micro thumbnail support (for Recent Books list)
|
||||
std::string getMicroThumbBmpPath() const;
|
||||
bool generateMicroThumbBmp() const;
|
||||
// Generate all covers at once (for pre-generation on book open)
|
||||
bool generateAllCovers(const std::function<void(int)>& progressCallback = nullptr) const;
|
||||
|
||||
// Page access
|
||||
uint32_t getPageCount() const;
|
||||
|
||||
Reference in New Issue
Block a user