diff --git a/CHANGELOG.md b/CHANGELOG.md index ddd7118f3d..abbf389548 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Long-press reader shortcuts that open another screen no longer close or confirm it again when releasing the shortcut button. - RoundedRaff's header battery icon and percentage now sit lower to avoid clipping at the top edge. - Lyra Carousel now redraws the Home header when restoring cached carousel frames so battery percentage and clock values stay current while navigating between books. +- EPUB ornamental breaks now show their hidden text fallback when the decorative image uses an unsupported format. - Reader-launched settings screens no longer jump back two screens after pressing Back. - Web file manager multi-delete now handles larger selections without failing after a small batch. diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index b335ffe254..ae4d334c6a 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -108,6 +108,10 @@ bool attributeContainsToken(const char* value, const char* token) { return false; } +bool classContainsToken(const std::string& value, const char* token) { + return attributeContainsToken(value.c_str(), token); +} + bool isHeaderOrBlock(const char* name) { return matches(name, HEADER_TAGS, std::size(HEADER_TAGS)) || matches(name, BLOCK_TAGS, std::size(BLOCK_TAGS)); } @@ -899,8 +903,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* // Skip elements with display:none before all fast paths (tables, links, etc.). if (cssStyle.hasDisplay() && cssStyle.display == CssDisplay::None) { - self->skipCurrentElement(); - return; + const bool isHiddenOrnamentalTextFallback = strcmp(name, "p") == 0 && self->unsupportedOrnamentalBreakDepth >= 0 && + self->ornamentalBreakDepth == self->unsupportedOrnamentalBreakDepth && + classContainsToken(classAttr, "ornamental-break-as-text"); + if (!isHiddenOrnamentalTextFallback) { + self->skipCurrentElement(); + return; + } + } + + if (self->ornamentalBreakDepth < 0 && classContainsToken(classAttr, "ornamental-break")) { + self->ornamentalBreakDepth = self->depth; + self->unsupportedOrnamentalBreakDepth = -1; } // Special handling for tables/cells: buffer simple tables for grid layout, with @@ -1111,7 +1125,12 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* // Resolve the image path relative to the HTML file std::string resolvedPath = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(self->contentBase + src)); - if (ImageDecoderFactory::isFormatSupported(resolvedPath)) { + const bool supportedFormat = ImageDecoderFactory::isFormatSupported(resolvedPath); + if (!supportedFormat && alt.empty() && self->ornamentalBreakDepth >= 0) { + self->unsupportedOrnamentalBreakDepth = self->ornamentalBreakDepth; + } + + if (supportedFormat) { // Create a unique filename for the cached image std::string ext; size_t extPos = resolvedPath.rfind('.'); @@ -1977,6 +1996,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n self->skipEndElementStateUntilDepth = INT_MAX; } + if (self->ornamentalBreakDepth == self->depth) { + self->ornamentalBreakDepth = -1; + self->unsupportedOrnamentalBreakDepth = -1; + } + if (self->tableDepth == 1 && (strcmp(name, "td") == 0 || strcmp(name, "th") == 0)) { self->finalizeCurrentTableCell(); self->nextWordContinues = false; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 92aee7bcdf..a504c8cbf1 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -66,6 +66,8 @@ class ChapterHtmlSlimParser { bool lowMemoryImageFallback = false; bool lowMemoryAbort = false; bool attemptedTextLayoutFontCacheRelease = false; + int ornamentalBreakDepth = -1; + int unsupportedOrnamentalBreakDepth = -1; // Style tracking (replaces depth-based approach) struct StyleStackEntry {