Add XTC/XTCH ebook format support (#135)
## Summary * **What is the goal of this PR?** Add support for XTC (XTeink X4 native) ebook format, which contains pre-rendered 480x800 1-bit bitmap pages optimized for e-ink displays. * **What changes are included?** - New `lib/Xtc/` library with XtcParser for reading XTC files - XtcReaderActivity for displaying XTC pages on e-ink display - XTC file detection in FileSelectionActivity - Cover BMP generation from first XTC page - Correct XTG page header structure (22 bytes) and bit polarity handling ## Additional Context - XTC files contain pre-rendered bitmap pages with embedded status bar (page numbers, progress %) - XTG page header: 22 bytes (magic + dimensions + reserved fields + bitmap size) - Bit polarity: 0 = black, 1 = white - No runtime text rendering needed - pages display directly on e-ink - Faster page display compared to EPUB since no parsing/rendering required - Memory efficient: loads one page at a time (48KB per page) - Tested with XTC files generated from https://x4converter.rho.sh/ - Verified correct page alignment and color rendering - Please report any issues if you test with XTC files from other sources. --------- Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
316
lib/Xtc/Xtc/XtcParser.cpp
Normal file
316
lib/Xtc/Xtc/XtcParser.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
/**
|
||||
* XtcParser.cpp
|
||||
*
|
||||
* XTC file parsing implementation
|
||||
* XTC ebook support for CrossPoint Reader
|
||||
*/
|
||||
|
||||
#include "XtcParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace xtc {
|
||||
|
||||
XtcParser::XtcParser()
|
||||
: m_isOpen(false),
|
||||
m_defaultWidth(DISPLAY_WIDTH),
|
||||
m_defaultHeight(DISPLAY_HEIGHT),
|
||||
m_bitDepth(1),
|
||||
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 (!FsHelpers::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 if available
|
||||
readTitle();
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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_title.clear();
|
||||
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
|
||||
if (m_header.version > 1) {
|
||||
Serial.printf("[%lu] [XTC] Unsupported version: %d\n", millis(), m_header.version);
|
||||
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, pages=%u, bitDepth=%u\n", millis(), m_header.magic,
|
||||
(m_header.magic == XTCH_MAGIC) ? "XTCH" : "XTC", m_header.version, m_header.pageCount, m_bitDepth);
|
||||
|
||||
return XtcError::OK;
|
||||
}
|
||||
|
||||
XtcError XtcParser::readTitle() {
|
||||
// Title is usually at offset 0x38 (56) for 88-byte headers
|
||||
// Read title as null-terminated UTF-8 string
|
||||
if (m_header.titleOffset == 0) {
|
||||
m_header.titleOffset = 0x38; // Default offset
|
||||
}
|
||||
|
||||
if (!m_file.seek(m_header.titleOffset)) {
|
||||
return XtcError::READ_ERROR;
|
||||
}
|
||||
|
||||
char titleBuf[128] = {0};
|
||||
m_file.read(reinterpret_cast<uint8_t*>(titleBuf), sizeof(titleBuf) - 1);
|
||||
m_title = titleBuf;
|
||||
|
||||
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.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;
|
||||
}
|
||||
|
||||
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) {
|
||||
File file = SD.open(filepath, FILE_READ);
|
||||
if (!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
|
||||
96
lib/Xtc/Xtc/XtcParser.h
Normal file
96
lib/Xtc/Xtc/XtcParser.h
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* XtcParser.h
|
||||
*
|
||||
* XTC file parsing and page data extraction
|
||||
* XTC ebook support for CrossPoint Reader
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <SD.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "XtcTypes.h"
|
||||
|
||||
namespace xtc {
|
||||
|
||||
/**
|
||||
* XTC File Parser
|
||||
*
|
||||
* Reads XTC files from SD card and extracts page data.
|
||||
* Designed for ESP32-C3's limited RAM (~380KB) using streaming.
|
||||
*/
|
||||
class XtcParser {
|
||||
public:
|
||||
XtcParser();
|
||||
~XtcParser();
|
||||
|
||||
// File open/close
|
||||
XtcError open(const char* filepath);
|
||||
void close();
|
||||
bool isOpen() const { return m_isOpen; }
|
||||
|
||||
// Header information access
|
||||
const XtcHeader& getHeader() const { return m_header; }
|
||||
uint16_t getPageCount() const { return m_header.pageCount; }
|
||||
uint16_t getWidth() const { return m_defaultWidth; }
|
||||
uint16_t getHeight() const { return m_defaultHeight; }
|
||||
uint8_t getBitDepth() const { return m_bitDepth; } // 1 = XTC/XTG, 2 = XTCH/XTH
|
||||
|
||||
// Page information
|
||||
bool getPageInfo(uint32_t pageIndex, PageInfo& info) const;
|
||||
|
||||
/**
|
||||
* Load page bitmap (raw 1-bit data, skipping XTG header)
|
||||
*
|
||||
* @param pageIndex Page index (0-based)
|
||||
* @param buffer Output buffer (caller allocated)
|
||||
* @param bufferSize Buffer size
|
||||
* @return Number of bytes read on success, 0 on failure
|
||||
*/
|
||||
size_t loadPage(uint32_t pageIndex, uint8_t* buffer, size_t bufferSize);
|
||||
|
||||
/**
|
||||
* Streaming page load
|
||||
* Memory-efficient method that reads page data in chunks.
|
||||
*
|
||||
* @param pageIndex Page index
|
||||
* @param callback Callback function to receive data chunks
|
||||
* @param chunkSize Chunk size (default: 1024 bytes)
|
||||
* @return Error code
|
||||
*/
|
||||
XtcError loadPageStreaming(uint32_t pageIndex,
|
||||
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
|
||||
size_t chunkSize = 1024);
|
||||
|
||||
// Get title from metadata
|
||||
std::string getTitle() const { return m_title; }
|
||||
|
||||
// Validation
|
||||
static bool isValidXtcFile(const char* filepath);
|
||||
|
||||
// Error information
|
||||
XtcError getLastError() const { return m_lastError; }
|
||||
|
||||
private:
|
||||
File m_file;
|
||||
bool m_isOpen;
|
||||
XtcHeader m_header;
|
||||
std::vector<PageInfo> m_pageTable;
|
||||
std::string m_title;
|
||||
uint16_t m_defaultWidth;
|
||||
uint16_t m_defaultHeight;
|
||||
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
|
||||
XtcError m_lastError;
|
||||
|
||||
// Internal helper functions
|
||||
XtcError readHeader();
|
||||
XtcError readPageTable();
|
||||
XtcError readTitle();
|
||||
};
|
||||
|
||||
} // namespace xtc
|
||||
147
lib/Xtc/Xtc/XtcTypes.h
Normal file
147
lib/Xtc/Xtc/XtcTypes.h
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* XtcTypes.h
|
||||
*
|
||||
* XTC file format type definitions
|
||||
* XTC ebook support for CrossPoint Reader
|
||||
*
|
||||
* XTC is the native binary ebook format for XTeink X4 e-reader.
|
||||
* It stores pre-rendered bitmap images per page.
|
||||
*
|
||||
* Format based on EPUB2XTC converter by Rafal-P-Mazur
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace xtc {
|
||||
|
||||
// XTC file magic numbers (little-endian)
|
||||
// "XTC\0" = 0x58, 0x54, 0x43, 0x00
|
||||
constexpr uint32_t XTC_MAGIC = 0x00435458; // "XTC\0" in little-endian (1-bit fast mode)
|
||||
// "XTCH" = 0x58, 0x54, 0x43, 0x48
|
||||
constexpr uint32_t XTCH_MAGIC = 0x48435458; // "XTCH" in little-endian (2-bit high quality mode)
|
||||
// "XTG\0" = 0x58, 0x54, 0x47, 0x00
|
||||
constexpr uint32_t XTG_MAGIC = 0x00475458; // "XTG\0" for 1-bit page data
|
||||
// "XTH\0" = 0x58, 0x54, 0x48, 0x00
|
||||
constexpr uint32_t XTH_MAGIC = 0x00485458; // "XTH\0" for 2-bit page data
|
||||
|
||||
// XTeink X4 display resolution
|
||||
constexpr uint16_t DISPLAY_WIDTH = 480;
|
||||
constexpr uint16_t DISPLAY_HEIGHT = 800;
|
||||
|
||||
// XTC file header (56 bytes)
|
||||
#pragma pack(push, 1)
|
||||
struct XtcHeader {
|
||||
uint32_t magic; // 0x00: Magic number "XTC\0" (0x00435458)
|
||||
uint16_t version; // 0x04: Format version (typically 1)
|
||||
uint16_t pageCount; // 0x06: Total page count
|
||||
uint32_t flags; // 0x08: Flags/reserved
|
||||
uint32_t headerSize; // 0x0C: Size of header section (typically 88)
|
||||
uint32_t reserved1; // 0x10: Reserved
|
||||
uint32_t tocOffset; // 0x14: TOC offset (0 if unused) - 4 bytes, not 8!
|
||||
uint64_t pageTableOffset; // 0x18: Page table offset
|
||||
uint64_t dataOffset; // 0x20: First page data offset
|
||||
uint64_t reserved2; // 0x28: Reserved
|
||||
uint32_t titleOffset; // 0x30: Title string offset
|
||||
uint32_t padding; // 0x34: Padding to 56 bytes
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// Page table entry (16 bytes per page)
|
||||
#pragma pack(push, 1)
|
||||
struct PageTableEntry {
|
||||
uint64_t dataOffset; // 0x00: Absolute offset to page data
|
||||
uint32_t dataSize; // 0x08: Page data size in bytes
|
||||
uint16_t width; // 0x0C: Page width (480)
|
||||
uint16_t height; // 0x0E: Page height (800)
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// XTG/XTH page data header (22 bytes)
|
||||
// Used for both 1-bit (XTG) and 2-bit (XTH) formats
|
||||
#pragma pack(push, 1)
|
||||
struct XtgPageHeader {
|
||||
uint32_t magic; // 0x00: File identifier (XTG: 0x00475458, XTH: 0x00485458)
|
||||
uint16_t width; // 0x04: Image width (pixels)
|
||||
uint16_t height; // 0x06: Image height (pixels)
|
||||
uint8_t colorMode; // 0x08: Color mode (0=monochrome)
|
||||
uint8_t compression; // 0x09: Compression (0=uncompressed)
|
||||
uint32_t dataSize; // 0x0A: Image data size (bytes)
|
||||
uint64_t md5; // 0x0E: MD5 checksum (first 8 bytes, optional)
|
||||
// Followed by bitmap data at offset 0x16 (22)
|
||||
//
|
||||
// XTG (1-bit): Row-major, 8 pixels/byte, MSB first
|
||||
// dataSize = ((width + 7) / 8) * height
|
||||
//
|
||||
// XTH (2-bit): Two bit planes, column-major (right-to-left), 8 vertical pixels/byte
|
||||
// dataSize = ((width * height + 7) / 8) * 2
|
||||
// First plane: Bit1 for all pixels
|
||||
// Second plane: Bit2 for all pixels
|
||||
// pixelValue = (bit1 << 1) | bit2
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// Page information (internal use, optimized for memory)
|
||||
struct PageInfo {
|
||||
uint32_t offset; // File offset to page data (max 4GB file size)
|
||||
uint32_t size; // Data size (bytes)
|
||||
uint16_t width; // Page width
|
||||
uint16_t height; // Page height
|
||||
uint8_t bitDepth; // 1 = XTG (1-bit), 2 = XTH (2-bit grayscale)
|
||||
uint8_t padding; // Alignment padding
|
||||
}; // 16 bytes total
|
||||
|
||||
// Error codes
|
||||
enum class XtcError {
|
||||
OK = 0,
|
||||
FILE_NOT_FOUND,
|
||||
INVALID_MAGIC,
|
||||
INVALID_VERSION,
|
||||
CORRUPTED_HEADER,
|
||||
PAGE_OUT_OF_RANGE,
|
||||
READ_ERROR,
|
||||
WRITE_ERROR,
|
||||
MEMORY_ERROR,
|
||||
DECOMPRESSION_ERROR,
|
||||
};
|
||||
|
||||
// Convert error code to string
|
||||
inline const char* errorToString(XtcError err) {
|
||||
switch (err) {
|
||||
case XtcError::OK:
|
||||
return "OK";
|
||||
case XtcError::FILE_NOT_FOUND:
|
||||
return "File not found";
|
||||
case XtcError::INVALID_MAGIC:
|
||||
return "Invalid magic number";
|
||||
case XtcError::INVALID_VERSION:
|
||||
return "Unsupported version";
|
||||
case XtcError::CORRUPTED_HEADER:
|
||||
return "Corrupted header";
|
||||
case XtcError::PAGE_OUT_OF_RANGE:
|
||||
return "Page out of range";
|
||||
case XtcError::READ_ERROR:
|
||||
return "Read error";
|
||||
case XtcError::WRITE_ERROR:
|
||||
return "Write error";
|
||||
case XtcError::MEMORY_ERROR:
|
||||
return "Memory allocation error";
|
||||
case XtcError::DECOMPRESSION_ERROR:
|
||||
return "Decompression error";
|
||||
default:
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if filename has XTC/XTCH extension
|
||||
*/
|
||||
inline bool isXtcExtension(const char* filename) {
|
||||
if (!filename) return false;
|
||||
const char* ext = strrchr(filename, '.');
|
||||
if (!ext) return false;
|
||||
return (strcasecmp(ext, ".xtc") == 0 || strcasecmp(ext, ".xtch") == 0);
|
||||
}
|
||||
|
||||
} // namespace xtc
|
||||
Reference in New Issue
Block a user