Use cache files for TOC and spine
This commit is contained in:
parent
d23020e268
commit
a325f12656
@ -53,7 +53,7 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ContentOpfParser opfParser(getBasePath(), contentOpfSize);
|
||||
ContentOpfParser opfParser(getBasePath(), contentOpfSize, spineTocCache.get());
|
||||
|
||||
if (!opfParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
||||
@ -75,17 +75,11 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
||||
tocNcxItem = opfParser.tocNcxPath;
|
||||
}
|
||||
|
||||
for (auto& spineRef : opfParser.spineRefs) {
|
||||
if (opfParser.items.count(spineRef)) {
|
||||
spine.emplace_back(spineRef, opfParser.items.at(spineRef));
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNcxFile() {
|
||||
bool Epub::parseTocNcxFile() const {
|
||||
// the ncx file should have been specified in the content.opf file
|
||||
if (tocNcxItem.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
||||
@ -101,7 +95,7 @@ bool Epub::parseTocNcxFile() {
|
||||
tempNcxFile = SD.open(tmpNcxPath.c_str(), FILE_READ);
|
||||
const auto ncxSize = tempNcxFile.size();
|
||||
|
||||
TocNcxParser ncxParser(contentBasePath, ncxSize);
|
||||
TocNcxParser ncxParser(contentBasePath, ncxSize, spineTocCache.get());
|
||||
|
||||
if (!ncxParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||
@ -130,9 +124,7 @@ bool Epub::parseTocNcxFile() {
|
||||
tempNcxFile.close();
|
||||
SD.remove(tmpNcxPath.c_str());
|
||||
|
||||
this->toc = std::move(ncxParser.toc);
|
||||
|
||||
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
|
||||
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -140,6 +132,53 @@ bool Epub::parseTocNcxFile() {
|
||||
bool Epub::load() {
|
||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||
|
||||
// Initialize spine/TOC cache
|
||||
spineTocCache.reset(new SpineTocCache(cachePath));
|
||||
|
||||
// Try to load existing cache first
|
||||
if (spineTocCache->load()) {
|
||||
Serial.printf("[%lu] [EBP] Loaded spine/TOC from cache\n", millis());
|
||||
|
||||
// Still need to parse content.opf for title and cover
|
||||
// TODO: Should this data go in the cache?
|
||||
std::string contentOpfFilePath;
|
||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||
|
||||
// Parse content.opf but without cache (we already have it)
|
||||
size_t contentOpfSize;
|
||||
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||
Serial.printf("[%lu] [EBP] Could not get size of content.opf\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
ContentOpfParser opfParser(getBasePath(), contentOpfSize, nullptr);
|
||||
if (!opfParser.setup()) {
|
||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||
Serial.printf("[%lu] [EBP] Could not read content.opf\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
title = opfParser.title;
|
||||
if (!opfParser.coverItemId.empty() && opfParser.items.count(opfParser.coverItemId) > 0) {
|
||||
coverImageItem = opfParser.items.at(opfParser.coverItemId);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Cache doesn't exist or is invalid, build it
|
||||
Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis());
|
||||
|
||||
std::string contentOpfFilePath;
|
||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
||||
@ -150,6 +189,17 @@ bool Epub::load() {
|
||||
|
||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||
|
||||
// Ensure cache directory exists
|
||||
setupCacheDir();
|
||||
|
||||
Serial.printf("[%lu] [EBP] Cache path: %s\n", millis(), cachePath.c_str());
|
||||
|
||||
// Begin building cache - stream entries to disk immediately
|
||||
if (!spineTocCache->beginWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parseContentOpf(contentOpfFilePath)) {
|
||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
||||
return false;
|
||||
@ -160,30 +210,30 @@ bool Epub::load() {
|
||||
return false;
|
||||
}
|
||||
|
||||
initializeSpineItemSizes();
|
||||
// Close the cache files
|
||||
if (!spineTocCache->endWrite()) {
|
||||
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now compute mappings and sizes (this loads entries temporarily, computes, then rewrites)
|
||||
if (!spineTocCache->updateMappingsAndSizes(filepath)) {
|
||||
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reload the cache from disk so it's in the correct state
|
||||
spineTocCache.reset(new SpineTocCache(cachePath));
|
||||
if (!spineTocCache->load()) {
|
||||
Serial.printf("[%lu] [EBP] Failed to reload cache after writing\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::initializeSpineItemSizes() {
|
||||
Serial.printf("[%lu] [EBP] Calculating book size\n", millis());
|
||||
|
||||
const size_t spineItemsCount = getSpineItemsCount();
|
||||
size_t cumSpineItemSize = 0;
|
||||
const ZipFile zip("/sd" + filepath);
|
||||
|
||||
for (size_t i = 0; i < spineItemsCount; i++) {
|
||||
std::string spineItem = getSpineItem(i);
|
||||
size_t s = 0;
|
||||
getItemSize(zip, spineItem, &s);
|
||||
cumSpineItemSize += s;
|
||||
cumulativeSpineItemSize.emplace_back(cumSpineItemSize);
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Book size: %lu\n", millis(), cumSpineItemSize);
|
||||
}
|
||||
|
||||
bool Epub::clearCache() const {
|
||||
if (!SD.exists(cachePath.c_str())) {
|
||||
Serial.printf("[%lu] [EPB] Cache does not exist, no action needed\n", millis());
|
||||
@ -295,7 +345,7 @@ std::string normalisePath(const std::string& path) {
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, bool trailingNullByte) const {
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||
const ZipFile zip("/sd" + filepath);
|
||||
const std::string path = normalisePath(itemHref);
|
||||
|
||||
@ -325,99 +375,107 @@ bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t*
|
||||
return zip.getInflatedFileSize(path.c_str(), size);
|
||||
}
|
||||
|
||||
int Epub::getSpineItemsCount() const { return spine.size(); }
|
||||
int Epub::getSpineItemsCount() const {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
||||
return 0;
|
||||
}
|
||||
return spineTocCache->getSpineCount();
|
||||
}
|
||||
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
|
||||
if (spineIndex < 0 || spineIndex >= static_cast<int>(cumulativeSpineItemSize.size())) {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize called but cache not loaded\n", millis());
|
||||
return 0;
|
||||
}
|
||||
if (spineIndex < 0 || spineIndex >= spineTocCache->getSpineCount()) {
|
||||
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize index:%d is out of range\n", millis(), spineIndex);
|
||||
return 0;
|
||||
}
|
||||
return cumulativeSpineItemSize.at(spineIndex);
|
||||
return spineTocCache->getSpineEntry(spineIndex).cumulativeSize;
|
||||
}
|
||||
|
||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
||||
static std::string emptyString;
|
||||
if (spine.empty()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem called but spine is empty\n", millis());
|
||||
return emptyString;
|
||||
std::string Epub::getSpineHref(const int spineIndex) const {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem called but cache not loaded\n", millis());
|
||||
return "";
|
||||
}
|
||||
if (spineIndex < 0 || spineIndex >= static_cast<int>(spine.size())) {
|
||||
if (spineIndex < 0 || spineIndex >= spineTocCache->getSpineCount()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||
return spine.at(0).second;
|
||||
return spineTocCache->getSpineEntry(0).href;
|
||||
}
|
||||
|
||||
return spine.at(spineIndex).second;
|
||||
return spineTocCache->getSpineEntry(spineIndex).href;
|
||||
}
|
||||
|
||||
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
||||
static EpubTocEntry emptyEntry = {};
|
||||
if (toc.empty()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis());
|
||||
return emptyEntry;
|
||||
}
|
||||
if (tocTndex < 0 || tocTndex >= static_cast<int>(toc.size())) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocTndex);
|
||||
return toc.at(0);
|
||||
SpineTocCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
||||
return {};
|
||||
}
|
||||
|
||||
return toc.at(tocTndex);
|
||||
if (tocIndex < 0 || tocIndex >= spineTocCache->getTocCount()) {
|
||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
||||
return {};
|
||||
}
|
||||
|
||||
return spineTocCache->getTocEntry(tocIndex);
|
||||
}
|
||||
|
||||
int Epub::getTocItemsCount() const { return toc.size(); }
|
||||
int Epub::getTocItemsCount() const {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
||||
return 0;
|
||||
}
|
||||
return spineTocCache->getTocCount();
|
||||
}
|
||||
|
||||
// work out the section index for a toc index
|
||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||
if (tocIndex < 0 || tocIndex >= toc.size()) {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
||||
return 0;
|
||||
}
|
||||
if (tocIndex < 0 || tocIndex >= spineTocCache->getTocCount()) {
|
||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// the toc entry should have an href that matches the spine item
|
||||
// so we can find the spine index by looking for the href
|
||||
for (int i = 0; i < spine.size(); i++) {
|
||||
if (spine[i].second == toc[tocIndex].href) {
|
||||
return i;
|
||||
}
|
||||
const int spineIndex = spineTocCache->getTocEntry(tocIndex).spineIndex;
|
||||
if (spineIndex < 0) {
|
||||
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Section not found\n", millis());
|
||||
// not found - default to the start of the book
|
||||
return 0;
|
||||
return spineIndex;
|
||||
}
|
||||
|
||||
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] getTocIndexForSpineIndex called but cache not loaded\n", millis());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (spineIndex < 0 || spineIndex >= spineTocCache->getSpineCount()) {
|
||||
Serial.printf("[%lu] [EBP] getTocIndexForSpineIndex: spineIndex %d out of range\n", millis(), spineIndex);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// the toc entry should have an href that matches the spine item
|
||||
// so we can find the toc index by looking for the href
|
||||
for (int i = 0; i < toc.size(); i++) {
|
||||
if (toc[i].href == spine[spineIndex].second) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] TOC item not found\n", millis());
|
||||
return -1;
|
||||
return spineTocCache->getSpineEntry(spineIndex).tocIndex;
|
||||
}
|
||||
|
||||
size_t Epub::getBookSize() const {
|
||||
if (spine.empty()) {
|
||||
if (!spineTocCache || !spineTocCache->isLoaded() || spineTocCache->getSpineCount() == 0) {
|
||||
return 0;
|
||||
}
|
||||
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
||||
}
|
||||
|
||||
// Calculate progress in book
|
||||
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) {
|
||||
size_t bookSize = getBookSize();
|
||||
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||
const size_t bookSize = getBookSize();
|
||||
if (bookSize == 0) {
|
||||
return 0;
|
||||
}
|
||||
size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||
size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||
size_t sectionProgSize = currentSpineRead * curChapterSize;
|
||||
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||
const size_t sectionProgSize = currentSpineRead * curChapterSize;
|
||||
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
|
||||
}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/EpubTocEntry.h"
|
||||
#include "Epub/SpineTocCache.h"
|
||||
|
||||
class ZipFile;
|
||||
|
||||
@ -18,21 +19,16 @@ class Epub {
|
||||
std::string tocNcxItem;
|
||||
// where is the EPUBfile?
|
||||
std::string filepath;
|
||||
// the spine of the EPUB file
|
||||
std::vector<std::pair<std::string, std::string>> spine;
|
||||
// the file size of the spine items (proxy to book progress)
|
||||
std::vector<size_t> cumulativeSpineItemSize;
|
||||
// the toc of the EPUB file
|
||||
std::vector<EpubTocEntry> toc;
|
||||
// the base path for items in the EPUB file
|
||||
std::string contentBasePath;
|
||||
// Uniq cache key based on filepath
|
||||
std::string cachePath;
|
||||
// Spine and TOC cache
|
||||
std::unique_ptr<SpineTocCache> spineTocCache;
|
||||
|
||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||
bool parseContentOpf(const std::string& contentOpfFilePath);
|
||||
bool parseTocNcxFile();
|
||||
void initializeSpineItemSizes();
|
||||
bool parseTocNcxFile() const;
|
||||
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
|
||||
|
||||
public:
|
||||
@ -54,14 +50,14 @@ class Epub {
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
||||
std::string& getSpineItem(int spineIndex);
|
||||
std::string getSpineHref(int spineIndex) const;
|
||||
int getSpineItemsCount() const;
|
||||
size_t getCumulativeSpineItemSize(const int spineIndex) const;
|
||||
EpubTocEntry& getTocItem(int tocIndex);
|
||||
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
||||
SpineTocCache::TocEntry getTocItem(int tocIndex) const;
|
||||
int getTocItemsCount() const;
|
||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||
|
||||
size_t getBookSize() const;
|
||||
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead);
|
||||
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const;
|
||||
};
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
struct EpubTocEntry {
|
||||
std::string title;
|
||||
std::string href;
|
||||
std::string anchor;
|
||||
uint8_t level;
|
||||
};
|
||||
@ -117,7 +117,7 @@ bool Section::clearCache() const {
|
||||
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
||||
const int marginRight, const int marginBottom, const int marginLeft,
|
||||
const bool extraParagraphSpacing) {
|
||||
const auto localPath = epub->getSpineItem(spineIndex);
|
||||
const auto localPath = epub->getSpineHref(spineIndex);
|
||||
|
||||
// TODO: Should we get rid of this file all together?
|
||||
// It currently saves us a bit of memory by allowing for all the inflation bits to be released
|
||||
|
||||
388
lib/Epub/Epub/SpineTocCache.cpp
Normal file
388
lib/Epub/Epub/SpineTocCache.cpp
Normal file
@ -0,0 +1,388 @@
|
||||
#include "SpineTocCache.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SD.h>
|
||||
#include <Serialization.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SPINE_TOC_CACHE_VERSION = 1;
|
||||
|
||||
// TODO: Centralize this?
|
||||
std::string normalisePath(const std::string& path) {
|
||||
std::vector<std::string> components;
|
||||
std::string component;
|
||||
|
||||
for (const auto c : path) {
|
||||
if (c == '/') {
|
||||
if (!component.empty()) {
|
||||
if (component == "..") {
|
||||
if (!components.empty()) {
|
||||
components.pop_back();
|
||||
}
|
||||
} else {
|
||||
components.push_back(component);
|
||||
}
|
||||
component.clear();
|
||||
}
|
||||
} else {
|
||||
component += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.empty()) {
|
||||
components.push_back(component);
|
||||
}
|
||||
|
||||
std::string result;
|
||||
for (const auto& c : components) {
|
||||
if (!result.empty()) {
|
||||
result += "/";
|
||||
}
|
||||
result += c;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool SpineTocCache::beginWrite() {
|
||||
buildMode = true;
|
||||
spineCount = 0;
|
||||
tocCount = 0;
|
||||
|
||||
Serial.printf("[%lu] [STC] Beginning write to cache path: %s\n", millis(), cachePath.c_str());
|
||||
|
||||
// Open spine file for writing
|
||||
const std::string spineFilePath = cachePath + "/spine.bin";
|
||||
Serial.printf("[%lu] [STC] Opening spine file: %s\n", millis(), spineFilePath.c_str());
|
||||
spineFile = SD.open(spineFilePath.c_str(), FILE_WRITE, true);
|
||||
if (!spineFile) {
|
||||
Serial.printf("[%lu] [STC] Failed to open spine file for writing: %s\n", millis(), spineFilePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open TOC file for writing
|
||||
const std::string tocFilePath = cachePath + "/toc.bin";
|
||||
Serial.printf("[%lu] [STC] Opening toc file: %s\n", millis(), tocFilePath.c_str());
|
||||
tocFile = SD.open(tocFilePath.c_str(), FILE_WRITE, true);
|
||||
if (!tocFile) {
|
||||
Serial.printf("[%lu] [STC] Failed to open toc file for writing: %s\n", millis(), tocFilePath.c_str());
|
||||
spineFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [STC] Began writing cache files\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpineTocCache::writeString(File& file, const std::string& s) const {
|
||||
const auto len = static_cast<uint32_t>(s.size());
|
||||
file.write(reinterpret_cast<const uint8_t*>(&len), sizeof(len));
|
||||
file.write(reinterpret_cast<const uint8_t*>(s.data()), len);
|
||||
}
|
||||
|
||||
void SpineTocCache::writeSpineEntry(File& file, const SpineEntry& entry) const {
|
||||
writeString(file, entry.href);
|
||||
file.write(reinterpret_cast<const uint8_t*>(&entry.cumulativeSize), sizeof(entry.cumulativeSize));
|
||||
file.write(reinterpret_cast<const uint8_t*>(&entry.tocIndex), sizeof(entry.tocIndex));
|
||||
}
|
||||
|
||||
void SpineTocCache::writeTocEntry(File& file, const TocEntry& entry) const {
|
||||
writeString(file, entry.title);
|
||||
writeString(file, entry.href);
|
||||
writeString(file, entry.anchor);
|
||||
file.write(&entry.level, 1);
|
||||
file.write(reinterpret_cast<const uint8_t*>(&entry.spineIndex), sizeof(entry.spineIndex));
|
||||
}
|
||||
|
||||
void SpineTocCache::addSpineEntry(const std::string& href) {
|
||||
if (!buildMode || !spineFile) {
|
||||
Serial.printf("[%lu] [STC] addSpineEntry called but not in build mode\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
const SpineEntry entry(href, 0, -1);
|
||||
writeSpineEntry(spineFile, entry);
|
||||
spineCount++;
|
||||
}
|
||||
|
||||
void SpineTocCache::addTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||
const uint8_t level) {
|
||||
if (!buildMode || !tocFile) {
|
||||
Serial.printf("[%lu] [STC] addTocEntry called but not in build mode\n", millis());
|
||||
return;
|
||||
}
|
||||
|
||||
const TocEntry entry(title, href, anchor, level, -1);
|
||||
writeTocEntry(tocFile, entry);
|
||||
tocCount++;
|
||||
}
|
||||
|
||||
bool SpineTocCache::endWrite() {
|
||||
if (!buildMode) {
|
||||
Serial.printf("[%lu] [STC] endWrite called but not in build mode\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
|
||||
// Write metadata files with counts
|
||||
const auto spineMetaPath = cachePath + "/spine_meta.bin";
|
||||
File metaFile = SD.open(spineMetaPath.c_str(), FILE_WRITE, true);
|
||||
if (!metaFile) {
|
||||
Serial.printf("[%lu] [STC] Failed to write spine metadata\n", millis());
|
||||
return false;
|
||||
}
|
||||
metaFile.write(&SPINE_TOC_CACHE_VERSION, 1);
|
||||
metaFile.write(reinterpret_cast<const uint8_t*>(&spineCount), sizeof(spineCount));
|
||||
metaFile.write(reinterpret_cast<const uint8_t*>(&tocCount), sizeof(tocCount));
|
||||
metaFile.close();
|
||||
|
||||
buildMode = false;
|
||||
Serial.printf("[%lu] [STC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpineTocCache::readString(std::ifstream& is, std::string& s) const {
|
||||
uint32_t len;
|
||||
is.read(reinterpret_cast<char*>(&len), sizeof(len));
|
||||
s.resize(len);
|
||||
is.read(&s[0], len);
|
||||
}
|
||||
|
||||
SpineTocCache::SpineEntry SpineTocCache::readSpineEntry(std::ifstream& is) const {
|
||||
SpineEntry entry;
|
||||
readString(is, entry.href);
|
||||
is.read(reinterpret_cast<char*>(&entry.cumulativeSize), sizeof(entry.cumulativeSize));
|
||||
is.read(reinterpret_cast<char*>(&entry.tocIndex), sizeof(entry.tocIndex));
|
||||
return entry;
|
||||
}
|
||||
|
||||
SpineTocCache::TocEntry SpineTocCache::readTocEntry(std::ifstream& is) const {
|
||||
TocEntry entry;
|
||||
readString(is, entry.title);
|
||||
readString(is, entry.href);
|
||||
readString(is, entry.anchor);
|
||||
is.read(reinterpret_cast<char*>(&entry.level), 1);
|
||||
is.read(reinterpret_cast<char*>(&entry.spineIndex), sizeof(entry.spineIndex));
|
||||
return entry;
|
||||
}
|
||||
|
||||
bool SpineTocCache::updateMappingsAndSizes(const std::string& epubPath) const {
|
||||
Serial.printf("[%lu] [STC] Computing mappings and sizes for %d spine, %d TOC entries\n", millis(), spineCount,
|
||||
tocCount);
|
||||
|
||||
// Read all spine and TOC entries into temporary arrays (we need them all to compute mappings)
|
||||
// TODO: can we do this a bit smarter and avoid loading everything?
|
||||
std::vector<SpineEntry> spineEntries;
|
||||
std::vector<TocEntry> tocEntries;
|
||||
|
||||
spineEntries.reserve(spineCount);
|
||||
tocEntries.reserve(tocCount);
|
||||
|
||||
// Read spine entries
|
||||
{
|
||||
const auto spineFilePath = "/sd" + cachePath + "/spine.bin";
|
||||
std::ifstream spineStream(spineFilePath.c_str(), std::ios::binary);
|
||||
if (!spineStream) {
|
||||
Serial.printf("[%lu] [STC] Failed to open spine file for reading\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
spineEntries.push_back(readSpineEntry(spineStream));
|
||||
}
|
||||
spineStream.close();
|
||||
}
|
||||
|
||||
// Read TOC entries
|
||||
{
|
||||
const auto tocFilePath = "/sd" + cachePath + "/toc.bin";
|
||||
std::ifstream tocStream(tocFilePath.c_str(), std::ios::binary);
|
||||
if (!tocStream) {
|
||||
Serial.printf("[%lu] [STC] Failed to open toc file for reading\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
tocEntries.push_back(readTocEntry(tocStream));
|
||||
}
|
||||
tocStream.close();
|
||||
}
|
||||
|
||||
// Compute cumulative sizes
|
||||
const ZipFile zip("/sd" + epubPath);
|
||||
size_t cumSize = 0;
|
||||
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
size_t itemSize = 0;
|
||||
const std::string path = normalisePath(spineEntries[i].href);
|
||||
if (zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
cumSize += itemSize;
|
||||
spineEntries[i].cumulativeSize = cumSize;
|
||||
} else {
|
||||
Serial.printf("[%lu] [STC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [STC] Book size: %lu\n", millis(), cumSize);
|
||||
|
||||
// Compute spine → TOC mappings
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
if (tocEntries[j].href == spineEntries[i].href) {
|
||||
spineEntries[i].tocIndex = static_cast<int16_t>(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compute TOC → spine mappings
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
for (int j = 0; j < spineCount; j++) {
|
||||
if (spineEntries[j].href == tocEntries[i].href) {
|
||||
tocEntries[i].spineIndex = static_cast<int16_t>(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite spine file with updated data
|
||||
{
|
||||
const auto spineFilePath = cachePath + "/spine.bin";
|
||||
File spineFile = SD.open(spineFilePath.c_str(), FILE_WRITE, true);
|
||||
if (!spineFile) {
|
||||
Serial.printf("[%lu] [STC] Failed to reopen spine file for writing\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : spineEntries) {
|
||||
writeSpineEntry(spineFile, entry);
|
||||
}
|
||||
spineFile.close();
|
||||
}
|
||||
|
||||
// Rewrite TOC file with updated data
|
||||
{
|
||||
const auto tocFilePath = cachePath + "/toc.bin";
|
||||
File tocFile = SD.open(tocFilePath.c_str(), FILE_WRITE, true);
|
||||
if (!tocFile) {
|
||||
Serial.printf("[%lu] [STC] Failed to reopen toc file for writing\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const auto& entry : tocEntries) {
|
||||
writeTocEntry(tocFile, entry);
|
||||
}
|
||||
tocFile.close();
|
||||
}
|
||||
|
||||
// Clear vectors to free memory
|
||||
spineEntries.clear();
|
||||
spineEntries.shrink_to_fit();
|
||||
tocEntries.clear();
|
||||
tocEntries.shrink_to_fit();
|
||||
|
||||
Serial.printf("[%lu] [STC] Updated cache with mappings and sizes\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SpineTocCache::load() {
|
||||
// Load metadata
|
||||
const auto metaPath = cachePath + "/spine_meta.bin";
|
||||
if (!SD.exists(metaPath.c_str())) {
|
||||
Serial.printf("[%lu] [STC] Cache metadata does not exist: %s\n", millis(), metaPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
File metaFile = SD.open(metaPath.c_str(), FILE_READ);
|
||||
if (!metaFile) {
|
||||
Serial.printf("[%lu] [STC] Failed to open cache metadata\n", millis());
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
metaFile.read(&version, 1);
|
||||
if (version != SPINE_TOC_CACHE_VERSION) {
|
||||
Serial.printf("[%lu] [STC] Cache version mismatch: expected %d, got %d\n", millis(), SPINE_TOC_CACHE_VERSION,
|
||||
version);
|
||||
metaFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
metaFile.read(reinterpret_cast<uint8_t*>(&spineCount), sizeof(spineCount));
|
||||
metaFile.read(reinterpret_cast<uint8_t*>(&tocCount), sizeof(tocCount));
|
||||
metaFile.close();
|
||||
|
||||
loaded = true;
|
||||
Serial.printf("[%lu] [STC] Loaded cache metadata: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
SpineTocCache::SpineEntry SpineTocCache::getSpineEntry(const int index) const {
|
||||
if (!loaded) {
|
||||
Serial.printf("[%lu] [STC] getSpineEntry called but cache not loaded\n", millis());
|
||||
return SpineEntry();
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||
Serial.printf("[%lu] [STC] getSpineEntry index %d out of range\n", millis(), index);
|
||||
return SpineEntry();
|
||||
}
|
||||
|
||||
const auto spineFilePath = "/sd" + cachePath + "/spine.bin";
|
||||
std::ifstream spineStream(spineFilePath.c_str(), std::ios::binary);
|
||||
if (!spineStream) {
|
||||
Serial.printf("[%lu] [STC] Failed to open spine file for reading entry\n", millis());
|
||||
return SpineEntry();
|
||||
}
|
||||
|
||||
// Seek to the correct entry - need to read entries sequentially until we reach the index
|
||||
// TODO: This could/should be based on a look up table/fixed sizes
|
||||
for (int i = 0; i < index; i++) {
|
||||
readSpineEntry(spineStream); // Skip entries
|
||||
}
|
||||
|
||||
auto entry = readSpineEntry(spineStream);
|
||||
spineStream.close();
|
||||
return entry;
|
||||
}
|
||||
|
||||
SpineTocCache::TocEntry SpineTocCache::getTocEntry(const int index) const {
|
||||
if (!loaded) {
|
||||
Serial.printf("[%lu] [STC] getTocEntry called but cache not loaded\n", millis());
|
||||
return TocEntry();
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||
Serial.printf("[%lu] [STC] getTocEntry index %d out of range\n", millis(), index);
|
||||
return TocEntry();
|
||||
}
|
||||
|
||||
const auto tocFilePath = "/sd" + cachePath + "/toc.bin";
|
||||
std::ifstream tocStream(tocFilePath.c_str(), std::ios::binary);
|
||||
if (!tocStream) {
|
||||
Serial.printf("[%lu] [STC] Failed to open toc file for reading entry\n", millis());
|
||||
return TocEntry();
|
||||
}
|
||||
|
||||
// Seek to the correct entry - need to read entries sequentially until we reach the index
|
||||
// TODO: This could/should be based on a look up table/fixed sizes
|
||||
for (int i = 0; i < index; i++) {
|
||||
readTocEntry(tocStream); // Skip entries
|
||||
}
|
||||
|
||||
auto entry = readTocEntry(tocStream);
|
||||
tocStream.close();
|
||||
return entry;
|
||||
}
|
||||
|
||||
int SpineTocCache::getSpineCount() const { return spineCount; }
|
||||
|
||||
int SpineTocCache::getTocCount() const { return tocCount; }
|
||||
|
||||
bool SpineTocCache::isLoaded() const { return loaded; }
|
||||
75
lib/Epub/Epub/SpineTocCache.h
Normal file
75
lib/Epub/Epub/SpineTocCache.h
Normal file
@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include <SD.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
class SpineTocCache {
|
||||
public:
|
||||
struct SpineEntry {
|
||||
std::string href;
|
||||
size_t cumulativeSize;
|
||||
int16_t tocIndex;
|
||||
|
||||
SpineEntry() : cumulativeSize(0), tocIndex(-1) {}
|
||||
SpineEntry(std::string href, size_t cumulativeSize, int16_t tocIndex)
|
||||
: href(std::move(href)), cumulativeSize(cumulativeSize), tocIndex(tocIndex) {}
|
||||
};
|
||||
|
||||
struct TocEntry {
|
||||
std::string title;
|
||||
std::string href;
|
||||
std::string anchor;
|
||||
uint8_t level;
|
||||
int16_t spineIndex;
|
||||
|
||||
TocEntry() : level(0), spineIndex(-1) {}
|
||||
TocEntry(std::string title, std::string href, std::string anchor, uint8_t level, int16_t spineIndex)
|
||||
: title(std::move(title)),
|
||||
href(std::move(href)),
|
||||
anchor(std::move(anchor)),
|
||||
level(level),
|
||||
spineIndex(spineIndex) {}
|
||||
};
|
||||
|
||||
private:
|
||||
std::string cachePath;
|
||||
uint16_t spineCount;
|
||||
uint16_t tocCount;
|
||||
bool loaded;
|
||||
bool buildMode;
|
||||
|
||||
// Temp file handles during build
|
||||
File spineFile;
|
||||
File tocFile;
|
||||
|
||||
void writeString(File& file, const std::string& s) const;
|
||||
void readString(std::ifstream& is, std::string& s) const;
|
||||
void writeSpineEntry(File& file, const SpineEntry& entry) const;
|
||||
void writeTocEntry(File& file, const TocEntry& entry) const;
|
||||
SpineEntry readSpineEntry(std::ifstream& is) const;
|
||||
TocEntry readTocEntry(std::ifstream& is) const;
|
||||
|
||||
public:
|
||||
explicit SpineTocCache(std::string cachePath)
|
||||
: cachePath(std::move(cachePath)), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
||||
~SpineTocCache() = default;
|
||||
|
||||
// Building phase (stream to disk immediately)
|
||||
bool beginWrite();
|
||||
void addSpineEntry(const std::string& href);
|
||||
void addTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
|
||||
bool endWrite();
|
||||
|
||||
// Post-processing to update mappings and sizes
|
||||
bool updateMappingsAndSizes(const std::string& epubPath) const;
|
||||
|
||||
// Reading phase (read mode)
|
||||
bool load();
|
||||
SpineEntry getSpineEntry(int index) const;
|
||||
TocEntry getTocEntry(int index) const;
|
||||
int getSpineCount() const;
|
||||
int getTocCount() const;
|
||||
bool isLoaded() const;
|
||||
};
|
||||
@ -148,10 +148,18 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: This relies on spine appearing after item manifest
|
||||
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "idref") == 0) {
|
||||
self->spineRefs.emplace_back(atts[i + 1]);
|
||||
const std::string idref = atts[i + 1];
|
||||
// Resolve the idref to href using items map
|
||||
if (self->items.count(idref) > 0) {
|
||||
const std::string& href = self->items.at(idref);
|
||||
if (self->cache) {
|
||||
self->cache->addSpineEntry(href);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
#include <map>
|
||||
|
||||
#include "Epub.h"
|
||||
#include "Epub/SpineTocCache.h"
|
||||
#include "expat.h"
|
||||
|
||||
class ContentOpfParser final : public Print {
|
||||
@ -20,6 +21,7 @@ class ContentOpfParser final : public Print {
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
SpineTocCache* cache;
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
@ -30,10 +32,9 @@ class ContentOpfParser final : public Print {
|
||||
std::string tocNcxPath;
|
||||
std::string coverItemId;
|
||||
std::map<std::string, std::string> items;
|
||||
std::vector<std::string> spineRefs;
|
||||
|
||||
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
||||
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize, SpineTocCache* cache)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||
~ContentOpfParser() override;
|
||||
|
||||
bool setup();
|
||||
|
||||
@ -167,8 +167,9 @@ void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
|
||||
href = href.substr(0, pos);
|
||||
}
|
||||
|
||||
// Push to vector
|
||||
self->toc.push_back({std::move(self->currentLabel), std::move(href), std::move(anchor), self->currentDepth});
|
||||
if (self->cache) {
|
||||
self->cache->addTocEntry(self->currentLabel, href, anchor, self->currentDepth);
|
||||
}
|
||||
|
||||
// Clear them so we don't re-add them if there are weird XML structures
|
||||
self->currentLabel.clear();
|
||||
|
||||
@ -2,9 +2,8 @@
|
||||
#include <Print.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/EpubTocEntry.h"
|
||||
#include "Epub/SpineTocCache.h"
|
||||
#include "expat.h"
|
||||
|
||||
class TocNcxParser final : public Print {
|
||||
@ -14,6 +13,7 @@ class TocNcxParser final : public Print {
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
SpineTocCache* cache;
|
||||
|
||||
std::string currentLabel;
|
||||
std::string currentSrc;
|
||||
@ -24,10 +24,8 @@ class TocNcxParser final : public Print {
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
std::vector<EpubTocEntry> toc;
|
||||
|
||||
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
||||
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize, SpineTocCache* cache)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||
~TocNcxParser() override;
|
||||
|
||||
bool setup();
|
||||
|
||||
@ -212,7 +212,7 @@ void EpubReaderActivity::renderScreen() {
|
||||
}
|
||||
|
||||
if (!section) {
|
||||
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
||||
const auto filepath = epub->getSpineHref(currentSpineIndex);
|
||||
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), currentSpineIndex);
|
||||
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
||||
|
||||
@ -29,7 +29,7 @@ void EpubReaderChapterSelectionActivity::onEnter() {
|
||||
// Trigger first update
|
||||
updateRequired = true;
|
||||
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
|
||||
2048, // Stack size
|
||||
4096, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user