mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 22:57:50 +03:00
## Summary * Extract author from XTC/XTCH files ## Additional Context * Based on updated details in https://gist.github.com/CrazyCoder/b125f26d6987c0620058249f59f1327d --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No
463 lines
13 KiB
C++
463 lines
13 KiB
C++
/**
|
|
* XtcParser.cpp
|
|
*
|
|
* XTC file parsing implementation
|
|
* XTC ebook support for CrossPoint Reader
|
|
*/
|
|
|
|
#include "XtcParser.h"
|
|
|
|
#include <FsHelpers.h>
|
|
#include <HardwareSerial.h>
|
|
#include <SDCardManager.h>
|
|
|
|
#include <cstring>
|
|
|
|
namespace xtc {
|
|
|
|
XtcParser::XtcParser()
|
|
: m_isOpen(false),
|
|
m_defaultWidth(DISPLAY_WIDTH),
|
|
m_defaultHeight(DISPLAY_HEIGHT),
|
|
m_bitDepth(1),
|
|
m_hasChapters(false),
|
|
m_lastError(XtcError::OK) {
|
|
memset(&m_header, 0, sizeof(m_header));
|
|
}
|
|
|
|
XtcParser::~XtcParser() { close(); }
|
|
|
|
XtcError XtcParser::open(const char* filepath) {
|
|
// Close if already open
|
|
if (m_isOpen) {
|
|
close();
|
|
}
|
|
|
|
// Open file
|
|
if (!SdMan.openFileForRead("XTC", filepath, m_file)) {
|
|
m_lastError = XtcError::FILE_NOT_FOUND;
|
|
return m_lastError;
|
|
}
|
|
|
|
// Read header
|
|
m_lastError = readHeader();
|
|
if (m_lastError != XtcError::OK) {
|
|
Serial.printf("[%lu] [XTC] Failed to read header: %s\n", millis(), errorToString(m_lastError));
|
|
m_file.close();
|
|
return m_lastError;
|
|
}
|
|
|
|
// Read title & author if available
|
|
if (m_header.hasMetadata) {
|
|
m_lastError = readTitle();
|
|
if (m_lastError != XtcError::OK) {
|
|
Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError));
|
|
m_file.close();
|
|
return m_lastError;
|
|
}
|
|
m_lastError = readAuthor();
|
|
if (m_lastError != XtcError::OK) {
|
|
Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError));
|
|
m_file.close();
|
|
return m_lastError;
|
|
}
|
|
}
|
|
|
|
// Read page table
|
|
m_lastError = readPageTable();
|
|
if (m_lastError != XtcError::OK) {
|
|
Serial.printf("[%lu] [XTC] Failed to read page table: %s\n", millis(), errorToString(m_lastError));
|
|
m_file.close();
|
|
return m_lastError;
|
|
}
|
|
|
|
// Read chapters if present
|
|
m_lastError = readChapters();
|
|
if (m_lastError != XtcError::OK) {
|
|
Serial.printf("[%lu] [XTC] Failed to read chapters: %s\n", millis(), errorToString(m_lastError));
|
|
m_file.close();
|
|
return m_lastError;
|
|
}
|
|
|
|
m_isOpen = true;
|
|
Serial.printf("[%lu] [XTC] Opened file: %s (%u pages, %dx%d)\n", millis(), filepath, m_header.pageCount,
|
|
m_defaultWidth, m_defaultHeight);
|
|
return XtcError::OK;
|
|
}
|
|
|
|
void XtcParser::close() {
|
|
if (m_isOpen) {
|
|
m_file.close();
|
|
m_isOpen = false;
|
|
}
|
|
m_pageTable.clear();
|
|
m_chapters.clear();
|
|
m_title.clear();
|
|
m_hasChapters = false;
|
|
memset(&m_header, 0, sizeof(m_header));
|
|
}
|
|
|
|
XtcError XtcParser::readHeader() {
|
|
// Read first 56 bytes of header
|
|
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&m_header), sizeof(XtcHeader));
|
|
if (bytesRead != sizeof(XtcHeader)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
// Verify magic number (accept both XTC and XTCH)
|
|
if (m_header.magic != XTC_MAGIC && m_header.magic != XTCH_MAGIC) {
|
|
Serial.printf("[%lu] [XTC] Invalid magic: 0x%08X (expected 0x%08X or 0x%08X)\n", millis(), m_header.magic,
|
|
XTC_MAGIC, XTCH_MAGIC);
|
|
return XtcError::INVALID_MAGIC;
|
|
}
|
|
|
|
// Determine bit depth from file magic
|
|
m_bitDepth = (m_header.magic == XTCH_MAGIC) ? 2 : 1;
|
|
|
|
// Check version
|
|
// Currently, version 1.0 is the only valid version, however some generators are swapping the bytes around, so we
|
|
// accept both 1.0 and 0.1 for compatibility
|
|
const bool validVersion = m_header.versionMajor == 1 && m_header.versionMinor == 0 ||
|
|
m_header.versionMajor == 0 && m_header.versionMinor == 1;
|
|
if (!validVersion) {
|
|
Serial.printf("[%lu] [XTC] Unsupported version: %u.%u\n", millis(), m_header.versionMajor, m_header.versionMinor);
|
|
return XtcError::INVALID_VERSION;
|
|
}
|
|
|
|
// Basic validation
|
|
if (m_header.pageCount == 0) {
|
|
return XtcError::CORRUPTED_HEADER;
|
|
}
|
|
|
|
Serial.printf("[%lu] [XTC] Header: magic=0x%08X (%s), ver=%u.%u, pages=%u, bitDepth=%u\n", millis(), m_header.magic,
|
|
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.versionMajor, m_header.versionMinor,
|
|
m_header.pageCount, m_bitDepth);
|
|
|
|
return XtcError::OK;
|
|
}
|
|
|
|
XtcError XtcParser::readTitle() {
|
|
constexpr auto titleOffset = 0x38;
|
|
if (!m_file.seek(titleOffset)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
char titleBuf[128] = {0};
|
|
m_file.read(titleBuf, sizeof(titleBuf) - 1);
|
|
m_title = titleBuf;
|
|
|
|
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());
|
|
return XtcError::OK;
|
|
}
|
|
|
|
XtcError XtcParser::readAuthor() {
|
|
// Read author as null-terminated UTF-8 string with max length 64, directly following title
|
|
constexpr auto authorOffset = 0xB8;
|
|
if (!m_file.seek(authorOffset)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
char authorBuf[64] = {0};
|
|
m_file.read(authorBuf, sizeof(authorBuf) - 1);
|
|
m_author = authorBuf;
|
|
|
|
Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str());
|
|
return XtcError::OK;
|
|
}
|
|
|
|
XtcError XtcParser::readPageTable() {
|
|
if (m_header.pageTableOffset == 0) {
|
|
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());
|
|
return XtcError::CORRUPTED_HEADER;
|
|
}
|
|
|
|
// Seek to page table
|
|
if (!m_file.seek(m_header.pageTableOffset)) {
|
|
Serial.printf("[%lu] [XTC] Failed to seek to page table at %llu\n", millis(), m_header.pageTableOffset);
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
m_pageTable.resize(m_header.pageCount);
|
|
|
|
// Read page table entries
|
|
for (uint16_t i = 0; i < m_header.pageCount; i++) {
|
|
PageTableEntry entry;
|
|
size_t bytesRead = m_file.read(reinterpret_cast<uint8_t*>(&entry), sizeof(PageTableEntry));
|
|
if (bytesRead != sizeof(PageTableEntry)) {
|
|
Serial.printf("[%lu] [XTC] Failed to read page table entry %u\n", millis(), i);
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
m_pageTable[i].offset = static_cast<uint32_t>(entry.dataOffset);
|
|
m_pageTable[i].size = entry.dataSize;
|
|
m_pageTable[i].width = entry.width;
|
|
m_pageTable[i].height = entry.height;
|
|
m_pageTable[i].bitDepth = m_bitDepth;
|
|
|
|
// Update default dimensions from first page
|
|
if (i == 0) {
|
|
m_defaultWidth = entry.width;
|
|
m_defaultHeight = entry.height;
|
|
}
|
|
}
|
|
|
|
Serial.printf("[%lu] [XTC] Read %u page table entries\n", millis(), m_header.pageCount);
|
|
return XtcError::OK;
|
|
}
|
|
|
|
XtcError XtcParser::readChapters() {
|
|
m_hasChapters = false;
|
|
m_chapters.clear();
|
|
|
|
uint8_t hasChaptersFlag = 0;
|
|
if (!m_file.seek(0x0B)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
if (m_file.read(&hasChaptersFlag, sizeof(hasChaptersFlag)) != sizeof(hasChaptersFlag)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
if (hasChaptersFlag != 1) {
|
|
return XtcError::OK;
|
|
}
|
|
|
|
uint64_t chapterOffset = 0;
|
|
if (!m_file.seek(0x30)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
if (m_file.read(reinterpret_cast<uint8_t*>(&chapterOffset), sizeof(chapterOffset)) != sizeof(chapterOffset)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
if (chapterOffset == 0) {
|
|
return XtcError::OK;
|
|
}
|
|
|
|
const uint64_t fileSize = m_file.size();
|
|
if (chapterOffset < sizeof(XtcHeader) || chapterOffset >= fileSize || chapterOffset + 96 > fileSize) {
|
|
return XtcError::OK;
|
|
}
|
|
|
|
uint64_t maxOffset = 0;
|
|
if (m_header.pageTableOffset > chapterOffset) {
|
|
maxOffset = m_header.pageTableOffset;
|
|
} else if (m_header.dataOffset > chapterOffset) {
|
|
maxOffset = m_header.dataOffset;
|
|
} else {
|
|
maxOffset = fileSize;
|
|
}
|
|
|
|
if (maxOffset <= chapterOffset) {
|
|
return XtcError::OK;
|
|
}
|
|
|
|
constexpr size_t chapterSize = 96;
|
|
const uint64_t available = maxOffset - chapterOffset;
|
|
const size_t chapterCount = static_cast<size_t>(available / chapterSize);
|
|
if (chapterCount == 0) {
|
|
return XtcError::OK;
|
|
}
|
|
|
|
if (!m_file.seek(chapterOffset)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
std::vector<uint8_t> chapterBuf(chapterSize);
|
|
for (size_t i = 0; i < chapterCount; i++) {
|
|
if (m_file.read(chapterBuf.data(), chapterSize) != chapterSize) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
char nameBuf[81];
|
|
memcpy(nameBuf, chapterBuf.data(), 80);
|
|
nameBuf[80] = '\0';
|
|
const size_t nameLen = strnlen(nameBuf, 80);
|
|
std::string name(nameBuf, nameLen);
|
|
|
|
uint16_t startPage = 0;
|
|
uint16_t endPage = 0;
|
|
memcpy(&startPage, chapterBuf.data() + 0x50, sizeof(startPage));
|
|
memcpy(&endPage, chapterBuf.data() + 0x52, sizeof(endPage));
|
|
|
|
if (name.empty() && startPage == 0 && endPage == 0) {
|
|
break;
|
|
}
|
|
|
|
if (startPage > 0) {
|
|
startPage--;
|
|
}
|
|
if (endPage > 0) {
|
|
endPage--;
|
|
}
|
|
|
|
if (startPage >= m_header.pageCount) {
|
|
continue;
|
|
}
|
|
|
|
if (endPage >= m_header.pageCount) {
|
|
endPage = m_header.pageCount - 1;
|
|
}
|
|
|
|
if (startPage > endPage) {
|
|
continue;
|
|
}
|
|
|
|
ChapterInfo chapter{std::move(name), startPage, endPage};
|
|
m_chapters.push_back(std::move(chapter));
|
|
}
|
|
|
|
m_hasChapters = !m_chapters.empty();
|
|
Serial.printf("[%lu] [XTC] Chapters: %u\n", millis(), static_cast<unsigned int>(m_chapters.size()));
|
|
return XtcError::OK;
|
|
}
|
|
|
|
bool XtcParser::getPageInfo(uint32_t pageIndex, PageInfo& info) const {
|
|
if (pageIndex >= m_pageTable.size()) {
|
|
return false;
|
|
}
|
|
info = m_pageTable[pageIndex];
|
|
return true;
|
|
}
|
|
|
|
size_t XtcParser::loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize) {
|
|
if (!m_isOpen) {
|
|
m_lastError = XtcError::FILE_NOT_FOUND;
|
|
return 0;
|
|
}
|
|
|
|
if (pageIndex >= m_header.pageCount) {
|
|
m_lastError = XtcError::PAGE_OUT_OF_RANGE;
|
|
return 0;
|
|
}
|
|
|
|
const PageInfo& page = m_pageTable[pageIndex];
|
|
|
|
// Seek to page data
|
|
if (!m_file.seek(page.offset)) {
|
|
Serial.printf("[%lu] [XTC] Failed to seek to page %u at offset %lu\n", millis(), pageIndex, page.offset);
|
|
m_lastError = XtcError::READ_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
// Read page header (XTG for 1-bit, XTH for 2-bit - same structure)
|
|
XtgPageHeader pageHeader;
|
|
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
|
if (headerRead != sizeof(XtgPageHeader)) {
|
|
Serial.printf("[%lu] [XTC] Failed to read page header for page %u\n", millis(), pageIndex);
|
|
m_lastError = XtcError::READ_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
// Verify page magic (XTG for 1-bit, XTH for 2-bit)
|
|
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
|
if (pageHeader.magic != expectedMagic) {
|
|
Serial.printf("[%lu] [XTC] Invalid page magic for page %u: 0x%08X (expected 0x%08X)\n", millis(), pageIndex,
|
|
pageHeader.magic, expectedMagic);
|
|
m_lastError = XtcError::INVALID_MAGIC;
|
|
return 0;
|
|
}
|
|
|
|
// Calculate bitmap size based on bit depth
|
|
// XTG (1-bit): Row-major, ((width+7)/8) * height bytes
|
|
// XTH (2-bit): Two bit planes, column-major, ((width * height + 7) / 8) * 2 bytes
|
|
size_t bitmapSize;
|
|
if (m_bitDepth == 2) {
|
|
// XTH: two bit planes, each containing (width * height) bits rounded up to bytes
|
|
bitmapSize = ((static_cast<size_t>(pageHeader.width) * pageHeader.height + 7) / 8) * 2;
|
|
} else {
|
|
bitmapSize = ((pageHeader.width + 7) / 8) * pageHeader.height;
|
|
}
|
|
|
|
// Check buffer size
|
|
if (bufferSize < bitmapSize) {
|
|
Serial.printf("[%lu] [XTC] Buffer too small: need %u, have %u\n", millis(), bitmapSize, bufferSize);
|
|
m_lastError = XtcError::MEMORY_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
// Read bitmap data
|
|
size_t bytesRead = m_file.read(buffer, bitmapSize);
|
|
if (bytesRead != bitmapSize) {
|
|
Serial.printf("[%lu] [XTC] Page read error: expected %u, got %u\n", millis(), bitmapSize, bytesRead);
|
|
m_lastError = XtcError::READ_ERROR;
|
|
return 0;
|
|
}
|
|
|
|
m_lastError = XtcError::OK;
|
|
return bytesRead;
|
|
}
|
|
|
|
XtcError XtcParser::loadPageStreaming(uint32_t pageIndex,
|
|
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
|
|
size_t chunkSize) {
|
|
if (!m_isOpen) {
|
|
return XtcError::FILE_NOT_FOUND;
|
|
}
|
|
|
|
if (pageIndex >= m_header.pageCount) {
|
|
return XtcError::PAGE_OUT_OF_RANGE;
|
|
}
|
|
|
|
const PageInfo& page = m_pageTable[pageIndex];
|
|
|
|
// Seek to page data
|
|
if (!m_file.seek(page.offset)) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
// Read and skip page header (XTG for 1-bit, XTH for 2-bit)
|
|
XtgPageHeader pageHeader;
|
|
size_t headerRead = m_file.read(reinterpret_cast<uint8_t*>(&pageHeader), sizeof(XtgPageHeader));
|
|
const uint32_t expectedMagic = (m_bitDepth == 2) ? XTH_MAGIC : XTG_MAGIC;
|
|
if (headerRead != sizeof(XtgPageHeader) || pageHeader.magic != expectedMagic) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
// Calculate bitmap size based on bit depth
|
|
// XTG (1-bit): Row-major, ((width+7)/8) * height bytes
|
|
// XTH (2-bit): Two bit planes, ((width * height + 7) / 8) * 2 bytes
|
|
size_t bitmapSize;
|
|
if (m_bitDepth == 2) {
|
|
bitmapSize = ((static_cast<size_t>(pageHeader.width) * pageHeader.height + 7) / 8) * 2;
|
|
} else {
|
|
bitmapSize = ((pageHeader.width + 7) / 8) * pageHeader.height;
|
|
}
|
|
|
|
// Read in chunks
|
|
std::vector<uint8_t> chunk(chunkSize);
|
|
size_t totalRead = 0;
|
|
|
|
while (totalRead < bitmapSize) {
|
|
size_t toRead = std::min(chunkSize, bitmapSize - totalRead);
|
|
size_t bytesRead = m_file.read(chunk.data(), toRead);
|
|
|
|
if (bytesRead == 0) {
|
|
return XtcError::READ_ERROR;
|
|
}
|
|
|
|
callback(chunk.data(), bytesRead, totalRead);
|
|
totalRead += bytesRead;
|
|
}
|
|
|
|
return XtcError::OK;
|
|
}
|
|
|
|
bool XtcParser::isValidXtcFile(const char* filepath) {
|
|
FsFile file;
|
|
if (!SdMan.openFileForRead("XTC", filepath, file)) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t magic = 0;
|
|
size_t bytesRead = file.read(reinterpret_cast<uint8_t*>(&magic), sizeof(magic));
|
|
file.close();
|
|
|
|
if (bytesRead != sizeof(magic)) {
|
|
return false;
|
|
}
|
|
|
|
return (magic == XTC_MAGIC || magic == XTCH_MAGIC);
|
|
}
|
|
|
|
} // namespace xtc
|