Add chapter select support to XTC files (#145)

## Summary

- **What is the goal of this PR?** Add chapter selection support to the
XTC reader activity, including parsing chapter metadata from XTC files.
- **What changes are included?** Implemented XTC chapter parsing and
exposure in the XTC library, added a chapter selection activity for XTC,
integrated it into XtcReaderActivity, and normalized chapter page
indices by shifting them to 0-based.

  ## Additional Context

- The reader uses 0-based page indexing (first page = 0), but the XTC
chapter table appears to be 1-based (first page = 1), so chapter
start/end pages are shifted down by 1 during parsing.
This commit is contained in:
Sam Davis
2025-12-30 12:49:18 +11:00
committed by GitHub
parent b01eb50325
commit 278b056bd0
9 changed files with 376 additions and 5 deletions

View File

@@ -87,6 +87,21 @@ std::string Xtc::getTitle() const {
return filepath.substr(lastSlash, lastDot - lastSlash);
}
bool Xtc::hasChapters() const {
if (!loaded || !parser) {
return false;
}
return parser->hasChapters();
}
const std::vector<xtc::ChapterInfo>& Xtc::getChapters() const {
static const std::vector<xtc::ChapterInfo> kEmpty;
if (!loaded || !parser) {
return kEmpty;
}
return parser->getChapters();
}
std::string Xtc::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
bool Xtc::generateCoverBmp() const {

View File

@@ -9,6 +9,7 @@
#include <memory>
#include <string>
#include <vector>
#include "Xtc/XtcParser.h"
#include "Xtc/XtcTypes.h"
@@ -55,6 +56,8 @@ class Xtc {
// Metadata
std::string getTitle() const;
bool hasChapters() const;
const std::vector<xtc::ChapterInfo>& getChapters() const;
// Cover image support (for sleep screen)
std::string getCoverBmpPath() const;

View File

@@ -19,6 +19,7 @@ XtcParser::XtcParser()
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));
}
@@ -56,6 +57,14 @@ XtcError XtcParser::open(const char* filepath) {
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);
@@ -68,7 +77,9 @@ void XtcParser::close() {
m_isOpen = false;
}
m_pageTable.clear();
m_chapters.clear();
m_title.clear();
m_hasChapters = false;
memset(&m_header, 0, sizeof(m_header));
}
@@ -165,6 +176,112 @@ XtcError XtcParser::readPageTable() {
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;

View File

@@ -70,6 +70,9 @@ class XtcParser {
// Get title from metadata
std::string getTitle() const { return m_title; }
bool hasChapters() const { return m_hasChapters; }
const std::vector<ChapterInfo>& getChapters() const { return m_chapters; }
// Validation
static bool isValidXtcFile(const char* filepath);
@@ -81,16 +84,19 @@ class XtcParser {
bool m_isOpen;
XtcHeader m_header;
std::vector<PageInfo> m_pageTable;
std::vector<ChapterInfo> m_chapters;
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)
bool m_hasChapters;
XtcError m_lastError;
// Internal helper functions
XtcError readHeader();
XtcError readPageTable();
XtcError readTitle();
XtcError readChapters();
};
} // namespace xtc

View File

@@ -13,6 +13,7 @@
#pragma once
#include <cstdint>
#include <string>
namespace xtc {
@@ -92,6 +93,12 @@ struct PageInfo {
uint8_t padding; // Alignment padding
}; // 16 bytes total
struct ChapterInfo {
std::string name;
uint16_t startPage;
uint16_t endPage;
};
// Error codes
enum class XtcError {
OK = 0,