feat: integrate CSS rules caching in Epub loader

- Add cssFiles member to Epub class (moved from BookMetadataCache)
- Add getCssRulesCache() and loadCssRulesFromCache() methods
- Update parseCssFiles() to save parsed rules to cache
- Try loading from css_rules.cache before parsing CSS files
- Add skipLoadingCss parameter to Epub::load() for performance
- Remove cssFiles from BookMetadataCache (no longer needed)
- Revert BookMetadataCache version to 5 (pre-CSS-files format)

When loading an EPUB:
1. Try to load cached CSS rules first
2. If cache miss, parse CSS files and save to cache
3. If skipLoadingCss=true, skip CSS entirely (for cover display)
This commit is contained in:
Jake Kenneally 2026-02-03 19:44:58 -05:00
parent 6115bf3cd2
commit cd61b263e5
4 changed files with 77 additions and 66 deletions

View File

@ -86,8 +86,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
tocNavItem = opfParser.tocNavPath; tocNavItem = opfParser.tocNavPath;
} }
// Copy CSS files to metadata if (!opfParser.cssFiles.empty()) {
bookMetadata.cssFiles = opfParser.cssFiles; cssFiles = opfParser.cssFiles;
}
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis()); Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
return true; return true;
@ -207,21 +208,30 @@ bool Epub::parseTocNavFile() const {
return true; return true;
} }
bool Epub::parseCssFiles() { std::string Epub::getCssRulesCache() const { return cachePath + "/css_rules.cache"; }
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
Serial.printf("[%lu] [EBP] Cannot parse CSS, cache not loaded\n", millis()); bool Epub::loadCssRulesFromCache() const {
FsFile cssCacheFile;
if (SdMan.openFileForRead("EBP", getCssRulesCache(), cssCacheFile)) {
if (cssParser->loadFromCache(cssCacheFile)) {
cssCacheFile.close();
Serial.printf("[%lu] [EBP] Loaded CSS rules from cache\n", millis());
return true;
}
cssCacheFile.close();
Serial.printf("[%lu] [EBP] CSS cache invalid, reparsing\n", millis());
}
return false; return false;
} }
// Always create CssParser - needed for inline style parsing even without CSS files void Epub::parseCssFiles() const {
cssParser.reset(new CssParser());
const auto& cssFiles = bookMetadataCache->coreMetadata.cssFiles;
if (cssFiles.empty()) { if (cssFiles.empty()) {
Serial.printf("[%lu] [EBP] No CSS files to parse, but CssParser created for inline styles\n", millis()); Serial.printf("[%lu] [EBP] No CSS files to parse, but CssParser created for inline styles\n", millis());
return true;
} }
// Try to load from CSS cache first
if (!loadCssRulesFromCache()) {
// Cache miss - parse CSS files
for (const auto& cssPath : cssFiles) { for (const auto& cssPath : cssFiles) {
Serial.printf("[%lu] [EBP] Parsing CSS file: %s\n", millis(), cssPath.c_str()); Serial.printf("[%lu] [EBP] Parsing CSS file: %s\n", millis(), cssPath.c_str());
@ -251,22 +261,38 @@ bool Epub::parseCssFiles() {
SdMan.remove(tmpCssPath.c_str()); SdMan.remove(tmpCssPath.c_str());
} }
// Save to cache for next time
FsFile cssCacheFile;
if (SdMan.openFileForWrite("EBP", getCssRulesCache(), cssCacheFile)) {
cssParser->saveToCache(cssCacheFile);
cssCacheFile.close();
}
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files\n", millis(), cssParser->ruleCount(), Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files\n", millis(), cssParser->ruleCount(),
cssFiles.size()); cssFiles.size());
return true; }
} }
// load in the meta data for the epub file // load in the meta data for the epub file
bool Epub::load(const bool buildIfMissing) { bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str()); Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
// Initialize spine/TOC cache // Initialize spine/TOC cache
bookMetadataCache.reset(new BookMetadataCache(cachePath)); bookMetadataCache.reset(new BookMetadataCache(cachePath));
// Always create CssParser - needed for inline style parsing even without CSS files
cssParser.reset(new CssParser());
// Try to load existing cache first // Try to load existing cache first
if (bookMetadataCache->load()) { if (bookMetadataCache->load()) {
// Parse CSS files from loaded cache if (!skipLoadingCss && !loadCssRulesFromCache()) {
Serial.printf("[%lu] [EBP] Warning: CSS rules cache not found, attempting to parse CSS files\n", millis());
// to get CSS file list
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
Serial.printf("[%lu] [EBP] Could not parse content.opf from cached bookMetadata for CSS files\n", millis());
// continue anyway - book will work without CSS and we'll still load any inline style CSS
}
parseCssFiles(); parseCssFiles();
}
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true; return true;
} }
@ -363,8 +389,10 @@ bool Epub::load(const bool buildIfMissing) {
return false; return false;
} }
if (!skipLoadingCss) {
// Parse CSS files after cache reload // Parse CSS files after cache reload
parseCssFiles(); parseCssFiles();
}
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str()); Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
return true; return true;

View File

@ -27,12 +27,16 @@ class Epub {
std::unique_ptr<BookMetadataCache> bookMetadataCache; std::unique_ptr<BookMetadataCache> bookMetadataCache;
// CSS parser for styling // CSS parser for styling
std::unique_ptr<CssParser> cssParser; std::unique_ptr<CssParser> cssParser;
// CSS files
std::vector<std::string> cssFiles;
bool findContentOpfFile(std::string* contentOpfFile) const; bool findContentOpfFile(std::string* contentOpfFile) const;
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata); bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
bool parseTocNcxFile() const; bool parseTocNcxFile() const;
bool parseTocNavFile() const; bool parseTocNavFile() const;
bool parseCssFiles(); void parseCssFiles() const;
std::string getCssRulesCache() const;
bool loadCssRulesFromCache() const;
public: public:
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) { explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
@ -41,7 +45,7 @@ class Epub {
} }
~Epub() = default; ~Epub() = default;
std::string& getBasePath() { return contentBasePath; } std::string& getBasePath() { return contentBasePath; }
bool load(bool buildIfMissing = true); bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
bool clearCache() const; bool clearCache() const;
void setupCacheDir() const; void setupCacheDir() const;
const std::string& getCachePath() const; const std::string& getCachePath() const;

View File

@ -9,7 +9,7 @@
#include "FsHelpers.h" #include "FsHelpers.h"
namespace { namespace {
constexpr uint8_t BOOK_CACHE_VERSION = 6; constexpr uint8_t BOOK_CACHE_VERSION = 5;
constexpr char bookBinFile[] = "/book.bin"; constexpr char bookBinFile[] = "/book.bin";
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp"; constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
constexpr char tmpTocBinFile[] = "/toc.bin.tmp"; constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
@ -115,14 +115,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
constexpr uint32_t headerASize = constexpr uint32_t headerASize =
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount); sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
// Calculate CSS files size: count + each string (length + data)
uint32_t cssFilesSize = sizeof(uint16_t); // count
for (const auto& css : metadata.cssFiles) {
cssFilesSize += sizeof(uint32_t) + css.size();
}
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() + const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
metadata.coverItemHref.size() + metadata.textReferenceHref.size() + metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
sizeof(uint32_t) * 5 + cssFilesSize; sizeof(uint32_t) * 5;
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount; const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
const uint32_t lutOffset = headerASize + metadataSize; const uint32_t lutOffset = headerASize + metadataSize;
@ -137,11 +132,6 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
serialization::writeString(bookFile, metadata.language); serialization::writeString(bookFile, metadata.language);
serialization::writeString(bookFile, metadata.coverItemHref); serialization::writeString(bookFile, metadata.coverItemHref);
serialization::writeString(bookFile, metadata.textReferenceHref); serialization::writeString(bookFile, metadata.textReferenceHref);
// CSS files
serialization::writePod(bookFile, static_cast<uint16_t>(metadata.cssFiles.size()));
for (const auto& css : metadata.cssFiles) {
serialization::writeString(bookFile, css);
}
// Loop through spine entries, writing LUT positions // Loop through spine entries, writing LUT positions
spineFile.seek(0); spineFile.seek(0);
@ -395,16 +385,6 @@ bool BookMetadataCache::load() {
serialization::readString(bookFile, coreMetadata.language); serialization::readString(bookFile, coreMetadata.language);
serialization::readString(bookFile, coreMetadata.coverItemHref); serialization::readString(bookFile, coreMetadata.coverItemHref);
serialization::readString(bookFile, coreMetadata.textReferenceHref); serialization::readString(bookFile, coreMetadata.textReferenceHref);
// CSS files
uint16_t cssCount;
serialization::readPod(bookFile, cssCount);
coreMetadata.cssFiles.clear();
coreMetadata.cssFiles.reserve(cssCount);
for (uint16_t i = 0; i < cssCount; i++) {
std::string cssPath;
serialization::readString(bookFile, cssPath);
coreMetadata.cssFiles.push_back(std::move(cssPath));
}
loaded = true; loaded = true;
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount); Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);

View File

@ -14,7 +14,6 @@ class BookMetadataCache {
std::string language; std::string language;
std::string coverItemHref; std::string coverItemHref;
std::string textReferenceHref; std::string textReferenceHref;
std::vector<std::string> cssFiles;
}; };
struct SpineEntry { struct SpineEntry {