A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: Fix img layout issue / support CSS display:none for elements and images (#1443)

## Summary
- Add CSS `display: none` support to the EPUB rendering pipeline (fixes
#1431)
- Parse `display` property in stylesheets and inline styles, with full
cascade resolution (element, class, element.class, inline)
- Skip hidden elements and all their descendants in
`ChapterHtmlSlimParser`
- Separate display:none check for `<img>` tags (image code path is
independent of the general element handler)
- Flush pending text blocks before placing images to fix layout ordering
(text preceding an image now correctly renders above it)
- Bump CSS cache version to 4 to invalidate stale caches
- Add test EPUB (`test_display_none.epub`) covering class selectors,
element selectors, combined selectors, inline styles, nested hidden
content, hidden images, style priority/override, and realistic use cases

authored by

jpirnay and committed by
GitHub
ceb6acc8 7d56810e

+130 -16
+74
lib/Epub/Epub/css/CssParser.cpp
··· 52 52 // Check if character is CSS whitespace 53 53 bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; } 54 54 55 + std::string_view stripTrailingImportant(std::string_view value) { 56 + constexpr std::string_view IMPORTANT = "!important"; 57 + 58 + while (!value.empty() && isCssWhitespace(value.back())) { 59 + value.remove_suffix(1); 60 + } 61 + 62 + if (value.size() < IMPORTANT.size()) { 63 + return value; 64 + } 65 + 66 + const size_t suffixPos = value.size() - IMPORTANT.size(); 67 + if (value.substr(suffixPos) != IMPORTANT) { 68 + return value; 69 + } 70 + 71 + value.remove_suffix(IMPORTANT.size()); 72 + while (!value.empty() && isCssWhitespace(value.back())) { 73 + value.remove_suffix(1); 74 + } 75 + return value; 76 + } 77 + 55 78 } // anonymous namespace 56 79 57 80 // String utilities implementation ··· 317 340 style.imageWidth = len; 318 341 style.defined.imageWidth = 1; 319 342 } 343 + } else if (propNameBuf == "display") { 344 + const std::string_view displayValue = stripTrailingImportant(propValueBuf); 345 + style.display = (displayValue == "none") ? CssDisplay::None : CssDisplay::Block; 346 + style.defined.display = 1; 320 347 } 321 348 } 322 349 ··· 692 719 writeLength(style.paddingRight); 693 720 writeLength(style.imageHeight); 694 721 writeLength(style.imageWidth); 722 + file.write(static_cast<uint8_t>(style.display)); 695 723 696 724 // Write defined flags as uint16_t 697 725 uint16_t definedBits = 0; ··· 710 738 if (style.defined.paddingRight) definedBits |= 1 << 12; 711 739 if (style.defined.imageHeight) definedBits |= 1 << 13; 712 740 if (style.defined.imageWidth) definedBits |= 1 << 14; 741 + if (style.defined.display) definedBits |= 1 << 15; 713 742 file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits)); 714 743 } 715 744 ··· 748 777 return false; 749 778 } 750 779 780 + if (ruleCount > MAX_RULES) { 781 + LOG_DBG("CSS", "Invalid cache rule count (%u > %zu)", ruleCount, MAX_RULES); 782 + rulesBySelector_.clear(); 783 + file.close(); 784 + return false; 785 + } 786 + 787 + auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool { 788 + return static_cast<size_t>(file.available()) >= neededBytes; 789 + }; 790 + 791 + constexpr size_t CSS_LENGTH_FIELD_COUNT = 11; 792 + constexpr size_t CSS_LENGTH_BYTES = sizeof(float) + sizeof(uint8_t); 793 + constexpr size_t CSS_FIXED_STYLE_BYTES = 794 + 4 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + sizeof(uint8_t) + sizeof(uint16_t); 795 + 751 796 // Read each rule 752 797 for (uint16_t i = 0; i < ruleCount; ++i) { 753 798 // Read selector string 754 799 uint16_t selectorLen = 0; 800 + if (!hasRemainingBytes(sizeof(selectorLen))) { 801 + rulesBySelector_.clear(); 802 + file.close(); 803 + return false; 804 + } 755 805 if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) { 756 806 rulesBySelector_.clear(); 757 807 file.close(); 758 808 return false; 759 809 } 760 810 811 + if (selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || !hasRemainingBytes(selectorLen)) { 812 + LOG_DBG("CSS", "Invalid selector length in cache: %u", selectorLen); 813 + rulesBySelector_.clear(); 814 + file.close(); 815 + return false; 816 + } 817 + 761 818 std::string selector; 762 819 selector.resize(selectorLen); 763 820 if (file.read(&selector[0], selectorLen) != selectorLen) { 821 + rulesBySelector_.clear(); 822 + file.close(); 823 + return false; 824 + } 825 + 826 + if (!hasRemainingBytes(CSS_FIXED_STYLE_BYTES)) { 827 + LOG_DBG("CSS", "Truncated CSS cache while reading style payload"); 764 828 rulesBySelector_.clear(); 765 829 file.close(); 766 830 return false; ··· 820 884 return false; 821 885 } 822 886 887 + // Read display value 888 + uint8_t displayVal; 889 + if (file.read(&displayVal, 1) != 1) { 890 + rulesBySelector_.clear(); 891 + file.close(); 892 + return false; 893 + } 894 + style.display = static_cast<CssDisplay>(displayVal); 895 + 823 896 // Read defined flags 824 897 uint16_t definedBits = 0; 825 898 if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) { ··· 842 915 style.defined.paddingRight = (definedBits & 1 << 12) != 0; 843 916 style.defined.imageHeight = (definedBits & 1 << 13) != 0; 844 917 style.defined.imageWidth = (definedBits & 1 << 14) != 0; 918 + style.defined.display = (definedBits & 1 << 15) != 0; 845 919 846 920 rulesBySelector_[selector] = style; 847 921 }
+1 -1
lib/Epub/Epub/css/CssParser.h
··· 31 31 class CssParser { 32 32 public: 33 33 // Bump when CSS cache format or rules change; section caches are invalidated when this changes 34 - static constexpr uint8_t CSS_CACHE_VERSION = 3; 34 + static constexpr uint8_t CSS_CACHE_VERSION = 4; 35 35 36 36 explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} 37 37 ~CssParser() = default;
+15 -3
lib/Epub/Epub/css/CssStyle.h
··· 54 54 // Text decoration options 55 55 enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 }; 56 56 57 + // Display options - only None and Block are relevant for e-ink rendering 58 + enum class CssDisplay : uint8_t { Block = 0, None = 1 }; 59 + 57 60 // Bitmask for tracking which properties have been explicitly set 58 61 struct CssPropertyFlags { 59 62 uint16_t textAlign : 1; ··· 71 74 uint16_t paddingRight : 1; 72 75 uint16_t imageHeight : 1; 73 76 uint16_t imageWidth : 1; 77 + uint16_t display : 1; 74 78 75 79 CssPropertyFlags() 76 80 : textAlign(0), ··· 87 91 paddingLeft(0), 88 92 paddingRight(0), 89 93 imageHeight(0), 90 - imageWidth(0) {} 94 + imageWidth(0), 95 + display(0) {} 91 96 92 97 [[nodiscard]] bool anySet() const { 93 98 return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom || 94 99 marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight || 95 - imageWidth; 100 + imageWidth || display; 96 101 } 97 102 98 103 void clearAll() { 99 104 textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0; 100 105 marginTop = marginBottom = marginLeft = marginRight = 0; 101 106 paddingTop = paddingBottom = paddingLeft = paddingRight = 0; 102 - imageHeight = imageWidth = 0; 107 + imageHeight = imageWidth = display = 0; 103 108 } 104 109 }; 105 110 ··· 123 128 CssLength paddingRight; // Padding right 124 129 CssLength imageHeight; // Height for img (e.g. 2em) – width derived from aspect ratio when only height set 125 130 CssLength imageWidth; // Width for img when both or only width set 131 + CssDisplay display = CssDisplay::Block; // display property (Block or None) 126 132 127 133 CssPropertyFlags defined; // Tracks which properties were explicitly set 128 134 ··· 189 195 imageWidth = base.imageWidth; 190 196 defined.imageWidth = 1; 191 197 } 198 + if (base.hasDisplay()) { 199 + display = base.display; 200 + defined.display = 1; 201 + } 192 202 } 193 203 194 204 [[nodiscard]] bool hasTextAlign() const { return defined.textAlign; } ··· 206 216 [[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; } 207 217 [[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; } 208 218 [[nodiscard]] bool hasImageWidth() const { return defined.imageWidth; } 219 + [[nodiscard]] bool hasDisplay() const { return defined.display; } 209 220 210 221 void reset() { 211 222 textAlign = CssTextAlign::Left; ··· 216 227 marginTop = marginBottom = marginLeft = marginRight = CssLength{}; 217 228 paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{}; 218 229 imageHeight = imageWidth = CssLength{}; 230 + display = CssDisplay::Block; 219 231 defined.clearAll(); 220 232 } 221 233 };
+40 -12
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
··· 182 182 centeredBlockStyle.textAlignDefined = true; 183 183 centeredBlockStyle.alignment = CssTextAlign::Center; 184 184 185 + // Compute CSS style for this element early so display:none can short-circuit 186 + // before tag-specific branches emit any content or metadata. 187 + CssStyle cssStyle; 188 + if (self->cssParser) { 189 + cssStyle = self->cssParser->resolveStyle(name, classAttr); 190 + if (!styleAttr.empty()) { 191 + CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr); 192 + cssStyle.applyOver(inlineStyle); 193 + } 194 + } 195 + 196 + // Skip elements with display:none before all fast paths (tables, links, etc.). 197 + if (cssStyle.hasDisplay() && cssStyle.display == CssDisplay::None) { 198 + self->skipUntilDepth = self->depth; 199 + self->depth += 1; 200 + return; 201 + } 202 + 185 203 // Special handling for tables/cells: flatten into per-cell paragraphs with a prefixed header. 186 204 if (strcmp(name, "table") == 0) { 187 205 // skip nested tables ··· 264 282 return; 265 283 } 266 284 285 + // Skip image if CSS display:none 286 + if (self->cssParser) { 287 + CssStyle imgDisplayStyle = self->cssParser->resolveStyle("img", classAttr); 288 + if (!styleAttr.empty()) { 289 + imgDisplayStyle.applyOver(CssParser::parseInlineStyle(styleAttr)); 290 + } 291 + if (imgDisplayStyle.hasDisplay() && imgDisplayStyle.display == CssDisplay::None) { 292 + self->skipUntilDepth = self->depth; 293 + self->depth += 1; 294 + return; 295 + } 296 + } 297 + 267 298 if (!src.empty() && self->imageRendering != 1) { 268 299 LOG_DBG("EHP", "Found image: src=%s", src.c_str()); 269 300 ··· 384 415 LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale); 385 416 } 386 417 418 + // Flush any pending text block so it appears before the image 419 + if (self->partWordBufferIndex > 0) { 420 + self->flushPartWordBuffer(); 421 + } 422 + if (self->currentTextBlock && !self->currentTextBlock->isEmpty()) { 423 + const BlockStyle parentBlockStyle = self->currentTextBlock->getBlockStyle(); 424 + self->startNewTextBlock(parentBlockStyle); 425 + } 426 + 387 427 // Create page for image - only break if image won't fit remaining space 388 428 if (self->currentPage && !self->currentPage->elements.empty() && 389 429 (self->currentPageNextY + displayHeight > self->viewportHeight)) { ··· 511 551 // Skip CSS resolution — we already handled styling for this <a> tag 512 552 self->depth += 1; 513 553 return; 514 - } 515 - } 516 - 517 - // Compute CSS style for this element 518 - CssStyle cssStyle; 519 - if (self->cssParser) { 520 - // Get combined tag + class styles 521 - cssStyle = self->cssParser->resolveStyle(name, classAttr); 522 - // Merge inline style (highest priority) 523 - if (!styleAttr.empty()) { 524 - CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr); 525 - cssStyle.applyOver(inlineStyle); 526 554 } 527 555 } 528 556
test/epubs/test_display_none.epub

This is a binary file and will not be displayed.