Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
19 kB · 546 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547/** * Xtc.cpp * * Main XTC ebook class implementation * XTC ebook support for CrossPoint Reader */
#include "Xtc.h"
#include <Bitmap.h>#include <HalStorage.h>#include <Logging.h>#include <freertos/FreeRTOS.h>#include <freertos/task.h>
namespace {void yieldDuringThumbnail(uint8_t& rowsSinceYield) { if (++rowsSinceYield < 8) return; rowsSinceYield = 0; vTaskDelay(1);}} // namespace
#include "../Memory/Memory.h"
bool Xtc::load() { LOG_DBG("XTC", "Loading XTC: %s", filepath.c_str());
// Initialize parser parser = makeUniqueNoThrow<xtc::XtcParser>(); if (!parser) { LOG_ERR("XTC", "OOM: XtcParser"); return false; }
// Open XTC file xtc::XtcError err = parser->open(filepath.c_str()); if (err != xtc::XtcError::OK) { LOG_ERR("XTC", "Failed to load: %s", xtc::errorToString(err)); parser.reset(); return false; }
loaded = true; LOG_DBG("XTC", "Loaded XTC: %s (%lu pages)", filepath.c_str(), parser->getPageCount()); return true;}
bool Xtc::clearCache() const { if (!Storage.exists(cachePath.c_str())) { LOG_DBG("XTC", "Cache does not exist, no action needed"); return true; }
if (!Storage.removeDir(cachePath.c_str())) { LOG_ERR("XTC", "Failed to clear cache"); return false; }
LOG_DBG("XTC", "Cache cleared successfully"); return true;}
void Xtc::setupCacheDir() const { if (Storage.exists(cachePath.c_str())) { return; }
// Create directories recursively for (size_t i = 1; i < cachePath.length(); i++) { if (cachePath[i] == '/') { Storage.mkdir(cachePath.substr(0, i).c_str()); } } Storage.mkdir(cachePath.c_str());}
std::string Xtc::getTitle() const { if (!loaded || !parser) { return ""; }
// Try to get title from XTC metadata first std::string title = parser->getTitle(); if (!title.empty()) { return title; }
// Fallback: extract filename from path as title size_t lastSlash = filepath.find_last_of('/'); size_t lastDot = filepath.find_last_of('.');
if (lastSlash == std::string::npos) { lastSlash = 0; } else { lastSlash++; }
if (lastDot == std::string::npos || lastDot <= lastSlash) { return filepath.substr(lastSlash); }
return filepath.substr(lastSlash, lastDot - lastSlash);}
std::string Xtc::getAuthor() const { if (!loaded || !parser) { return ""; }
// Try to get author from XTC metadata return parser->getAuthor();}
bool Xtc::hasChapters() const { if (!loaded || !parser) { return false; } return parser->hasChapters();}
const std::vector<xtc::ChapterInfo>& Xtc::getChapters() { static const std::vector<xtc::ChapterInfo> kEmpty; if (!loaded || !parser) { return kEmpty; } return parser->getChapters();}
std::string Xtc::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
bool Xtc::generateCoverBmp() const { // Already generated if (Storage.exists(getCoverBmpPath().c_str())) { return true; }
if (!loaded || !parser) { LOG_ERR("XTC", "Cannot generate cover BMP, file not loaded"); return false; }
if (parser->getPageCount() == 0) { LOG_ERR("XTC", "No pages in XTC file"); return false; }
// Setup cache directory setupCacheDir();
// Get first page info for cover xtc::PageInfo pageInfo; if (!parser->getPageInfo(0, pageInfo)) { LOG_DBG("XTC", "Failed to get first page info"); return false; }
// Get bit depth const uint8_t bitDepth = parser->getBitDepth();
// Allocate buffer for page data // XTG (1-bit): Row-major, ((width+7)/8) * height bytes // XTH (2-bit): Two column-major bit planes; each column occupies ceil(height/8) bytes size_t bitmapSize; if (bitDepth == 2) { bitmapSize = static_cast<size_t>(pageInfo.width) * ((static_cast<size_t>(pageInfo.height) + 7) / 8) * 2; } else { bitmapSize = ((pageInfo.width + 7) / 8) * pageInfo.height; } const uint32_t rowSize2 = ((static_cast<uint32_t>(pageInfo.width) * 2 + 31) / 32) * 4; const size_t rowBufferSize = (bitDepth == 2) ? rowSize2 : 0; auto coverScratch = makeUniqueNoThrow<uint8_t[]>(bitmapSize + rowBufferSize); if (!coverScratch) { LOG_ERR("XTC", "Failed to allocate cover buffers (%lu bytes)", static_cast<unsigned long>(bitmapSize + rowBufferSize)); return false; } uint8_t* pageBuffer = coverScratch.get(); uint8_t* rowBuffer = coverScratch.get() + bitmapSize;
// Load first page (cover) size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize); if (bytesRead == 0) { LOG_ERR("XTC", "Failed to load cover page"); return false; }
// Create BMP file HalFile coverBmp; if (!Storage.openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) { LOG_DBG("XTC", "Failed to create cover BMP file"); return false; }
if (bitDepth == 2) { // XTH 2-bit mode: preserve all 4 gray levels in a 2-bit BMP so the sleep // screen's grayscale pass can render them (a 1-bit cover would silently // disable it). Source is two bit planes, column-major order: // - Columns scanned right to left (x = width-1 down to 0) // - 8 vertical pixels per byte (MSB = topmost pixel in group) // - First plane: Bit1, Second plane: Bit2 // - Pixel value = (bit1 << 1) | bit2: 0=white, 1=dark gray, // 2=light gray, 3=black // 70-byte 2-bit BMP header (14 file + 40 DIB + 4-entry gray palette), // top-down rows. Only the size and dimension fields vary; patch them in. // clang-format off uint8_t hdr[70] = { 'B', 'M', 0, 0, 0, 0, 0, 0, 0, 0, 70, 0, 0, 0, // file header 40, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, // DIB: w/h patched 0, 0, 0, 0, 0, 0, 0, 0, 0x13, 0x0B, 0, 0, 0x13, 0x0B, 0, 0, // 0, 2835 DPI 4, 0, 0, 0, 4, 0, 0, 0, // 4 palette colors 0x00, 0x00, 0x00, 0x00, 0x55, 0x55, 0x55, 0x00, // black, dark gray 0xAA, 0xAA, 0xAA, 0x00, 0xFF, 0xFF, 0xFF, 0x00}; // light gray, white // clang-format on const uint32_t imageSize = rowSize2 * pageInfo.height; const uint32_t fileSize = sizeof(hdr) + imageSize; const int32_t topDownHeight = -static_cast<int32_t>(pageInfo.height); memcpy(hdr + 2, &fileSize, 4); memcpy(hdr + 18, &pageInfo.width, 2); // biWidth (upper bytes stay 0) memcpy(hdr + 22, &topDownHeight, 4); // negative biHeight = top-down memcpy(hdr + 34, &imageSize, 4); coverBmp.write(hdr, sizeof(hdr));
const size_t colBytes = (pageInfo.height + 7) / 8; // Bytes per column const size_t planeSize = static_cast<size_t>(pageInfo.width) * colBytes; const uint8_t* plane1 = pageBuffer; // Bit1 plane const uint8_t* plane2 = pageBuffer + planeSize; // Bit2 plane
// XTH value -> BMP palette index (palette: 0=black, 1=dark, 2=light, 3=white) static constexpr uint8_t kXthToBmp[4] = {3, 1, 2, 0}; for (uint16_t y = 0; y < pageInfo.height; y++) { memset(rowBuffer, 0x00, rowSize2);
for (uint16_t x = 0; x < pageInfo.width; x++) { // Column-major, right to left: column index = (width - 1 - x) const size_t colIndex = pageInfo.width - 1 - x; const size_t byteInCol = y / 8; const size_t bitInByte = 7 - (y % 8); // MSB = topmost pixel
const size_t byteOffset = colIndex * colBytes + byteInCol; const uint8_t bit1 = (plane1[byteOffset] >> bitInByte) & 1; const uint8_t bit2 = (plane2[byteOffset] >> bitInByte) & 1; const uint8_t pixelValue = (bit1 << 1) | bit2;
const uint8_t bmpVal = kXthToBmp[pixelValue]; rowBuffer[(x * 2) / 8] |= bmpVal << (6 - ((x * 2) % 8)); }
// Row buffer is rowSize2 bytes and zero-padded, write it whole coverBmp.write(rowBuffer, rowSize2); }
} else { // Write 1-bit BMP header (top-down row order) BmpHeader bmpHeader; createBmpHeader(&bmpHeader, pageInfo.width, pageInfo.height, BmpRowOrder::TopDown); coverBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeader), sizeof(bmpHeader));
const uint32_t rowSize = ((pageInfo.width + 31) / 32) * 4; // 1-bit source: write directly with proper padding const size_t srcRowSize = (pageInfo.width + 7) / 8;
for (uint16_t y = 0; y < pageInfo.height; y++) { // Write source row coverBmp.write(pageBuffer + y * srcRowSize, srcRowSize);
// Pad to 4-byte boundary uint8_t padding[4] = {0, 0, 0, 0}; size_t paddingSize = rowSize - srcRowSize; if (paddingSize > 0) { coverBmp.write(padding, paddingSize); } } } LOG_DBG("XTC", "Generated cover BMP: %s", getCoverBmpPath().c_str()); return true;}
std::string Xtc::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }std::string Xtc::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
bool Xtc::generateThumbBmp(int height) const { // Already generated if (Storage.exists(getThumbBmpPath(height).c_str())) { return true; }
if (!loaded || !parser) { LOG_ERR("XTC", "Cannot generate thumb BMP, file not loaded"); return false; }
if (parser->getPageCount() == 0) { LOG_ERR("XTC", "No pages in XTC file"); return false; }
// Setup cache directory setupCacheDir();
// Get first page info for cover xtc::PageInfo pageInfo; if (!parser->getPageInfo(0, pageInfo)) { LOG_DBG("XTC", "Failed to get first page info"); return false; }
// Get bit depth const uint8_t bitDepth = parser->getBitDepth();
// Calculate target dimensions for thumbnail (fit within 240x400 Continue Reading card) int THUMB_TARGET_WIDTH = height * 0.6; int THUMB_TARGET_HEIGHT = height;
// Calculate scale factor float scaleX = static_cast<float>(THUMB_TARGET_WIDTH) / pageInfo.width; float scaleY = static_cast<float>(THUMB_TARGET_HEIGHT) / pageInfo.height; float scale = (scaleX > scaleY) ? scaleX : scaleY; // for cropping
// Only scale down, never up if (scale >= 1.0f) { // Page is already small enough, just use cover.bmp // Copy cover.bmp to thumb.bmp if (generateCoverBmp()) { HalFile src, dst; if (Storage.openFileForRead("XTC", getCoverBmpPath(), src)) { if (Storage.openFileForWrite("XTC", getThumbBmpPath(height), dst)) { uint8_t buffer[512]; while (src.available()) { size_t bytesRead = src.read(buffer, sizeof(buffer)); dst.write(buffer, bytesRead); } } } LOG_DBG("XTC", "Copied cover to thumb (no scaling needed)"); return Storage.exists(getThumbBmpPath(height).c_str()); } return false; }
uint16_t thumbWidth = static_cast<uint16_t>(pageInfo.width * scale); uint16_t thumbHeight = static_cast<uint16_t>(pageInfo.height * scale);
LOG_DBG("XTC", "Generating thumb BMP: %dx%d -> %dx%d (scale: %.3f)", pageInfo.width, pageInfo.height, thumbWidth, thumbHeight, scale);
// Allocate buffer for page data size_t bitmapSize; if (bitDepth == 2) { bitmapSize = static_cast<size_t>(pageInfo.width) * ((static_cast<size_t>(pageInfo.height) + 7) / 8) * 2; } else { bitmapSize = ((pageInfo.width + 7) / 8) * pageInfo.height; } const uint32_t rowSize = (thumbWidth + 31) / 32 * 4; auto thumbScratch = makeUniqueNoThrow<uint8_t[]>(bitmapSize + rowSize); if (!thumbScratch) { LOG_ERR("XTC", "Failed to allocate thumb buffers (%lu bytes)", static_cast<unsigned long>(bitmapSize + rowSize)); return false; } uint8_t* pageBuffer = thumbScratch.get(); uint8_t* rowBuffer = thumbScratch.get() + bitmapSize;
// Load first page (cover) size_t bytesRead = const_cast<xtc::XtcParser*>(parser.get())->loadPage(0, pageBuffer, bitmapSize); if (bytesRead == 0) { LOG_ERR("XTC", "Failed to load cover page for thumb"); return false; }
// Create thumbnail BMP file - use 1-bit format for fast home screen rendering (no gray passes) HalFile thumbBmp; if (!Storage.openFileForWrite("XTC", getThumbBmpPath(height), thumbBmp)) { LOG_DBG("XTC", "Failed to create thumb BMP file"); return false; }
// Write 1-bit BMP header (top-down row order) BmpHeader bmpHeader; createBmpHeader(&bmpHeader, thumbWidth, thumbHeight, BmpRowOrder::TopDown); thumbBmp.write(reinterpret_cast<const uint8_t*>(&bmpHeader), sizeof(bmpHeader));
// 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 colBytes = (bitDepth == 2) ? ((pageInfo.height + 7) / 8) : 0; const size_t planeSize = (bitDepth == 2) ? static_cast<size_t>(pageInfo.width) * colBytes : 0; const uint8_t* plane1 = (bitDepth == 2) ? pageBuffer : nullptr; const uint8_t* plane2 = (bitDepth == 2) ? pageBuffer + planeSize : nullptr; const size_t srcRowBytes = (bitDepth == 1) ? ((pageInfo.width + 7) / 8) : 0; uint8_t rowsSinceYield = 0;
for (uint16_t dstY = 0; dstY < thumbHeight; dstY++) { memset(rowBuffer, 0xFF, rowSize); // Start with all white (bit 1)
// Calculate source Y range with bounds checking 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 < thumbWidth; dstX++) { // Calculate source X range with bounds checking 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: sum grayscale values (0-255 range) 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; // Default: white
if (bitDepth == 2) { // XTH 2-bit mode: pixel value 0-3 // Bounds check for column index 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; // Bounds check for buffer access 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; // pixelValue: 0=white, 1=dark gray, 2=light gray, 3=black — // same semantics as the cover's kXthToBmp mapping above static constexpr uint8_t kXthToGray[4] = {255, 85, 170, 0}; grayValue = kXthToGray[pixelValue]; } } } else { // 1-bit mode const size_t byteIdx = srcY * srcRowBytes + srcX / 8; const size_t bitIdx = 7 - (srcX % 8); // Bounds check for buffer access if (byteIdx < bitmapSize) { const uint8_t pixelBit = (pageBuffer[byteIdx] >> bitIdx) & 1; // XTC 1-bit polarity: 0=black, 1=white (same as BMP palette) grayValue = pixelBit ? 255 : 0; } }
graySum += grayValue; totalCount++; } }
// Calculate average grayscale and quantize to 1-bit with noise dithering uint8_t avgGray = (totalCount > 0) ? static_cast<uint8_t>(graySum / totalCount) : 255;
// Hash-based noise dithering for 1-bit output 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); // 0-255 const int adjustedThreshold = 128 + ((threshold - 128) / 2); // Range: 64-192
// Quantize to 1-bit: 0=black, 1=white uint8_t oneBit = (avgGray >= adjustedThreshold) ? 1 : 0;
// Pack 1-bit value into row buffer (MSB first, 8 pixels per byte) const size_t byteIndex = dstX / 8; const size_t bitOffset = 7 - (dstX % 8); // Bounds check for row buffer access if (byteIndex < rowSize) { if (oneBit) { rowBuffer[byteIndex] |= (1 << bitOffset); // Set bit for white } else { rowBuffer[byteIndex] &= ~(1 << bitOffset); // Clear bit for black } } }
// Write row (already padded to 4-byte boundary by rowSize) thumbBmp.write(rowBuffer, rowSize); yieldDuringThumbnail(rowsSinceYield); } LOG_DBG("XTC", "Generated thumb BMP (%dx%d): %s", thumbWidth, thumbHeight, getThumbBmpPath(height).c_str()); return true;}
uint32_t Xtc::getPageCount() const { if (!loaded || !parser) { return 0; } return parser->getPageCount();}
uint16_t Xtc::getPageWidth() const { if (!loaded || !parser) { return 0; } return parser->getWidth();}
uint16_t Xtc::getPageHeight() const { if (!loaded || !parser) { return 0; } return parser->getHeight();}
uint8_t Xtc::getBitDepth() const { if (!loaded || !parser) { return 1; // Default to 1-bit } return parser->getBitDepth();}
size_t Xtc::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize) const { if (!loaded || !parser) { return 0; } return const_cast<xtc::XtcParser*>(parser.get())->loadPage(pageIndex, buffer, bufferSize);}
xtc::XtcError Xtc::loadPageStreaming(uint32_t pageIndex, std::function<void(const uint8_t* data, size_t size, size_t offset)> callback, size_t chunkSize) const { if (!loaded || !parser) { return xtc::XtcError::FILE_NOT_FOUND; } return const_cast<xtc::XtcParser*>(parser.get())->loadPageStreaming(pageIndex, callback, chunkSize);}
uint8_t Xtc::calculateProgress(uint32_t currentPage) const { if (!loaded || !parser || parser->getPageCount() == 0) { return 0; } return static_cast<uint8_t>((currentPage + 1) * 100 / parser->getPageCount());}
xtc::XtcError Xtc::getLastError() const { if (!parser) { return xtc::XtcError::FILE_NOT_FOUND; } return parser->getLastError();}