From 9b04c2ec76dccfd94e2cf92cb381604aa68d3c33 Mon Sep 17 00:00:00 2001 From: Jake Kenneally Date: Sun, 8 Feb 2026 16:31:52 -0500 Subject: [PATCH] feat: Add percentage support to CSS properties (#738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Closes #730 **What is the goal of this PR?** - Adds percentage-based value support to CSS properties that accept percentages (padding, margin, text-indent) **What changes are included?** - Adds `Percent` as another CSS unit - Passes the viewport width to `fromCssStyle` so that we can resolve percentage-based values - Adds a fallback of using an emspace for text-indent if we have an unresolvable value for whatever reason ## Additional Context - This was missed in my CSS support feature, and the fallback when we encounter a percentage value is to use px instead. This means 5% (which would be ~30px on the screen) turns into 5px. When percentages are used in `text-indent`, this fallback behavior makes the indent look like a single space character. Whoops! 😬 My test EPUB has been updated [here](https://github.com/jdk2pq/css-test-epub) with percentage based CSS values at the end of the book. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_, Claude Code --- lib/Epub/Epub/blocks/BlockStyle.h | 30 +++++++++++-------- lib/Epub/Epub/css/CssParser.cpp | 4 ++- lib/Epub/Epub/css/CssStyle.h | 17 +++++++++-- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 6 ++-- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h index 63b054c9..a5a616bf 100644 --- a/lib/Epub/Epub/blocks/BlockStyle.h +++ b/lib/Epub/Epub/blocks/BlockStyle.h @@ -64,21 +64,27 @@ struct BlockStyle { // Create a BlockStyle from CSS style properties, resolving CssLength values to pixels // emSize is the current font line height, used for em/rem unit conversion // paragraphAlignment is the user's paragraphAlignment setting preference - static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment) { + static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment, + const uint16_t viewportWidth = 0) { BlockStyle blockStyle; - // Resolve all CssLength values to pixels using the current font's em size - blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize); - blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize); - blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize); - blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize); + const float vw = viewportWidth; + // Resolve all CssLength values to pixels using the current font's em size and viewport width + blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw); + blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw); + blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw); + blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw); - blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize); - blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize); - blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize); - blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize); + blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw); + blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw); + blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw); + blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw); - blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize); - blockStyle.textIndentDefined = cssStyle.hasTextIndent(); + // For textIndent: if it's a percentage we can't resolve (no viewport width), + // leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used + if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) { + blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw); + blockStyle.textIndentDefined = true; + } blockStyle.textAlignDefined = cssStyle.hasTextAlign(); // User setting overrides CSS, unless "Book's Style" alignment setting is selected if (paragraphAlignment == CssTextAlign::None) { diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index d51ebba7..ba187e4c 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -283,6 +283,8 @@ CssLength CssParser::interpretLength(const std::string& val) { unit = CssUnit::Rem; } else if (unitPart == "pt") { unit = CssUnit::Points; + } else if (unitPart == "%") { + unit = CssUnit::Percent; } // px and unitless default to Pixels @@ -518,7 +520,7 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par // Cache serialization // Cache format version - increment when format changes -constexpr uint8_t CSS_CACHE_VERSION = 1; +constexpr uint8_t CSS_CACHE_VERSION = 2; bool CssParser::saveToCache(FsFile& file) const { if (!file) { diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index adbc19e2..b90fa7ab 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -4,7 +4,7 @@ // Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 }; -enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 }; +enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 }; // Represents a CSS length value with its unit, allowing deferred resolution to pixels struct CssLength { @@ -17,21 +17,32 @@ struct CssLength { // Convenience constructor for pixel values (most common case) explicit CssLength(const float pixels) : value(pixels) {} + // Returns true if this length can be resolved to pixels with the given context. + // Percentage units require a non-zero containerWidth to resolve. + [[nodiscard]] bool isResolvable(const float containerWidth = 0) const { + return unit != CssUnit::Percent || containerWidth > 0; + } + // Resolve to pixels given the current em size (font line height) - [[nodiscard]] float toPixels(const float emSize) const { + // containerWidth is needed for percentage units (e.g. viewport width) + [[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const { switch (unit) { case CssUnit::Em: case CssUnit::Rem: return value * emSize; case CssUnit::Points: return value * 1.33f; // Approximate pt to px conversion + case CssUnit::Percent: + return value * containerWidth / 100.0f; default: return value; } } // Resolve to int16_t pixels (for BlockStyle fields) - [[nodiscard]] int16_t toPixelsInt16(const float emSize) const { return static_cast(toPixels(emSize)); } + [[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const { + return static_cast(toPixels(emSize, containerWidth)); + } }; // Font style options matching CSS font-style property diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 6022e0ee..cc0ecfda 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -213,12 +213,12 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } const float emSize = static_cast(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; - const auto userAlignmentBlockStyle = - BlockStyle::fromCssStyle(cssStyle, emSize, static_cast(self->paragraphAlignment)); + const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle( + cssStyle, emSize, static_cast(self->paragraphAlignment), self->viewportWidth); if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { self->currentCssStyle = cssStyle; - auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center); + auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth); headerBlockStyle.textAlignDefined = true; if (self->embeddedStyle && cssStyle.hasTextAlign()) { headerBlockStyle.alignment = cssStyle.textAlign;