feat: Extract author from XTC/XTCH files (#563)

## 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
This commit is contained in:
Dave Allie 2026-01-27 22:56:51 +11:00 committed by GitHub
parent e2ca0e94ca
commit 5e24895f6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 58 additions and 18 deletions

View File

@ -7,7 +7,6 @@
#include "Xtc.h" #include "Xtc.h"
#include <FsHelpers.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <SDCardManager.h> #include <SDCardManager.h>
@ -87,6 +86,15 @@ std::string Xtc::getTitle() const {
return filepath.substr(lastSlash, lastDot - 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 { bool Xtc::hasChapters() const {
if (!loaded || !parser) { if (!loaded || !parser) {
return false; return false;

View File

@ -56,6 +56,7 @@ class Xtc {
// Metadata // Metadata
std::string getTitle() const; std::string getTitle() const;
std::string getAuthor() const;
bool hasChapters() const; bool hasChapters() const;
const std::vector<xtc::ChapterInfo>& getChapters() const; const std::vector<xtc::ChapterInfo>& getChapters() const;

View File

@ -47,8 +47,21 @@ XtcError XtcParser::open(const char* filepath) {
return m_lastError; return m_lastError;
} }
// Read title if available // Read title & author if available
readTitle(); 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 // Read page table
m_lastError = readPageTable(); m_lastError = readPageTable();
@ -124,24 +137,34 @@ XtcError XtcParser::readHeader() {
} }
XtcError XtcParser::readTitle() { XtcError XtcParser::readTitle() {
// Title is usually at offset 0x38 (56) for 88-byte headers constexpr auto titleOffset = 0x38;
// Read title as null-terminated UTF-8 string if (!m_file.seek(titleOffset)) {
if (m_header.titleOffset == 0) {
m_header.titleOffset = 0x38; // Default offset
}
if (!m_file.seek(m_header.titleOffset)) {
return XtcError::READ_ERROR; return XtcError::READ_ERROR;
} }
char titleBuf[128] = {0}; char titleBuf[128] = {0};
m_file.read(reinterpret_cast<uint8_t*>(titleBuf), sizeof(titleBuf) - 1); m_file.read(titleBuf, sizeof(titleBuf) - 1);
m_title = titleBuf; m_title = titleBuf;
Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str()); Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str());
return XtcError::OK; 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() { XtcError XtcParser::readPageTable() {
if (m_header.pageTableOffset == 0) { if (m_header.pageTableOffset == 0) {
Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis()); Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis());

View File

@ -67,8 +67,9 @@ class XtcParser {
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback, std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
size_t chunkSize = 1024); size_t chunkSize = 1024);
// Get title from metadata // Get title/author from metadata
std::string getTitle() const { return m_title; } std::string getTitle() const { return m_title; }
std::string getAuthor() const { return m_author; }
bool hasChapters() const { return m_hasChapters; } bool hasChapters() const { return m_hasChapters; }
const std::vector<ChapterInfo>& getChapters() const { return m_chapters; } const std::vector<ChapterInfo>& getChapters() const { return m_chapters; }
@ -86,6 +87,7 @@ class XtcParser {
std::vector<PageInfo> m_pageTable; std::vector<PageInfo> m_pageTable;
std::vector<ChapterInfo> m_chapters; std::vector<ChapterInfo> m_chapters;
std::string m_title; std::string m_title;
std::string m_author;
uint16_t m_defaultWidth; uint16_t m_defaultWidth;
uint16_t m_defaultHeight; uint16_t m_defaultHeight;
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit) uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
@ -96,6 +98,7 @@ class XtcParser {
XtcError readHeader(); XtcError readHeader();
XtcError readPageTable(); XtcError readPageTable();
XtcError readTitle(); XtcError readTitle();
XtcError readAuthor();
XtcError readChapters(); XtcError readChapters();
}; };

View File

@ -38,14 +38,16 @@ struct XtcHeader {
uint8_t versionMajor; // 0x04: Format version major (typically 1) (together with minor = 1.0) uint8_t versionMajor; // 0x04: Format version major (typically 1) (together with minor = 1.0)
uint8_t versionMinor; // 0x05: Format version minor (typically 0) uint8_t versionMinor; // 0x05: Format version minor (typically 0)
uint16_t pageCount; // 0x06: Total page count uint16_t pageCount; // 0x06: Total page count
uint32_t flags; // 0x08: Flags/reserved uint8_t readDirection; // 0x08: Reading direction (0-2)
uint32_t headerSize; // 0x0C: Size of header section (typically 88) uint8_t hasMetadata; // 0x09: Has metadata (0-1)
uint32_t reserved1; // 0x10: Reserved uint8_t hasThumbnails; // 0x0A: Has thumbnails (0-1)
uint32_t tocOffset; // 0x14: TOC offset (0 if unused) - 4 bytes, not 8! uint8_t hasChapters; // 0x0B: Has chapters (0-1)
uint32_t currentPage; // 0x0C: Current page (1-based) (0-65535)
uint64_t metadataOffset; // 0x10: Metadata offset (0 if unused)
uint64_t pageTableOffset; // 0x18: Page table offset uint64_t pageTableOffset; // 0x18: Page table offset
uint64_t dataOffset; // 0x20: First page data offset uint64_t dataOffset; // 0x20: First page data offset
uint64_t reserved2; // 0x28: Reserved uint64_t thumbOffset; // 0x28: Thumbnail offset
uint32_t titleOffset; // 0x30: Title string offset uint32_t chapterOffset; // 0x30: Chapter data offset
uint32_t padding; // 0x34: Padding to 56 bytes uint32_t padding; // 0x34: Padding to 56 bytes
}; };
#pragma pack(pop) #pragma pack(pop)

View File

@ -71,6 +71,9 @@ void HomeActivity::onEnter() {
if (!xtc.getTitle().empty()) { if (!xtc.getTitle().empty()) {
lastBookTitle = std::string(xtc.getTitle()); lastBookTitle = std::string(xtc.getTitle());
} }
if (!xtc.getAuthor().empty()) {
lastBookAuthor = std::string(xtc.getAuthor());
}
// Try to generate thumbnail image for Continue Reading card // Try to generate thumbnail image for Continue Reading card
if (xtc.generateThumbBmp()) { if (xtc.generateThumbBmp()) {
coverBmpPath = xtc.getThumbBmpPath(); coverBmpPath = xtc.getThumbBmpPath();