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 <FsHelpers.h>
#include <HardwareSerial.h>
#include <SDCardManager.h>
@ -87,6 +86,15 @@ std::string Xtc::getTitle() const {
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;

View File

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

View File

@ -47,8 +47,21 @@ XtcError XtcParser::open(const char* filepath) {
return m_lastError;
}
// Read title if available
readTitle();
// 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();
@ -124,24 +137,34 @@ XtcError XtcParser::readHeader() {
}
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)) {
constexpr auto titleOffset = 0x38;
if (!m_file.seek(titleOffset)) {
return XtcError::READ_ERROR;
}
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;
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());

View File

@ -67,8 +67,9 @@ class XtcParser {
std::function<void(const uint8_t* data, size_t size, size_t offset)> callback,
size_t chunkSize = 1024);
// Get title from metadata
// Get title/author from metadata
std::string getTitle() const { return m_title; }
std::string getAuthor() const { return m_author; }
bool hasChapters() const { return m_hasChapters; }
const std::vector<ChapterInfo>& getChapters() const { return m_chapters; }
@ -86,6 +87,7 @@ class XtcParser {
std::vector<PageInfo> m_pageTable;
std::vector<ChapterInfo> m_chapters;
std::string m_title;
std::string m_author;
uint16_t m_defaultWidth;
uint16_t m_defaultHeight;
uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit)
@ -96,6 +98,7 @@ class XtcParser {
XtcError readHeader();
XtcError readPageTable();
XtcError readTitle();
XtcError readAuthor();
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 versionMinor; // 0x05: Format version minor (typically 0)
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!
uint8_t readDirection; // 0x08: Reading direction (0-2)
uint8_t hasMetadata; // 0x09: Has metadata (0-1)
uint8_t hasThumbnails; // 0x0A: Has thumbnails (0-1)
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 dataOffset; // 0x20: First page data offset
uint64_t reserved2; // 0x28: Reserved
uint32_t titleOffset; // 0x30: Title string offset
uint64_t thumbOffset; // 0x28: Thumbnail offset
uint32_t chapterOffset; // 0x30: Chapter data offset
uint32_t padding; // 0x34: Padding to 56 bytes
};
#pragma pack(pop)

View File

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