/** * Xtc.cpp * * Main XTC ebook class implementation * XTC ebook support for CrossPoint Reader */ #include "Xtc.h" #include #include #include bool Xtc::load() { Serial.printf("[%lu] [XTC] Loading XTC: %s\n", millis(), filepath.c_str()); // Initialize parser parser.reset(new xtc::XtcParser()); // Open XTC file xtc::XtcError err = parser->open(filepath.c_str()); if (err != xtc::XtcError::OK) { Serial.printf("[%lu] [XTC] Failed to load: %s\n", millis(), xtc::errorToString(err)); parser.reset(); return false; } loaded = true; Serial.printf("[%lu] [XTC] Loaded XTC: %s (%lu pages)\n", millis(), filepath.c_str(), parser->getPageCount()); return true; } bool Xtc::clearCache() const { if (!SD.exists(cachePath.c_str())) { Serial.printf("[%lu] [XTC] Cache does not exist, no action needed\n", millis()); return true; } if (!FsHelpers::removeDir(cachePath.c_str())) { Serial.printf("[%lu] [XTC] Failed to clear cache\n", millis()); return false; } Serial.printf("[%lu] [XTC] Cache cleared successfully\n", millis()); return true; } void Xtc::setupCacheDir() const { if (SD.exists(cachePath.c_str())) { return; } // Create directories recursively for (size_t i = 1; i < cachePath.length(); i++) { if (cachePath[i] == '/') { SD.mkdir(cachePath.substr(0, i).c_str()); } } SD.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::getCoverBmpPath() const { return cachePath + "/cover.bmp"; } bool Xtc::generateCoverBmp() const { // Already generated if (SD.exists(getCoverBmpPath().c_str())) { return true; } if (!loaded || !parser) { Serial.printf("[%lu] [XTC] Cannot generate cover 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; } // Allocate buffer for page data (XTC is always 1-bit monochrome) const size_t bitmapSize = ((pageInfo.width + 7) / 8) * pageInfo.height; uint8_t* pageBuffer = static_cast(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(parser.get())->loadPage(0, pageBuffer, bitmapSize); if (bytesRead == 0) { Serial.printf("[%lu] [XTC] Failed to load cover page\n", millis()); free(pageBuffer); return false; } // Create BMP file File coverBmp; if (!FsHelpers::openFileForWrite("XTC", getCoverBmpPath(), coverBmp)) { Serial.printf("[%lu] [XTC] Failed to create cover BMP file\n", millis()); free(pageBuffer); return false; } // Write BMP header // BMP file header (14 bytes) const uint32_t rowSize = ((pageInfo.width + 31) / 32) * 4; // Row size aligned to 4 bytes const uint32_t imageSize = rowSize * pageInfo.height; const uint32_t fileSize = 14 + 40 + 8 + imageSize; // Header + DIB + palette + data // File header coverBmp.write('B'); coverBmp.write('M'); coverBmp.write(reinterpret_cast(&fileSize), 4); uint32_t reserved = 0; coverBmp.write(reinterpret_cast(&reserved), 4); uint32_t dataOffset = 14 + 40 + 8; // 1-bit palette has 2 colors (8 bytes) coverBmp.write(reinterpret_cast(&dataOffset), 4); // DIB header (BITMAPINFOHEADER - 40 bytes) uint32_t dibHeaderSize = 40; coverBmp.write(reinterpret_cast(&dibHeaderSize), 4); int32_t width = pageInfo.width; coverBmp.write(reinterpret_cast(&width), 4); int32_t height = -static_cast(pageInfo.height); // Negative for top-down coverBmp.write(reinterpret_cast(&height), 4); uint16_t planes = 1; coverBmp.write(reinterpret_cast(&planes), 2); uint16_t bitsPerPixel = 1; // 1-bit monochrome coverBmp.write(reinterpret_cast(&bitsPerPixel), 2); uint32_t compression = 0; // BI_RGB (no compression) coverBmp.write(reinterpret_cast(&compression), 4); coverBmp.write(reinterpret_cast(&imageSize), 4); int32_t ppmX = 2835; // 72 DPI 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); // Color palette (2 colors for 1-bit) // XTC uses inverted polarity: 0 = black, 1 = white // Color 0: Black (text/foreground in XTC) uint8_t black[4] = {0x00, 0x00, 0x00, 0x00}; coverBmp.write(black, 4); // Color 1: White (background in XTC) uint8_t white[4] = {0xFF, 0xFF, 0xFF, 0x00}; coverBmp.write(white, 4); // Write bitmap data // XTC stores data as packed bits, same as BMP 1-bit format // But we need to ensure proper row alignment (4-byte boundary) const size_t srcRowSize = (pageInfo.width + 7) / 8; // Source row size 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); } } coverBmp.close(); free(pageBuffer); Serial.printf("[%lu] [XTC] Generated cover BMP: %s\n", millis(), getCoverBmpPath().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(); } size_t Xtc::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize) const { if (!loaded || !parser) { return 0; } return const_cast(parser.get())->loadPage(pageIndex, buffer, bufferSize); } xtc::XtcError Xtc::loadPageStreaming(uint32_t pageIndex, std::function callback, size_t chunkSize) const { if (!loaded || !parser) { return xtc::XtcError::FILE_NOT_FOUND; } return const_cast(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((currentPage + 1) * 100 / parser->getPageCount()); } xtc::XtcError Xtc::getLastError() const { if (!parser) { return xtc::XtcError::FILE_NOT_FOUND; } return parser->getLastError(); }