#include "Epub.h" #include #include #include #include #include #include #include #include #include #include #include "Epub/parsers/ContainerParser.h" #include "Epub/parsers/ContentOpfParser.h" #include "Epub/parsers/PageListSink.h" #include "Epub/parsers/PageMapParser.h" #include "Epub/parsers/TocNavParser.h" #include "Epub/parsers/TocNcxParser.h" namespace { enum class CoverImageFormat { Unknown, Jpeg, Png }; CoverImageFormat detectCoverImageFormat(FsFile& imageFile) { if (!imageFile || !imageFile.seek(0)) { return CoverImageFormat::Unknown; } uint8_t header[8] = {}; const int readBytes = imageFile.read(header, sizeof(header)); imageFile.seek(0); if (readBytes >= 3 && header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF) { return CoverImageFormat::Jpeg; } constexpr uint8_t PNG_SIGNATURE[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}; if (readBytes >= 8 && memcmp(header, PNG_SIGNATURE, sizeof(PNG_SIGNATURE)) == 0) { return CoverImageFormat::Png; } return CoverImageFormat::Unknown; } } // namespace bool Epub::findContentOpfFile(std::string* contentOpfFile) const { const auto containerPath = "META-INF/container.xml"; size_t containerSize; // Get file size without loading it all into heap if (!getItemSize(containerPath, &containerSize)) { LOG_ERR("EBP", "Could not find or size META-INF/container.xml"); return false; } ContainerParser containerParser(containerSize); if (!containerParser.setup()) { return false; } // Stream read (reusing your existing stream logic) if (!readItemContentsToStream(containerPath, containerParser, 512)) { LOG_ERR("EBP", "Could not read META-INF/container.xml"); return false; } // Extract the result if (containerParser.fullPath.empty()) { LOG_ERR("EBP", "Could not find valid rootfile in container.xml"); return false; } *contentOpfFile = std::move(containerParser.fullPath); return true; } bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, OpfCacheMode cacheMode) { const unsigned long opfParseStart = millis(); std::string contentOpfFilePath; if (!findContentOpfFile(&contentOpfFilePath)) { LOG_ERR("EBP", "Could not find content.opf in zip"); return false; } contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1); LOG_DBG("EBP", "Parsing content.opf: %s", contentOpfFilePath.c_str()); size_t contentOpfSize; if (!getItemSize(contentOpfFilePath, &contentOpfSize)) { LOG_ERR("EBP", "Could not get size of content.opf"); return false; } LOG_DBG("EBP", "content.opf size=%zu bytes", contentOpfSize); ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize, cacheMode == OpfCacheMode::Enabled ? bookMetadataCache.get() : nullptr); if (!opfParser.setup()) { LOG_ERR("EBP", "Could not setup content.opf parser"); return false; } const unsigned long streamStart = millis(); if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) { LOG_ERR("EBP", "Could not read content.opf"); return false; } const unsigned long streamMs = millis() - streamStart; // Grab data from opfParser into epub bookMetadata.title = opfParser.title; bookMetadata.author = opfParser.author; bookMetadata.language = opfParser.language; bookMetadata.coverItemHref = opfParser.coverItemHref; bookMetadata.series = opfParser.series; bookMetadata.seriesIndex = opfParser.seriesIndex; bookMetadata.description = opfParser.description; // Guide-based cover fallback: if no cover found via metadata/properties, // or if the manifest-declared cover path is invalid, try extracting the image // reference from the guide's cover page XHTML. bool shouldTryGuideCoverFallback = bookMetadata.coverItemHref.empty(); if (!bookMetadata.coverItemHref.empty()) { size_t coverItemSize = 0; if (!getItemSize(bookMetadata.coverItemHref, &coverItemSize)) { LOG_DBG("EBP", "Manifest cover not found in archive, trying guide cover fallback: %s", bookMetadata.coverItemHref.c_str()); shouldTryGuideCoverFallback = true; } } if (shouldTryGuideCoverFallback && !opfParser.guideCoverPageHref.empty()) { LOG_DBG("EBP", "Trying guide cover page: %s", opfParser.guideCoverPageHref.c_str()); size_t coverPageSize; uint8_t* coverPageData = readItemContentsToBytes(opfParser.guideCoverPageHref, &coverPageSize, true); if (coverPageData) { const std::string coverPageHtml(reinterpret_cast(coverPageData), coverPageSize); free(coverPageData); // Determine base path of the cover page for resolving relative image references std::string coverPageBase; const auto lastSlash = opfParser.guideCoverPageHref.rfind('/'); if (lastSlash != std::string::npos) { coverPageBase = opfParser.guideCoverPageHref.substr(0, lastSlash + 1); } // Search for image references: xlink:href="..." (SVG) and src="..." (img) std::string imageRef; for (const char* pattern : {"xlink:href=\"", "src=\""}) { auto pos = coverPageHtml.find(pattern); while (pos != std::string::npos) { pos += strlen(pattern); const auto endPos = coverPageHtml.find('"', pos); if (endPos != std::string::npos) { const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos); // Check if it's an image file if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref) || FsHelpers::hasGifExtension(ref)) { imageRef = ref; break; } } pos = coverPageHtml.find(pattern, pos); } if (!imageRef.empty()) break; } if (!imageRef.empty()) { bookMetadata.coverItemHref = FsHelpers::normalisePath(coverPageBase + imageRef); LOG_DBG("EBP", "Found cover image from guide: %s", bookMetadata.coverItemHref.c_str()); } } } auto isSupportedCoverType = [](const std::string& path) { return FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path); }; auto hasReadableSupportedCover = [&](const std::string& path) { if (path.empty() || !isSupportedCoverType(path)) return false; size_t coverSize = 0; return getItemSize(path, &coverSize); }; if (!hasReadableSupportedCover(bookMetadata.coverItemHref)) { if (!bookMetadata.coverItemHref.empty()) { LOG_DBG("EBP", "Cover href unresolved/unsupported, trying common cover candidates: %s", bookMetadata.coverItemHref.c_str()); } std::vector baseDirs; auto addBaseDir = [&](const std::string& dir) { if (dir.empty()) { for (const auto& existing : baseDirs) { if (existing.empty()) return; } baseDirs.emplace_back(); return; } const std::string normalized = FsHelpers::normalisePath(dir); const std::string withSlash = normalized.empty() ? std::string() : normalized + "/"; for (const auto& existing : baseDirs) { if (existing == withSlash) return; } baseDirs.push_back(withSlash); }; // 1) OPF directory first (most likely) // 2) Parent dir of OPF directory // 3) Common EPUB roots // 4) Archive root addBaseDir(contentBasePath); if (!contentBasePath.empty()) { const auto trimmed = contentBasePath.back() == '/' ? contentBasePath.substr(0, contentBasePath.size() - 1) : contentBasePath; const auto lastSlash = trimmed.rfind('/'); if (lastSlash != std::string::npos) { addBaseDir(trimmed.substr(0, lastSlash + 1)); } } addBaseDir("OEBPS/"); addBaseDir("OPS/"); addBaseDir("EPUB/"); addBaseDir(""); static constexpr const char* kCoverSubdirs[] = { "", "images/", "Images/", "image/", "img/", "graphics/", }; static constexpr const char* kCoverBaseNames[] = { "cover", "frontcover", "titlepage", "title", "cover-image", "coverimage", }; static constexpr const char* kCoverExtensions[] = { "jpg", "jpeg", "png", }; auto toUpper = [](std::string value) { for (char& ch : value) { ch = static_cast(std::toupper(static_cast(ch))); } return value; }; const unsigned long coverBatchStart = millis(); ZipFile zip(filepath); const bool zipIndexLoaded = zip.loadAllFileStatSlims(); LOG_DBG("EBP", "Common cover fallback indexed ZIP in %lu ms (ok=%d)", millis() - coverBatchStart, zipIndexLoaded ? 1 : 0); if (zipIndexLoaded) { int checkedCandidates = 0; const auto tryCandidate = [&](const std::string& candidate) { checkedCandidates++; size_t coverSize = 0; if (zip.getInflatedFileSize(candidate.c_str(), &coverSize) && coverSize > 0) { bookMetadata.coverItemHref = candidate; LOG_DBG("EBP", "Found cover image via common candidate fallback after %d checks in %lu ms: %s", checkedCandidates, millis() - coverBatchStart, bookMetadata.coverItemHref.c_str()); return true; } return false; }; bool foundCoverCandidate = false; for (const auto& baseDir : baseDirs) { for (const char* subDir : kCoverSubdirs) { for (const char* baseName : kCoverBaseNames) { const std::string lowerBase = baseName; const std::string upperBase = toUpper(lowerBase); for (const std::string* baseVariant : {&lowerBase, &upperBase}) { for (const char* ext : kCoverExtensions) { const std::string lowerExt = ext; const std::string upperExt = toUpper(lowerExt); for (const std::string* extVariant : {&lowerExt, &upperExt}) { const std::string candidate = FsHelpers::normalisePath(baseDir + subDir + *baseVariant + "." + *extVariant); if (tryCandidate(candidate)) { foundCoverCandidate = true; break; } } if (foundCoverCandidate) break; } if (foundCoverCandidate) break; } if (foundCoverCandidate) break; } if (foundCoverCandidate) break; } if (foundCoverCandidate) break; } if (!foundCoverCandidate) { LOG_DBG("EBP", "Common cover fallback checked %d cached candidates in %lu ms with no match", checkedCandidates, millis() - coverBatchStart); } } } bookMetadata.textReferenceHref = opfParser.textReferenceHref; if (!opfParser.tocNcxPath.empty()) { tocNcxItem = opfParser.tocNcxPath; } if (!opfParser.tocNavPath.empty()) { tocNavItem = opfParser.tocNavPath; } if (!opfParser.pageMapPath.empty()) { pageMapItem = opfParser.pageMapPath; } if (!opfParser.cssFiles.empty()) { cssFiles = opfParser.cssFiles; } LOG_DBG("EBP", "parseContentOpf total=%lu ms", millis() - opfParseStart); LOG_DBG("EBP", "Successfully parsed content.opf"); return true; } bool Epub::parseTocNcxFile() const { // the ncx file should have been specified in the content.opf file if (tocNcxItem.empty()) { LOG_DBG("EBP", "No ncx file specified"); return false; } LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str()); const auto tmpNcxPath = getCachePath() + "/toc.ncx"; FsFile tempNcxFile; if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) { return false; } readItemContentsToStream(tocNcxItem, tempNcxFile, 1024); tempNcxFile.close(); if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) { return false; } const auto ncxSize = tempNcxFile.size(); // Stream entries straight to pagelist.bin (long printed-page lists used to // blow the X3 heap when accumulated in a std::vector — see PageListSink). PageListSink ncxPageListSink(getCachePath()); TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get(), &ncxPageListSink); if (!ncxParser.setup()) { LOG_ERR("EBP", "Could not setup toc ncx parser"); tempNcxFile.close(); return false; } const auto ncxBuffer = static_cast(malloc(1024)); if (!ncxBuffer) { LOG_ERR("EBP", "Could not allocate memory for toc ncx parser"); tempNcxFile.close(); return false; } while (tempNcxFile.available()) { const auto readSize = tempNcxFile.read(ncxBuffer, 1024); if (readSize == 0) break; const auto processedSize = ncxParser.write(ncxBuffer, readSize); if (processedSize != readSize) { LOG_ERR("EBP", "Could not process all toc ncx data"); free(ncxBuffer); tempNcxFile.close(); return false; } } free(ncxBuffer); tempNcxFile.close(); Storage.remove(tmpNcxPath.c_str()); // Flush u16 count + close pagelist.bin (or remove it if no entries were // streamed). The section builder later reads this file to stamp printed-page labels // onto rendered pages without re-parsing the NCX. ncxPageListSink.finalize(); LOG_DBG("EBP", "Parsed TOC items"); return true; } bool Epub::parseTocNavFile() const { // the nav file should have been specified in the content.opf file (EPUB 3) if (tocNavItem.empty()) { LOG_DBG("EBP", "No nav file specified"); return false; } LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str()); const auto tmpNavPath = getCachePath() + "/toc.nav"; FsFile tempNavFile; if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) { return false; } readItemContentsToStream(tocNavItem, tempNavFile, 1024); tempNavFile.close(); if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) { return false; } const auto navSize = tempNavFile.size(); // Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf // and the HTMLX nav file will have hrefs relative to itself const std::string navContentBasePath = tocNavItem.substr(0, tocNavItem.find_last_of('/') + 1); // Stream