From 57447a56a69d0163ce24a18333d61564221cb049 Mon Sep 17 00:00:00 2001 From: Leopoldo Pla Sempere Date: Thu, 2 Jul 2026 11:35:24 +0200 Subject: [PATCH] feat(epub): improve text-decoration support (#2397) Co-authored-by: JiangoJ --- docs/file-formats.md | 8 +- lib/EpdFont/EpdFontFamily.cpp | 2 +- lib/EpdFont/EpdFontFamily.h | 4 + lib/Epub/Epub/Section.cpp | 4 +- lib/Epub/Epub/blocks/TextBlock.cpp | 76 +++++++++++- lib/Epub/Epub/css/CssParser.cpp | 28 +++-- lib/Epub/Epub/css/CssParser.h | 2 +- lib/Epub/Epub/css/CssStyle.h | 15 ++- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 116 ++++++++++-------- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 9 +- test/epubs/test_text_decorations.epub | Bin 0 -> 2987 bytes 11 files changed, 183 insertions(+), 81 deletions(-) create mode 100644 test/epubs/test_text_decorations.epub diff --git a/docs/file-formats.md b/docs/file-formats.md index 0b505fda..eec9474f 100644 --- a/docs/file-formats.md +++ b/docs/file-formats.md @@ -90,13 +90,13 @@ if (parsedSize != fileSize) { ## `section.bin` -### Version 25 +### Version 28 Each file in `sections/*.bin` stores one laid-out spine section. The header is also the cache-busting key: if any layout-affecting setting differs from the current reader settings, the section is discarded and rebuilt. -Version 25 includes: +Version 28 includes: - cache-busting fields for paragraph alignment, hyphenation, embedded CSS, image rendering mode, and Focus Reading @@ -105,6 +105,8 @@ Version 25 includes: - paragraph and list-item LUTs used by KOReader sync page refinement - optional per-word Focus Reading split metadata - per-page footnote entries +- serialized word style bits for underline, strikethrough, superscript, and + subscript ImHex pattern: @@ -113,7 +115,7 @@ import std.mem; import std.string; import std.core; -#define EXPECTED_VERSION 25 +#define EXPECTED_VERSION 28 #define MAX_STRING_LENGTH 65535 #define FOOTNOTE_NUMBER_LEN 32 #define FOOTNOTE_HREF_LEN 96 diff --git a/lib/EpdFont/EpdFontFamily.cpp b/lib/EpdFont/EpdFontFamily.cpp index 7af388b5..4299c80f 100644 --- a/lib/EpdFont/EpdFontFamily.cpp +++ b/lib/EpdFont/EpdFontFamily.cpp @@ -1,7 +1,7 @@ #include "EpdFontFamily.h" const EpdFont* EpdFontFamily::getFont(const Style style) const { - // Extract font style bits (ignore UNDERLINE bit for font selection) + // Extract font style bits; render-time overlay bits do not affect font selection. const bool hasBold = (style & BOLD) != 0; const bool hasItalic = (style & ITALIC) != 0; diff --git a/lib/EpdFont/EpdFontFamily.h b/lib/EpdFont/EpdFontFamily.h index 2712bdbc..c65fb1f4 100644 --- a/lib/EpdFont/EpdFontFamily.h +++ b/lib/EpdFont/EpdFontFamily.h @@ -17,6 +17,7 @@ class EpdFontFamily { SUP = 16, // superscript: glyph scaled 50%, raised ~40% of ascender SUB = 32, // subscript: glyph scaled 50%, lowered ~25% of ascender }; + static constexpr uint8_t TEXT_DECORATION_MASK = static_cast(UNDERLINE | STRIKETHROUGH); explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr, const EpdFont* boldItalic = nullptr) @@ -27,6 +28,9 @@ class EpdFontFamily { const EpdGlyph* getGlyph(uint32_t cp, Style style = REGULAR) const; int8_t getKerning(uint32_t leftCp, uint32_t rightCp, Style style = REGULAR) const; uint32_t applyLigatures(uint32_t cp, const char*& text, Style style = REGULAR) const; + static constexpr bool hasTextDecoration(const Style style) { + return (static_cast(style) & TEXT_DECORATION_MASK) != 0; + } private: const EpdFont* regular; diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index db249481..12ef05eb 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -10,8 +10,8 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -// v27: words NFC-composed at layout time; bump invalidates NFD section caches. -constexpr uint8_t SECTION_FILE_VERSION = 27; +// v28: text decoration bits now include line-through in serialized wordStyles. +constexpr uint8_t SECTION_FILE_VERSION = 28; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) + diff --git a/lib/Epub/Epub/blocks/TextBlock.cpp b/lib/Epub/Epub/blocks/TextBlock.cpp index 9260fa32..3d5f920b 100644 --- a/lib/Epub/Epub/blocks/TextBlock.cpp +++ b/lib/Epub/Epub/blocks/TextBlock.cpp @@ -21,6 +21,39 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int const bool scanning = renderer.isFontCacheScanning(); const int ascender = renderer.getFontAscenderSize(fontId); + + struct DecorationLineTracker { + EpdFontFamily::Style style; + int yOffset; + int startX = -1; + int endX = -1; + int yPos = 0; + + bool active() const { return startX != -1; } + void reset() { + startX = -1; + endX = -1; + yPos = 0; + } + }; + + DecorationLineTracker decorationLines[] = { + {EpdFontFamily::UNDERLINE, ascender + 2}, + {EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5}, + }; + + const auto flushDecoration = [&](DecorationLineTracker& line) { + if (line.active()) { + renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true); + line.reset(); + } + }; + const auto flushDecorations = [&]() { + for (auto& line : decorationLines) { + flushDecoration(line); + } + }; + for (size_t i = 0; i < words.size(); i++) { const int wordX = wordXpos[i] + x; const EpdFontFamily::Style currentStyle = wordStyles[i]; @@ -59,18 +92,51 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int renderer.drawText(fontId, wordX, wordY, words[i].c_str(), true, currentStyle, baseDir); } - if (!scanning && (currentStyle & EpdFontFamily::UNDERLINE) != 0) { + if (scanning) { + continue; + } + + if (EpdFontFamily::hasTextDecoration(currentStyle)) { const std::string& w = words[i]; - int underlineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir); - const int underlineY = wordY + ascender + 2; + int lineStartX = wordX; + int lineWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle, baseDir); if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) { - underlineWidth = (underlineWidth + 1) / 2; + lineWidth = (lineWidth + 1) / 2; } - renderer.drawLine(wordX, underlineY, wordX + underlineWidth, underlineY, true); + // Do not decorate the synthetic em-space used for paragraph indentation. + if (w.size() >= 3 && static_cast(w[0]) == 0xE2 && static_cast(w[1]) == 0x80 && + static_cast(w[2]) == 0x83) { + const char* visibleText = w.c_str() + 3; + lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle); + lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir); + if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) { + lineWidth = (lineWidth + 1) / 2; + } + } + + for (auto& line : decorationLines) { + if ((currentStyle & line.style) == 0) { + flushDecoration(line); + continue; + } + + const int lineY = wordY + line.yOffset; + if (line.active() && line.yPos != lineY) { + flushDecoration(line); + } + if (!line.active()) { + line.startX = lineStartX; + line.yPos = lineY; + } + line.endX = lineStartX + lineWidth; + } + } else { + flushDecorations(); } } + flushDecorations(); } bool TextBlock::serialize(HalFile& file) const { diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index 9d0e3252..3c30fd85 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -67,13 +67,6 @@ constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKe [](char a, char b) { return asciiToLower(a) == b; }); } -// Case-insensitive ASCII substring search. Only needed by text-decoration, -// which accepts multi-value strings like "underline solid red". -constexpr bool icontainsAscii(std::string_view value, std::string_view lowercaseKeyword) { - return std::search(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(), - [](char a, char b) { return asciiToLower(a) == b; }) != value.end(); -} - // Walk s and invoke fn(token) for each non-empty run between delimiters. // Tokens are boundary-trimmed and yielded as string_views into s; no // allocation. Runs of consecutive delimiters coalesce — no empty tokens are @@ -252,11 +245,20 @@ CssFontWeight CssParser::interpretFontWeight(std::string_view val) { } CssTextDecoration CssParser::interpretDecoration(std::string_view val) { - // text-decoration can have multiple space-separated values - if (icontainsAscii(val, "underline")) { - return CssTextDecoration::Underline; - } - return CssTextDecoration::None; + // text-decoration can have multiple space-separated values. Compare whole tokens + // so malformed values like "notunderline" do not accidentally enable a line. + CssTextDecoration result = CssTextDecoration::None; + bool explicitNone = false; + forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) { + if (iequalsAscii(token, "none")) { + explicitNone = true; + } else if (iequalsAscii(token, "underline")) { + result = result | CssTextDecoration::Underline; + } else if (iequalsAscii(token, "line-through")) { + result = result | CssTextDecoration::LineThrough; + } + }); + return explicitNone ? CssTextDecoration::None : result; } CssLength CssParser::interpretLength(std::string_view val) { @@ -868,7 +870,7 @@ bool CssParser::loadFromCache() { rulesBySelector_.clear(); return false; } - style.textDecoration = static_cast(enumVal); + style.textDecoration = static_cast(enumVal & CSS_TEXT_DECORATION_MASK); if (file.read(&enumVal, 1) != 1) { rulesBySelector_.clear(); diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index 45346c71..41797fd3 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -33,7 +33,7 @@ class CssParser { public: // Bump when CSS cache format or rules change; section caches are invalidated when this changes - static constexpr uint8_t CSS_CACHE_VERSION = 6; + static constexpr uint8_t CSS_CACHE_VERSION = 7; explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} ~CssParser() = default; diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index 3fd47c8b..3e3a6fa6 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -52,8 +52,19 @@ enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 }; // Font weight options - CSS supports 100-900, we simplify to normal/bold enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 }; -// Text decoration options -enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 }; +// Text decoration options. Values are bit flags so CSS can combine multiple line decorations. +enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 }; + +constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) { + return static_cast(static_cast(a) | static_cast(b)); +} + +constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) { + return static_cast(static_cast(a) & static_cast(b)); +} + +constexpr uint8_t CSS_TEXT_DECORATION_MASK = + static_cast(CssTextDecoration::Underline) | static_cast(CssTextDecoration::LineThrough); // Display options - only None and Block are relevant for e-ink rendering enum class CssDisplay : uint8_t { Block = 0, None = 1 }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index bd51f9d2..31b94fbf 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -34,6 +34,7 @@ constexpr const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr const char* BOLD_TAGS[] = {"b", "strong"}; constexpr const char* ITALIC_TAGS[] = {"i", "em"}; constexpr const char* UNDERLINE_TAGS[] = {"u", "ins"}; +constexpr const char* LINETHROUGH_TAGS[] = {"del", "s", "strike"}; constexpr const char* IMAGE_TAGS[] = {"img"}; constexpr const char* SKIP_TAGS[] = {"head"}; @@ -89,13 +90,50 @@ void ChapterHtmlSlimParser::applyDirectionToEntry(StyleStackEntry& entry, const } } -// Update effective bold/italic/underline based on block style and inline style stack +EpdFontFamily::Style ChapterHtmlSlimParser::fontStyleForTextDecoration(const CssTextDecoration decoration) { + EpdFontFamily::Style style = EpdFontFamily::REGULAR; + if ((decoration & CssTextDecoration::Underline) != CssTextDecoration::None) { + style = static_cast(style | EpdFontFamily::UNDERLINE); + } + if ((decoration & CssTextDecoration::LineThrough) != CssTextDecoration::None) { + style = static_cast(style | EpdFontFamily::STRIKETHROUGH); + } + return style; +} + +void ChapterHtmlSlimParser::applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css) { + if (css.hasTextDecoration()) { + entry.hasTextDecoration = true; + entry.textDecoration = css.textDecoration; + } +} + +void ChapterHtmlSlimParser::pushDecorationStyleEntry(const CssTextDecoration defaultDecoration, + const CssStyle& cssStyle) { + StyleStackEntry entry; + entry.depth = depth; + entry.hasTextDecoration = true; + entry.textDecoration = cssStyle.hasTextDecoration() ? cssStyle.textDecoration : defaultDecoration; + if (cssStyle.hasFontWeight()) { + entry.hasBold = true; + entry.bold = cssStyle.fontWeight == CssFontWeight::Bold; + } + if (cssStyle.hasFontStyle()) { + entry.hasItalic = true; + entry.italic = cssStyle.fontStyle == CssFontStyle::Italic; + } + applyDirectionToEntry(entry, cssStyle); + inlineStyleStack.push_back(entry); + updateEffectiveInlineStyle(); +} + +// Update effective bold/italic/decorations based on block style and inline style stack void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { // Start with block-level styles effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold; effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic; - effectiveUnderline = - currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline; + effectiveTextDecoration = + currentCssStyle.hasTextDecoration() ? currentCssStyle.textDecoration : CssTextDecoration::None; effectiveDirectionDefined = currentCssStyle.hasDirection(); effectiveDirection = currentCssStyle.direction; effectiveSup = false; @@ -109,8 +147,10 @@ void ChapterHtmlSlimParser::updateEffectiveInlineStyle() { if (entry.hasItalic) { effectiveItalic = entry.italic; } - if (entry.hasUnderline) { - effectiveUnderline = entry.underline; + // CSS line decorations propagate through descendants; child entries add + // their own lines but cannot cancel an ancestor's already active line. + if (entry.hasTextDecoration) { + effectiveTextDecoration = effectiveTextDecoration | entry.textDecoration; } if (entry.hasDirection) { effectiveDirectionDefined = true; @@ -164,7 +204,6 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() { // Determine font style from depth-based tracking and CSS effective style const bool isBold = boldUntilDepth < depth || effectiveBold; const bool isItalic = italicUntilDepth < depth || effectiveItalic; - const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline; // Combine style flags using bitwise OR EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; @@ -174,9 +213,7 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() { if (isItalic) { fontStyle = static_cast(fontStyle | EpdFontFamily::ITALIC); } - if (isUnderline) { - fontStyle = static_cast(fontStyle | EpdFontFamily::UNDERLINE); - } + fontStyle = static_cast(fontStyle | fontStyleForTextDecoration(effectiveTextDecoration)); if (effectiveSup) { fontStyle = static_cast(fontStyle | EpdFontFamily::SUP); } else if (effectiveSub) { @@ -420,14 +457,15 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* headerStyle.bold = false; headerStyle.hasItalic = true; headerStyle.italic = true; - headerStyle.hasUnderline = true; - headerStyle.underline = false; self->inlineStyleStack.push_back(headerStyle); self->updateEffectiveInlineStyle(); + const CssTextDecoration savedTextDecoration = self->effectiveTextDecoration; + self->effectiveTextDecoration = CssTextDecoration::None; self->characterData(userData, headerText.c_str(), static_cast(headerText.length())); if (self->partWordBufferIndex > 0) { self->flushPartWordBuffer(); } + self->effectiveTextDecoration = savedTextDecoration; self->nextWordContinues = false; self->inlineStyleStack.pop_back(); self->updateEffectiveInlineStyle(); @@ -760,12 +798,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->currentFootnote.number[0] = '\0'; self->currentFootnoteLinkTextLen = 0; - // Apply underline style to visually indicate the link - self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth); + // Apply underline style to visually indicate the link. StyleStackEntry entry; entry.depth = self->depth; - entry.hasUnderline = true; - entry.underline = true; + entry.hasTextDecoration = true; + entry.textDecoration = CssTextDecoration::Underline; applyDirectionToEntry(entry, cssStyle); self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); @@ -837,23 +874,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->flushPartWordBuffer(); self->nextWordContinues = true; } - self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth); - // Push inline style entry for underline tag - StyleStackEntry entry; - entry.depth = self->depth; // Track depth for matching pop - entry.hasUnderline = true; - entry.underline = true; - if (cssStyle.hasFontWeight()) { - entry.hasBold = true; - entry.bold = cssStyle.fontWeight == CssFontWeight::Bold; + self->pushDecorationStyleEntry(CssTextDecoration::Underline, cssStyle); + } else if (matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS))) { + // Flush buffer before style change so preceding text gets current style + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + self->nextWordContinues = true; } - if (cssStyle.hasFontStyle()) { - entry.hasItalic = true; - entry.italic = cssStyle.fontStyle == CssFontStyle::Italic; - } - applyDirectionToEntry(entry, cssStyle); - self->inlineStyleStack.push_back(entry); - self->updateEffectiveInlineStyle(); + self->pushDecorationStyleEntry(CssTextDecoration::LineThrough, cssStyle); } else if (matches(name, BOLD_TAGS, std::size(BOLD_TAGS))) { // Flush buffer before style change so preceding text gets current style if (self->partWordBufferIndex > 0) { @@ -870,10 +898,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.hasItalic = true; entry.italic = cssStyle.fontStyle == CssFontStyle::Italic; } - if (cssStyle.hasTextDecoration()) { - entry.hasUnderline = true; - entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline; - } + applyTextDecorationToEntry(entry, cssStyle); applyDirectionToEntry(entry, cssStyle); self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); @@ -893,10 +918,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.hasBold = true; entry.bold = cssStyle.fontWeight == CssFontWeight::Bold; } - if (cssStyle.hasTextDecoration()) { - entry.hasUnderline = true; - entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline; - } + applyTextDecorationToEntry(entry, cssStyle); applyDirectionToEntry(entry, cssStyle); self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); @@ -935,10 +957,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* entry.hasItalic = true; entry.italic = cssStyle.fontStyle == CssFontStyle::Italic; } - if (cssStyle.hasTextDecoration()) { - entry.hasUnderline = true; - entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline; - } + applyTextDecorationToEntry(entry, cssStyle); applyDirectionToEntry(entry, cssStyle); if (cssStyle.hasVerticalAlign()) { if (cssStyle.verticalAlign == CssVerticalAlign::Super) { @@ -1150,9 +1169,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n !self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth - 1; const bool willClearBold = self->boldUntilDepth == self->depth - 1; const bool willClearItalic = self->italicUntilDepth == self->depth - 1; - const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1; - const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline; + const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic; const bool headerOrBlockTag = isHeaderOrBlock(name); const bool tableStructuralTag = isTableStructuralTag(name); @@ -1171,7 +1189,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n !matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) && self->depth != 1; const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, std::size(BOLD_TAGS)) || matches(name, ITALIC_TAGS, std::size(ITALIC_TAGS)) || - matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || tableStructuralTag || + matches(name, UNDERLINE_TAGS, std::size(UNDERLINE_TAGS)) || + matches(name, LINETHROUGH_TAGS, std::size(LINETHROUGH_TAGS)) || tableStructuralTag || matches(name, IMAGE_TAGS, std::size(IMAGE_TAGS)) || self->depth == 1; if (shouldFlush) { @@ -1230,11 +1249,6 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n self->italicUntilDepth = INT_MAX; } - // Leaving underline tag - if (self->underlineUntilDepth == self->depth) { - self->underlineUntilDepth = INT_MAX; - } - // Pop from inline style stack if we pushed an entry at this depth // This handles all inline elements: b, i, u, span, etc. if (!self->inlineStyleStack.empty() && self->inlineStyleStack.back().depth == self->depth) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index d2fd7778..4e619ae3 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -31,7 +31,6 @@ class ChapterHtmlSlimParser { int skipUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX; int italicUntilDepth = INT_MAX; - int underlineUntilDepth = INT_MAX; // buffer for building up words from characters, will auto break if longer than this // leave one char at end for null pointer char partWordBuffer[MAX_WORD_SIZE + 1] = {}; @@ -60,7 +59,8 @@ class ChapterHtmlSlimParser { int depth = 0; bool hasBold = false, bold = false; bool hasItalic = false, italic = false; - bool hasUnderline = false, underline = false; + bool hasTextDecoration = false; + CssTextDecoration textDecoration = CssTextDecoration::None; bool hasDirection = false; CssTextDirection direction = CssTextDirection::Ltr; bool hasSup = false, sup = false; @@ -71,7 +71,7 @@ class ChapterHtmlSlimParser { CssStyle currentCssStyle; bool effectiveBold = false; bool effectiveItalic = false; - bool effectiveUnderline = false; + CssTextDecoration effectiveTextDecoration = CssTextDecoration::None; bool effectiveDirectionDefined = false; CssTextDirection effectiveDirection = CssTextDirection::Ltr; bool effectiveSup = false; @@ -101,7 +101,10 @@ class ChapterHtmlSlimParser { void flushPendingAnchor(); void flushPartWordBuffer(); void makePages(); + static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration); static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css); + static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css); + void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle); void emitHorizontalRule(const BlockStyle& blockStyle); // XML callbacks static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); diff --git a/test/epubs/test_text_decorations.epub b/test/epubs/test_text_decorations.epub new file mode 100644 index 0000000000000000000000000000000000000000..aed7fe50445d788d9129d3bdeb705bbffa2c9b75 GIT binary patch literal 2987 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