New book.bin spine and table of contents cache (#104)
## Summary * Use single unified cache file for book spine, table of contents, and core metadata (title, author, cover image) * Use new temp item store file in OPF parsing to store items to be rescaned when parsing spine * This avoids us holding these items in memory * Use new toc.bin.tmp and spine.bin.tmp to build out partial toc / spine data as part of parsing content.opf and the NCX file * These files are re-read multiple times to ultimately build book.bin ## Additional Context * Spec for file format included below as an image * This should help with: * #10 * #60 * #99
This commit is contained in:
@@ -6,8 +6,6 @@
|
|||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#include "Epub/parsers/ContainerParser.h"
|
#include "Epub/parsers/ContainerParser.h"
|
||||||
#include "Epub/parsers/ContentOpfParser.h"
|
#include "Epub/parsers/ContentOpfParser.h"
|
||||||
#include "Epub/parsers/TocNcxParser.h"
|
#include "Epub/parsers/TocNcxParser.h"
|
||||||
@@ -44,7 +42,15 @@ bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||||
|
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);
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
Serial.printf("[%lu] [EBP] Parsing content.opf: %s\n", millis(), contentOpfFilePath.c_str());
|
||||||
|
|
||||||
size_t contentOpfSize;
|
size_t contentOpfSize;
|
||||||
@@ -53,7 +59,9 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentOpfParser opfParser(getBasePath(), contentOpfSize);
|
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, bookMetadataCache.get());
|
||||||
|
Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(),
|
||||||
|
ESP.getHeapSize(), ESP.getMinFreeHeap());
|
||||||
|
|
||||||
if (!opfParser.setup()) {
|
if (!opfParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
Serial.printf("[%lu] [EBP] Could not setup content.opf parser\n", millis());
|
||||||
@@ -66,26 +74,20 @@ bool Epub::parseContentOpf(const std::string& contentOpfFilePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grab data from opfParser into epub
|
// Grab data from opfParser into epub
|
||||||
title = opfParser.title;
|
bookMetadata.title = opfParser.title;
|
||||||
if (!opfParser.coverItemId.empty() && opfParser.items.count(opfParser.coverItemId) > 0) {
|
// TODO: Parse author
|
||||||
coverImageItem = opfParser.items.at(opfParser.coverItemId);
|
bookMetadata.author = "";
|
||||||
}
|
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||||
|
|
||||||
if (!opfParser.tocNcxPath.empty()) {
|
if (!opfParser.tocNcxPath.empty()) {
|
||||||
tocNcxItem = opfParser.tocNcxPath;
|
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());
|
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::parseTocNcxFile() {
|
bool Epub::parseTocNcxFile() const {
|
||||||
// the ncx file should have been specified in the content.opf file
|
// the ncx file should have been specified in the content.opf file
|
||||||
if (tocNcxItem.empty()) {
|
if (tocNcxItem.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
Serial.printf("[%lu] [EBP] No ncx file specified\n", millis());
|
||||||
@@ -106,7 +108,7 @@ bool Epub::parseTocNcxFile() {
|
|||||||
}
|
}
|
||||||
const auto ncxSize = tempNcxFile.size();
|
const auto ncxSize = tempNcxFile.size();
|
||||||
|
|
||||||
TocNcxParser ncxParser(contentBasePath, ncxSize);
|
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||||
|
|
||||||
if (!ncxParser.setup()) {
|
if (!ncxParser.setup()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
Serial.printf("[%lu] [EBP] Could not setup toc ncx parser\n", millis());
|
||||||
@@ -135,9 +137,7 @@ bool Epub::parseTocNcxFile() {
|
|||||||
tempNcxFile.close();
|
tempNcxFile.close();
|
||||||
SD.remove(tmpNcxPath.c_str());
|
SD.remove(tmpNcxPath.c_str());
|
||||||
|
|
||||||
this->toc = std::move(ncxParser.toc);
|
Serial.printf("[%lu] [EBP] Parsed TOC items\n", millis());
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Parsed %d TOC items\n", millis(), this->toc.size());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,48 +145,79 @@ bool Epub::parseTocNcxFile() {
|
|||||||
bool Epub::load() {
|
bool Epub::load() {
|
||||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||||
|
|
||||||
std::string contentOpfFilePath;
|
// Initialize spine/TOC cache
|
||||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
Serial.printf("[%lu] [EBP] Could not find content.opf in zip\n", millis());
|
|
||||||
|
// Try to load existing cache first
|
||||||
|
if (bookMetadataCache->load()) {
|
||||||
|
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());
|
||||||
|
setupCacheDir();
|
||||||
|
|
||||||
|
// Begin building cache - stream entries to disk immediately
|
||||||
|
if (!bookMetadataCache->beginWrite()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Found content.opf at: %s\n", millis(), contentOpfFilePath.c_str());
|
// OPF Pass
|
||||||
|
BookMetadataCache::BookMetadata bookMetadata;
|
||||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis());
|
||||||
if (!parseContentOpf(contentOpfFilePath)) {
|
return false;
|
||||||
|
}
|
||||||
|
if (!parseContentOpf(bookMetadata)) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
Serial.printf("[%lu] [EBP] Could not parse content.opf\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!bookMetadataCache->endContentOpfPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOC Pass
|
||||||
|
if (!bookMetadataCache->beginTocPass()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!parseTocNcxFile()) {
|
if (!parseTocNcxFile()) {
|
||||||
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
|
Serial.printf("[%lu] [EBP] Could not parse toc\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!bookMetadataCache->endTocPass()) {
|
||||||
initializeSpineItemSizes();
|
Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis());
|
||||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
return false;
|
||||||
|
|
||||||
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);
|
// Close the cache files
|
||||||
|
if (!bookMetadataCache->endWrite()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not end writing cache\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build final book.bin
|
||||||
|
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the cache from disk so it's in the correct state
|
||||||
|
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||||
|
if (!bookMetadataCache->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;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Epub::clearCache() const {
|
bool Epub::clearCache() const {
|
||||||
@@ -222,7 +253,14 @@ const std::string& Epub::getCachePath() const { return cachePath; }
|
|||||||
|
|
||||||
const std::string& Epub::getPath() const { return filepath; }
|
const std::string& Epub::getPath() const { return filepath; }
|
||||||
|
|
||||||
const std::string& Epub::getTitle() const { return title; }
|
const std::string& Epub::getTitle() const {
|
||||||
|
static std::string blank;
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return blank;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->coreMetadata.title;
|
||||||
|
}
|
||||||
|
|
||||||
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||||
|
|
||||||
@@ -232,13 +270,19 @@ bool Epub::generateCoverBmp() const {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImageItem.empty()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] Cannot generate cover BMP, cache not loaded\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||||
|
if (coverImageHref.empty()) {
|
||||||
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
Serial.printf("[%lu] [EBP] No known cover image\n", millis());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coverImageItem.substr(coverImageItem.length() - 4) == ".jpg" ||
|
if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" ||
|
||||||
coverImageItem.substr(coverImageItem.length() - 5) == ".jpeg") {
|
coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") {
|
||||||
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
|
Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis());
|
||||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||||
|
|
||||||
@@ -246,7 +290,7 @@ bool Epub::generateCoverBmp() const {
|
|||||||
if (!FsHelpers::openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
if (!FsHelpers::openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
readItemContentsToStream(coverImageItem, coverJpg, 1024);
|
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||||
coverJpg.close();
|
coverJpg.close();
|
||||||
|
|
||||||
if (!FsHelpers::openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
if (!FsHelpers::openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||||
@@ -276,7 +320,7 @@ bool Epub::generateCoverBmp() const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 ZipFile zip("/sd" + filepath);
|
||||||
const std::string path = FsHelpers::normalisePath(itemHref);
|
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||||
|
|
||||||
@@ -306,99 +350,89 @@ bool Epub::getItemSize(const ZipFile& zip, const std::string& itemHref, size_t*
|
|||||||
return zip.getInflatedFileSize(path.c_str(), size);
|
return zip.getInflatedFileSize(path.c_str(), size);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getSpineItemsCount() const { return spine.size(); }
|
int Epub::getSpineItemsCount() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
|
|
||||||
if (spineIndex < 0 || spineIndex >= static_cast<int>(cumulativeSpineItemSize.size())) {
|
|
||||||
Serial.printf("[%lu] [EBP] getCumulativeSpineItemSize index:%d is out of range\n", millis(), spineIndex);
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return cumulativeSpineItemSize.at(spineIndex);
|
return bookMetadataCache->getSpineCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
|
||||||
static std::string emptyString;
|
|
||||||
if (spine.empty()) {
|
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem called but spine is empty\n", millis());
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
return emptyString;
|
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 >= bookMetadataCache->getSpineCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
Serial.printf("[%lu] [EBP] getSpineItem index:%d is out of range\n", millis(), spineIndex);
|
||||||
return spine.at(0).second;
|
return bookMetadataCache->getSpineEntry(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return spine.at(spineIndex).second;
|
return bookMetadataCache->getSpineEntry(spineIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpubTocEntry& Epub::getTocItem(const int tocIndex) {
|
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||||
static EpubTocEntry emptyEntry = {};
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
if (toc.empty()) {
|
Serial.printf("[%lu] [EBP] getTocItem called but cache not loaded\n", millis());
|
||||||
Serial.printf("[%lu] [EBP] getTocItem called but toc is empty\n", millis());
|
return {};
|
||||||
return emptyEntry;
|
|
||||||
}
|
}
|
||||||
if (tocIndex < 0 || tocIndex >= static_cast<int>(toc.size())) {
|
|
||||||
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
Serial.printf("[%lu] [EBP] getTocItem index:%d is out of range\n", millis(), tocIndex);
|
||||||
return toc.at(0);
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return toc.at(tocIndex);
|
return bookMetadataCache->getTocEntry(tocIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getTocItemsCount() const { return toc.size(); }
|
int Epub::getTocItemsCount() const {
|
||||||
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bookMetadataCache->getTocCount();
|
||||||
|
}
|
||||||
|
|
||||||
// work out the section index for a toc index
|
// work out the section index for a toc index
|
||||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||||
if (tocIndex < 0 || tocIndex >= toc.size()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||||
|
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex called but cache not loaded\n", millis());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||||
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
Serial.printf("[%lu] [EBP] getSpineIndexForTocIndex: tocIndex %d out of range\n", millis(), tocIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// the toc entry should have an href that matches the spine item
|
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
||||||
// so we can find the spine index by looking for the href
|
if (spineIndex < 0) {
|
||||||
for (int i = 0; i < spine.size(); i++) {
|
Serial.printf("[%lu] [EBP] Section not found for TOC index %d\n", millis(), tocIndex);
|
||||||
if (spine[i].second == toc[tocIndex].href) {
|
return 0;
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.printf("[%lu] [EBP] Section not found\n", millis());
|
return spineIndex;
|
||||||
// not found - default to the start of the book
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; }
|
||||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t Epub::getBookSize() const {
|
size_t Epub::getBookSize() const {
|
||||||
if (spine.empty()) {
|
if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate progress in book
|
// Calculate progress in book
|
||||||
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) {
|
uint8_t Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||||
size_t bookSize = getBookSize();
|
const size_t bookSize = getBookSize();
|
||||||
if (bookSize == 0) {
|
if (bookSize == 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||||
size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||||
size_t sectionProgSize = currentSpineRead * curChapterSize;
|
const size_t sectionProgSize = currentSpineRead * curChapterSize;
|
||||||
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
|
return round(static_cast<float>(prevChapterSize + sectionProgSize) / bookSize * 100.0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,29 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Print.h>
|
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Epub/EpubTocEntry.h"
|
#include "Epub/BookMetadataCache.h"
|
||||||
|
|
||||||
class ZipFile;
|
class ZipFile;
|
||||||
|
|
||||||
class Epub {
|
class Epub {
|
||||||
// the title read from the EPUB meta data
|
|
||||||
std::string title;
|
|
||||||
// the cover image
|
|
||||||
std::string coverImageItem;
|
|
||||||
// the ncx file
|
// the ncx file
|
||||||
std::string tocNcxItem;
|
std::string tocNcxItem;
|
||||||
// where is the EPUBfile?
|
// where is the EPUBfile?
|
||||||
std::string filepath;
|
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
|
// the base path for items in the EPUB file
|
||||||
std::string contentBasePath;
|
std::string contentBasePath;
|
||||||
// Uniq cache key based on filepath
|
// Uniq cache key based on filepath
|
||||||
std::string cachePath;
|
std::string cachePath;
|
||||||
|
// Spine and TOC cache
|
||||||
|
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
||||||
|
|
||||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||||
bool parseContentOpf(const std::string& contentOpfFilePath);
|
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||||
bool parseTocNcxFile();
|
bool parseTocNcxFile() const;
|
||||||
void initializeSpineItemSizes();
|
|
||||||
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
|
static bool getItemSize(const ZipFile& zip, const std::string& itemHref, size_t* size);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -54,14 +45,14 @@ class Epub {
|
|||||||
bool trailingNullByte = false) const;
|
bool trailingNullByte = false) const;
|
||||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||||
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
||||||
std::string& getSpineItem(int spineIndex);
|
BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const;
|
||||||
|
BookMetadataCache::TocEntry getTocItem(int tocIndex) const;
|
||||||
int getSpineItemsCount() const;
|
int getSpineItemsCount() const;
|
||||||
size_t getCumulativeSpineItemSize(const int spineIndex) const;
|
|
||||||
EpubTocEntry& getTocItem(int tocIndex);
|
|
||||||
int getTocItemsCount() const;
|
int getTocItemsCount() const;
|
||||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||||
|
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
||||||
|
|
||||||
size_t getBookSize() const;
|
size_t getBookSize() const;
|
||||||
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead);
|
uint8_t calculateProgress(const int currentSpineIndex, const float currentSpineRead) const;
|
||||||
};
|
};
|
||||||
|
|||||||
326
lib/Epub/Epub/BookMetadataCache.cpp
Normal file
326
lib/Epub/Epub/BookMetadataCache.cpp
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
#include "BookMetadataCache.h"
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <SD.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
#include <ZipFile.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint8_t BOOK_CACHE_VERSION = 1;
|
||||||
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginWrite() {
|
||||||
|
buildMode = true;
|
||||||
|
spineCount = 0;
|
||||||
|
tocCount = 0;
|
||||||
|
Serial.printf("[%lu] [BMC] Entering write mode\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginContentOpfPass() {
|
||||||
|
Serial.printf("[%lu] [BMC] Beginning content opf pass\n", millis());
|
||||||
|
|
||||||
|
// Open spine file for writing
|
||||||
|
return FsHelpers::openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endContentOpfPass() {
|
||||||
|
spineFile.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::beginTocPass() {
|
||||||
|
Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis());
|
||||||
|
|
||||||
|
// Open spine file for reading
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!FsHelpers::openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endTocPass() {
|
||||||
|
tocFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::endWrite() {
|
||||||
|
if (!buildMode) {
|
||||||
|
Serial.printf("[%lu] [BMC] endWrite called but not in build mode\n", millis());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildMode = false;
|
||||||
|
Serial.printf("[%lu] [BMC] Wrote %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||||
|
// Open all three files, writing to meta, reading from spine and toc
|
||||||
|
if (!FsHelpers::openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr size_t headerASize =
|
||||||
|
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(size_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||||
|
const size_t metadataSize =
|
||||||
|
metadata.title.size() + metadata.author.size() + metadata.coverItemHref.size() + sizeof(uint32_t) * 3;
|
||||||
|
const size_t lutSize = sizeof(size_t) * spineCount + sizeof(size_t) * tocCount;
|
||||||
|
const size_t lutOffset = headerASize + metadataSize;
|
||||||
|
|
||||||
|
// Header A
|
||||||
|
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||||
|
serialization::writePod(bookFile, lutOffset);
|
||||||
|
serialization::writePod(bookFile, spineCount);
|
||||||
|
serialization::writePod(bookFile, tocCount);
|
||||||
|
// Metadata
|
||||||
|
serialization::writeString(bookFile, metadata.title);
|
||||||
|
serialization::writeString(bookFile, metadata.author);
|
||||||
|
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||||
|
|
||||||
|
// Loop through spine entries, writing LUT positions
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto pos = spineFile.position();
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through toc entries, writing LUT positions
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
auto pos = tocFile.position();
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
serialization::writePod(bookFile, pos + lutOffset + lutSize + spineFile.position());
|
||||||
|
}
|
||||||
|
|
||||||
|
// LUTs complete
|
||||||
|
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
||||||
|
|
||||||
|
const ZipFile zip("/sd" + epubPath);
|
||||||
|
size_t cumSize = 0;
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int j = 0; j < tocCount; j++) {
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
if (tocEntry.spineIndex == i) {
|
||||||
|
spineEntry.tocIndex = j;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
||||||
|
// Logging here is for debugging
|
||||||
|
if (spineEntry.tocIndex == -1) {
|
||||||
|
Serial.printf("[%lu] [BMC] Warning: Could not find TOC entry for spine item %d: %s\n", millis(), i,
|
||||||
|
spineEntry.href.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate size for cumulative size
|
||||||
|
size_t itemSize = 0;
|
||||||
|
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||||
|
if (zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||||
|
cumSize += itemSize;
|
||||||
|
spineEntry.cumulativeSize = cumSize;
|
||||||
|
} else {
|
||||||
|
Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write out spine data to book.bin
|
||||||
|
writeSpineEntry(bookFile, spineEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop through toc entries from toc file writing to book.bin
|
||||||
|
tocFile.seek(0);
|
||||||
|
for (int i = 0; i < tocCount; i++) {
|
||||||
|
auto tocEntry = readTocEntry(tocFile);
|
||||||
|
writeTocEntry(bookFile, tocEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
bookFile.close();
|
||||||
|
spineFile.close();
|
||||||
|
tocFile.close();
|
||||||
|
|
||||||
|
Serial.printf("[%lu] [BMC] Successfully built book.bin\n", millis());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||||
|
if (SD.exists((cachePath + tmpSpineBinFile).c_str())) {
|
||||||
|
SD.remove((cachePath + tmpSpineBinFile).c_str());
|
||||||
|
}
|
||||||
|
if (SD.exists((cachePath + tmpTocBinFile).c_str())) {
|
||||||
|
SD.remove((cachePath + tmpTocBinFile).c_str());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t BookMetadataCache::writeSpineEntry(File& file, const SpineEntry& entry) const {
|
||||||
|
const auto pos = file.position();
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writePod(file, entry.cumulativeSize);
|
||||||
|
serialization::writePod(file, entry.tocIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t BookMetadataCache::writeTocEntry(File& file, const TocEntry& entry) const {
|
||||||
|
const auto pos = file.position();
|
||||||
|
serialization::writeString(file, entry.title);
|
||||||
|
serialization::writeString(file, entry.href);
|
||||||
|
serialization::writeString(file, entry.anchor);
|
||||||
|
serialization::writePod(file, entry.level);
|
||||||
|
serialization::writePod(file, entry.spineIndex);
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||||
|
// this is because in this function we're marking positions of the items
|
||||||
|
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||||
|
if (!buildMode || !spineFile) {
|
||||||
|
Serial.printf("[%lu] [BMC] createSpineEntry called but not in build mode\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SpineEntry entry(href, 0, -1);
|
||||||
|
writeSpineEntry(spineFile, entry);
|
||||||
|
spineCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||||
|
const uint8_t level) {
|
||||||
|
if (!buildMode || !tocFile || !spineFile) {
|
||||||
|
Serial.printf("[%lu] [BMC] createTocEntry called but not in build mode\n", millis());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int spineIndex = -1;
|
||||||
|
// find spine index
|
||||||
|
spineFile.seek(0);
|
||||||
|
for (int i = 0; i < spineCount; i++) {
|
||||||
|
auto spineEntry = readSpineEntry(spineFile);
|
||||||
|
if (spineEntry.href == href) {
|
||||||
|
spineIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spineIndex == -1) {
|
||||||
|
Serial.printf("[%lu] [BMC] addTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const TocEntry entry(title, href, anchor, level, spineIndex);
|
||||||
|
writeTocEntry(tocFile, entry);
|
||||||
|
tocCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||||
|
|
||||||
|
bool BookMetadataCache::load() {
|
||||||
|
if (!FsHelpers::openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
serialization::readPod(bookFile, version);
|
||||||
|
if (version != BOOK_CACHE_VERSION) {
|
||||||
|
Serial.printf("[%lu] [BMC] Cache version mismatch: expected %d, got %d\n", millis(), BOOK_CACHE_VERSION, version);
|
||||||
|
bookFile.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialization::readPod(bookFile, lutOffset);
|
||||||
|
serialization::readPod(bookFile, spineCount);
|
||||||
|
serialization::readPod(bookFile, tocCount);
|
||||||
|
|
||||||
|
serialization::readString(bookFile, coreMetadata.title);
|
||||||
|
serialization::readString(bookFile, coreMetadata.author);
|
||||||
|
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||||
|
|
||||||
|
loaded = true;
|
||||||
|
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
Serial.printf("[%lu] [BMC] getSpineEntry called but cache not loaded\n", millis());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||||
|
Serial.printf("[%lu] [BMC] getSpineEntry index %d out of range\n", millis(), index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to spine LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(size_t) * index);
|
||||||
|
size_t spineEntryPos;
|
||||||
|
serialization::readPod(bookFile, spineEntryPos);
|
||||||
|
bookFile.seek(spineEntryPos);
|
||||||
|
return readSpineEntry(bookFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||||
|
if (!loaded) {
|
||||||
|
Serial.printf("[%lu] [BMC] getTocEntry called but cache not loaded\n", millis());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||||
|
Serial.printf("[%lu] [BMC] getTocEntry index %d out of range\n", millis(), index);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seek to TOC LUT item, read from LUT and get out data
|
||||||
|
bookFile.seek(lutOffset + sizeof(size_t) * spineCount + sizeof(size_t) * index);
|
||||||
|
size_t tocEntryPos;
|
||||||
|
serialization::readPod(bookFile, tocEntryPos);
|
||||||
|
bookFile.seek(tocEntryPos);
|
||||||
|
return readTocEntry(bookFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(File& file) const {
|
||||||
|
SpineEntry entry;
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readPod(file, entry.cumulativeSize);
|
||||||
|
serialization::readPod(file, entry.tocIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(File& file) const {
|
||||||
|
TocEntry entry;
|
||||||
|
serialization::readString(file, entry.title);
|
||||||
|
serialization::readString(file, entry.href);
|
||||||
|
serialization::readString(file, entry.anchor);
|
||||||
|
serialization::readPod(file, entry.level);
|
||||||
|
serialization::readPod(file, entry.spineIndex);
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
87
lib/Epub/Epub/BookMetadataCache.h
Normal file
87
lib/Epub/Epub/BookMetadataCache.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class BookMetadataCache {
|
||||||
|
public:
|
||||||
|
struct BookMetadata {
|
||||||
|
std::string title;
|
||||||
|
std::string author;
|
||||||
|
std::string coverItemHref;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SpineEntry {
|
||||||
|
std::string href;
|
||||||
|
size_t cumulativeSize;
|
||||||
|
int16_t tocIndex;
|
||||||
|
|
||||||
|
SpineEntry() : cumulativeSize(0), tocIndex(-1) {}
|
||||||
|
SpineEntry(std::string href, const size_t cumulativeSize, const 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, const uint8_t level, const int16_t spineIndex)
|
||||||
|
: title(std::move(title)),
|
||||||
|
href(std::move(href)),
|
||||||
|
anchor(std::move(anchor)),
|
||||||
|
level(level),
|
||||||
|
spineIndex(spineIndex) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string cachePath;
|
||||||
|
size_t lutOffset;
|
||||||
|
uint16_t spineCount;
|
||||||
|
uint16_t tocCount;
|
||||||
|
bool loaded;
|
||||||
|
bool buildMode;
|
||||||
|
|
||||||
|
File bookFile;
|
||||||
|
// Temp file handles during build
|
||||||
|
File spineFile;
|
||||||
|
File tocFile;
|
||||||
|
|
||||||
|
size_t writeSpineEntry(File& file, const SpineEntry& entry) const;
|
||||||
|
size_t writeTocEntry(File& file, const TocEntry& entry) const;
|
||||||
|
SpineEntry readSpineEntry(File& file) const;
|
||||||
|
TocEntry readTocEntry(File& file) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
BookMetadata coreMetadata;
|
||||||
|
|
||||||
|
explicit BookMetadataCache(std::string cachePath)
|
||||||
|
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
||||||
|
~BookMetadataCache() = default;
|
||||||
|
|
||||||
|
// Building phase (stream to disk immediately)
|
||||||
|
bool beginWrite();
|
||||||
|
bool beginContentOpfPass();
|
||||||
|
void createSpineEntry(const std::string& href);
|
||||||
|
bool endContentOpfPass();
|
||||||
|
bool beginTocPass();
|
||||||
|
void createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
|
||||||
|
bool endTocPass();
|
||||||
|
bool endWrite();
|
||||||
|
bool cleanupTmpFiles() const;
|
||||||
|
|
||||||
|
// Post-processing to update mappings and sizes
|
||||||
|
bool buildBookBin(const std::string& epubPath, const BookMetadata& metadata);
|
||||||
|
|
||||||
|
// Reading phase (read mode)
|
||||||
|
bool load();
|
||||||
|
SpineEntry getSpineEntry(int index);
|
||||||
|
TocEntry getTocEntry(int index);
|
||||||
|
int getSpineCount() const { return spineCount; }
|
||||||
|
int getTocCount() const { return tocCount; }
|
||||||
|
bool isLoaded() const { return loaded; }
|
||||||
|
};
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
struct EpubTocEntry {
|
|
||||||
std::string title;
|
|
||||||
std::string href;
|
|
||||||
std::string anchor;
|
|
||||||
uint8_t level;
|
|
||||||
};
|
|
||||||
92
lib/Epub/Epub/FsHelpers.cpp
Normal file
92
lib/Epub/Epub/FsHelpers.cpp
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForRead(const char* moduleName, const std::string& path, File& file) {
|
||||||
|
file = SD.open(path.c_str(), FILE_READ);
|
||||||
|
if (!file) {
|
||||||
|
Serial.printf("[%lu] [%s] Failed to open file for reading: %s\n", millis(), moduleName, path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::openFileForWrite(const char* moduleName, const std::string& path, File& file) {
|
||||||
|
file = SD.open(path.c_str(), FILE_WRITE, true);
|
||||||
|
if (!file) {
|
||||||
|
Serial.printf("[%lu] [%s] Failed to open file for writing: %s\n", millis(), moduleName, path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FsHelpers::removeDir(const char* path) {
|
||||||
|
// 1. Open the directory
|
||||||
|
File dir = SD.open(path);
|
||||||
|
if (!dir) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!dir.isDirectory()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = dir.openNextFile();
|
||||||
|
while (file) {
|
||||||
|
String filePath = path;
|
||||||
|
if (!filePath.endsWith("/")) {
|
||||||
|
filePath += "/";
|
||||||
|
}
|
||||||
|
filePath += file.name();
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
if (!removeDir(filePath.c_str())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!SD.remove(filePath.c_str())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file = dir.openNextFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
return SD.rmdir(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FsHelpers::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;
|
||||||
|
}
|
||||||
12
lib/Epub/Epub/FsHelpers.h
Normal file
12
lib/Epub/Epub/FsHelpers.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <FS.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class FsHelpers {
|
||||||
|
public:
|
||||||
|
static bool openFileForRead(const char* moduleName, const std::string& path, File& file);
|
||||||
|
static bool openFileForWrite(const char* moduleName, const std::string& path, File& file);
|
||||||
|
static bool removeDir(const char* path);
|
||||||
|
static std::string normalisePath(const std::string& path);
|
||||||
|
};
|
||||||
@@ -116,8 +116,7 @@ bool Section::clearCache() const {
|
|||||||
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
bool Section::persistPageDataToSD(const int fontId, const float lineCompression, const int marginTop,
|
||||||
const int marginRight, const int marginBottom, const int marginLeft,
|
const int marginRight, const int marginBottom, const int marginLeft,
|
||||||
const bool extraParagraphSpacing) {
|
const bool extraParagraphSpacing) {
|
||||||
const auto localPath = epub->getSpineItem(spineIndex);
|
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||||
|
|
||||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
File tmpHtml;
|
File tmpHtml;
|
||||||
if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
if (!FsHelpers::openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
#include "ContentOpfParser.h"
|
#include "ContentOpfParser.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
#include <Serialization.h>
|
||||||
#include <ZipFile.h>
|
#include <ZipFile.h>
|
||||||
|
|
||||||
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||||
}
|
constexpr char itemCacheFile[] = "/.items.bin";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool ContentOpfParser::setup() {
|
bool ContentOpfParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
@@ -28,6 +33,12 @@ ContentOpfParser::~ContentOpfParser() {
|
|||||||
XML_ParserFree(parser);
|
XML_ParserFree(parser);
|
||||||
parser = nullptr;
|
parser = nullptr;
|
||||||
}
|
}
|
||||||
|
if (tempItemStore) {
|
||||||
|
tempItemStore.close();
|
||||||
|
}
|
||||||
|
if (SD.exists((cachePath + itemCacheFile).c_str())) {
|
||||||
|
SD.remove((cachePath + itemCacheFile).c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||||
@@ -94,11 +105,21 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
|
|
||||||
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
self->state = IN_MANIFEST;
|
self->state = IN_MANIFEST;
|
||||||
|
if (!FsHelpers::openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
Serial.printf(
|
||||||
|
"[%lu] [COF] Couldn't open temp items file for writing. This is probably going to be a fatal error.\n",
|
||||||
|
millis());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||||
self->state = IN_SPINE;
|
self->state = IN_SPINE;
|
||||||
|
if (!FsHelpers::openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||||
|
Serial.printf(
|
||||||
|
"[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n",
|
||||||
|
millis());
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +156,13 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self->items[itemId] = href;
|
// Write items down to SD card
|
||||||
|
serialization::writeString(self->tempItemStore, itemId);
|
||||||
|
serialization::writeString(self->tempItemStore, href);
|
||||||
|
|
||||||
|
if (itemId == self->coverItemId) {
|
||||||
|
self->coverItemHref = href;
|
||||||
|
}
|
||||||
|
|
||||||
if (mediaType == MEDIA_TYPE_NCX) {
|
if (mediaType == MEDIA_TYPE_NCX) {
|
||||||
if (self->tocNcxPath.empty()) {
|
if (self->tocNcxPath.empty()) {
|
||||||
@@ -148,14 +175,29 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
// NOTE: This relies on spine appearing after item manifest (which is pretty safe as it's part of the EPUB spec)
|
||||||
for (int i = 0; atts[i]; i += 2) {
|
// Only run the spine parsing if there's a cache to add it to
|
||||||
if (strcmp(atts[i], "idref") == 0) {
|
if (self->cache) {
|
||||||
self->spineRefs.emplace_back(atts[i + 1]);
|
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
||||||
break;
|
for (int i = 0; atts[i]; i += 2) {
|
||||||
|
if (strcmp(atts[i], "idref") == 0) {
|
||||||
|
const std::string idref = atts[i + 1];
|
||||||
|
// Resolve the idref to href using items map
|
||||||
|
self->tempItemStore.seek(0);
|
||||||
|
std::string itemId;
|
||||||
|
std::string href;
|
||||||
|
while (self->tempItemStore.available()) {
|
||||||
|
serialization::readString(self->tempItemStore, itemId);
|
||||||
|
serialization::readString(self->tempItemStore, href);
|
||||||
|
if (itemId == idref) {
|
||||||
|
self->cache->createSpineEntry(href);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,11 +216,13 @@ void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name)
|
|||||||
|
|
||||||
if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||||
self->state = IN_PACKAGE;
|
self->state = IN_PACKAGE;
|
||||||
|
self->tempItemStore.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||||
self->state = IN_PACKAGE;
|
self->state = IN_PACKAGE;
|
||||||
|
self->tempItemStore.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Print.h>
|
#include <Print.h>
|
||||||
|
|
||||||
#include <map>
|
|
||||||
|
|
||||||
#include "Epub.h"
|
#include "Epub.h"
|
||||||
#include "expat.h"
|
#include "expat.h"
|
||||||
|
|
||||||
|
class BookMetadataCache;
|
||||||
|
|
||||||
class ContentOpfParser final : public Print {
|
class ContentOpfParser final : public Print {
|
||||||
enum ParserState {
|
enum ParserState {
|
||||||
START,
|
START,
|
||||||
@@ -16,10 +16,14 @@ class ContentOpfParser final : public Print {
|
|||||||
IN_SPINE,
|
IN_SPINE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const std::string& cachePath;
|
||||||
const std::string& baseContentPath;
|
const std::string& baseContentPath;
|
||||||
size_t remainingSize;
|
size_t remainingSize;
|
||||||
XML_Parser parser = nullptr;
|
XML_Parser parser = nullptr;
|
||||||
ParserState state = START;
|
ParserState state = START;
|
||||||
|
BookMetadataCache* cache;
|
||||||
|
File tempItemStore;
|
||||||
|
std::string coverItemId;
|
||||||
|
|
||||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||||
static void characterData(void* userData, const XML_Char* s, int len);
|
static void characterData(void* userData, const XML_Char* s, int len);
|
||||||
@@ -28,12 +32,11 @@ class ContentOpfParser final : public Print {
|
|||||||
public:
|
public:
|
||||||
std::string title;
|
std::string title;
|
||||||
std::string tocNcxPath;
|
std::string tocNcxPath;
|
||||||
std::string coverItemId;
|
std::string coverItemHref;
|
||||||
std::map<std::string, std::string> items;
|
|
||||||
std::vector<std::string> spineRefs;
|
|
||||||
|
|
||||||
explicit ContentOpfParser(const std::string& baseContentPath, const size_t xmlSize)
|
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
||||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
BookMetadataCache* cache)
|
||||||
|
: cachePath(cachePath), baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||||
~ContentOpfParser() override;
|
~ContentOpfParser() override;
|
||||||
|
|
||||||
bool setup();
|
bool setup();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
#include "TocNcxParser.h"
|
#include "TocNcxParser.h"
|
||||||
|
|
||||||
#include <Esp.h>
|
|
||||||
#include <HardwareSerial.h>
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
|
#include "../BookMetadataCache.h"
|
||||||
|
|
||||||
bool TocNcxParser::setup() {
|
bool TocNcxParser::setup() {
|
||||||
parser = XML_ParserCreate(nullptr);
|
parser = XML_ParserCreate(nullptr);
|
||||||
if (!parser) {
|
if (!parser) {
|
||||||
@@ -167,8 +168,9 @@ void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
|
|||||||
href = href.substr(0, pos);
|
href = href.substr(0, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push to vector
|
if (self->cache) {
|
||||||
self->toc.push_back({std::move(self->currentLabel), std::move(href), std::move(anchor), self->currentDepth});
|
self->cache->createTocEntry(self->currentLabel, href, anchor, self->currentDepth);
|
||||||
|
}
|
||||||
|
|
||||||
// Clear them so we don't re-add them if there are weird XML structures
|
// Clear them so we don't re-add them if there are weird XML structures
|
||||||
self->currentLabel.clear();
|
self->currentLabel.clear();
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Print.h>
|
#include <Print.h>
|
||||||
|
#include <expat.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "Epub/EpubTocEntry.h"
|
class BookMetadataCache;
|
||||||
#include "expat.h"
|
|
||||||
|
|
||||||
class TocNcxParser final : public Print {
|
class TocNcxParser final : public Print {
|
||||||
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
|
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
|
||||||
@@ -14,6 +13,7 @@ class TocNcxParser final : public Print {
|
|||||||
size_t remainingSize;
|
size_t remainingSize;
|
||||||
XML_Parser parser = nullptr;
|
XML_Parser parser = nullptr;
|
||||||
ParserState state = START;
|
ParserState state = START;
|
||||||
|
BookMetadataCache* cache;
|
||||||
|
|
||||||
std::string currentLabel;
|
std::string currentLabel;
|
||||||
std::string currentSrc;
|
std::string currentSrc;
|
||||||
@@ -24,10 +24,8 @@ class TocNcxParser final : public Print {
|
|||||||
static void endElement(void* userData, const XML_Char* name);
|
static void endElement(void* userData, const XML_Char* name);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
std::vector<EpubTocEntry> toc;
|
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache)
|
||||||
|
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||||
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize)
|
|
||||||
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
|
|
||||||
~TocNcxParser() override;
|
~TocNcxParser() override;
|
||||||
|
|
||||||
bool setup();
|
bool setup();
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ void EpubReaderActivity::renderScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!section) {
|
if (!section) {
|
||||||
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
const auto filepath = epub->getSpineItem(currentSpineIndex).href;
|
||||||
Serial.printf("[%lu] [ERS] Loading file: %s, index: %d\n", millis(), filepath.c_str(), 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));
|
section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
|
||||||
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
if (!section->loadCacheMetadata(READER_FONT_ID, lineCompression, marginTop, marginRight, marginBottom, marginLeft,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ void EpubReaderChapterSelectionActivity::onEnter() {
|
|||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
|
xTaskCreate(&EpubReaderChapterSelectionActivity::taskTrampoline, "EpubReaderChapterSelectionActivityTask",
|
||||||
2048, // Stack size
|
4096, // Stack size
|
||||||
this, // Parameters
|
this, // Parameters
|
||||||
1, // Priority
|
1, // Priority
|
||||||
&displayTaskHandle // Task handle
|
&displayTaskHandle // Task handle
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ void setup() {
|
|||||||
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
||||||
|
|
||||||
// SD Card Initialization
|
// SD Card Initialization
|
||||||
if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ)) {
|
// We need 6 open files concurrently when parsing a new chapter
|
||||||
|
if (!SD.begin(SD_SPI_CS, SPI, SPI_FQ, "/sd", 6)) {
|
||||||
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
|
Serial.printf("[%lu] [ ] SD card initialization failed\n", millis());
|
||||||
setupDisplayAndFonts();
|
setupDisplayAndFonts();
|
||||||
exitActivity();
|
exitActivity();
|
||||||
|
|||||||
Reference in New Issue
Block a user