Deduplicate identical CSS files in EPUB parsing & probe images for dimensions instead of reading the full file

Some EPUB converters emit byte-identical stylesheets per chapter (100+ entries). Now scan the ZIP central directory once to identify duplicates by CRC32 and compressed size, then skip parsing identical files. This avoids redundant ZIP lookups and SD extraction round-trips while preserving all styles since rules merge into a global set. Also add extractItemToFile helper and allowEarlyStop parameter to readItemContentsToStream.
This commit is contained in:
Justin Mitchell
2026-07-20 17:16:54 -04:00
parent 792aab6d97
commit 9d147cade9
13 changed files with 420 additions and 50 deletions
+56 -4
View File
@@ -256,8 +256,44 @@ void Epub::parseCssFiles() const {
return;
}
// Some converters emit one byte-identical stylesheet per chapter (100+ .css
// entries), and each parse costs a zip locate plus an SD extract round-trip.
// Map every CSS path to its central-directory (CRC32, compressed size) in a
// single scan and parse only the first of each identical pair. Rules merge
// into one global set, so dropping exact duplicates cannot lose styles. A
// path that never matches a directory entry keeps key 0 and always parses.
std::vector<uint64_t> dedupKeys(cssFiles.size(), 0);
if (cssFiles.size() > 1) {
std::unordered_map<std::string, size_t> pathToIndex;
pathToIndex.reserve(cssFiles.size());
for (size_t i = 0; i < cssFiles.size(); i++) {
pathToIndex.emplace(FsHelpers::normalisePath(cssFiles[i]), i);
}
ZipFile(filepath).enumerateFileEntries([&](std::string_view entryPath, uint32_t crc32, uint32_t compressedSize) {
if (!FsHelpers::hasCssExtension(entryPath)) {
return;
}
const auto it = pathToIndex.find(std::string{entryPath});
if (it != pathToIndex.end()) {
dedupKeys[it->second] = (static_cast<uint64_t>(crc32) << 32) | compressedSize;
}
});
}
std::vector<uint64_t> seenKeys;
seenKeys.reserve(cssFiles.size());
size_t skippedDuplicates = 0;
// No cache yet - parse CSS files
for (const auto& cssPath : cssFiles) {
for (size_t cssIndex = 0; cssIndex < cssFiles.size(); cssIndex++) {
const auto& cssPath = cssFiles[cssIndex];
const uint64_t dedupKey = dedupKeys[cssIndex];
if (dedupKey != 0) {
if (std::find(seenKeys.begin(), seenKeys.end(), dedupKey) != seenKeys.end()) {
skippedDuplicates++;
continue;
}
seenKeys.push_back(dedupKey);
}
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
// Check heap before parsing - CSS parsing allocates heavily
@@ -312,7 +348,8 @@ void Epub::parseCssFiles() const {
LOG_ERR("EBP", "Failed to save CSS rules to cache");
}
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files (%zu identical duplicates skipped)",
cssParser->ruleCount(), cssFiles.size(), skippedDuplicates);
cssParser->clear();
}
@@ -728,14 +765,29 @@ uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size
return content;
}
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize,
const bool allowEarlyStop) const {
if (itemHref.empty()) {
LOG_DBG("EBP", "Failed to read item, empty href");
return false;
}
const std::string path = FsHelpers::normalisePath(itemHref);
return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize);
return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize, allowEarlyStop);
}
bool Epub::extractItemToFile(const std::string& itemHref, const std::string& destPath) const {
HalFile out;
if (!Storage.openFileForWrite("EBP", destPath, out)) {
return false;
}
const bool ok = readItemContentsToStream(itemHref, out, 4096);
out.flush();
out.close();
if (!ok) {
Storage.remove(destPath.c_str());
}
return ok;
}
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {