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:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user