From 6796989247aa1754c076977fb663e2959ed400a7 Mon Sep 17 00:00:00 2001 From: Jake Kenneally Date: Sat, 31 Jan 2026 14:19:28 -0500 Subject: [PATCH] calculate em based on font line height --- lib/Epub/Epub/css/CssParser.cpp | 66 +++++++++---------- lib/Epub/Epub/css/CssParser.h | 2 +- lib/Epub/Epub/css/CssStyle.h | 58 ++++++++++++---- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 33 ++++++---- 4 files changed, 99 insertions(+), 60 deletions(-) diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index 679537e9..8a17f4b8 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -253,15 +253,12 @@ CssTextDecoration CssParser::interpretDecoration(const std::string& val) { return CssTextDecoration::None; } -float CssParser::interpretLength(const std::string& val, const float emSize) { +CssLength CssParser::interpretLength(const std::string& val) { const std::string v = normalized(val); - if (v.empty()) return 0.0f; - - // Determine unit and multiplier - float multiplier = 1.0f; - size_t unitStart = v.size(); + if (v.empty()) return CssLength{}; // Find where the number ends + size_t unitStart = v.size(); for (size_t i = 0; i < v.size(); ++i) { const char c = v[i]; if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') { @@ -273,20 +270,23 @@ float CssParser::interpretLength(const std::string& val, const float emSize) { const std::string numPart = v.substr(0, unitStart); const std::string unitPart = v.substr(unitStart); - // Handle units - if (unitPart == "em" || unitPart == "rem") { - multiplier = emSize; - } else if (unitPart == "pt") { - multiplier = 1.33f; // Approximate pt to px conversion - } - // px is default (multiplier = 1.0) - + // Parse numeric value char* endPtr = nullptr; const float numericValue = std::strtof(numPart.c_str(), &endPtr); + if (endPtr == numPart.c_str()) return CssLength{}; // No number parsed - if (endPtr == numPart.c_str()) return 0.0f; // No number parsed + // Determine unit type (preserve for deferred resolution) + auto unit = CssUnit::Pixels; + if (unitPart == "em") { + unit = CssUnit::Em; + } else if (unitPart == "rem") { + unit = CssUnit::Rem; + } else if (unitPart == "pt") { + unit = CssUnit::Points; + } + // px and unitless default to Pixels - return numericValue * multiplier; + return CssLength{numericValue, unit}; } int8_t CssParser::interpretSpacing(const std::string& val) { @@ -367,28 +367,28 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) { style.decoration = interpretDecoration(propValue); style.defined.decoration = 1; } else if (propName == "text-indent") { - style.indentPixels = interpretLength(propValue); + style.indent = interpretLength(propValue); style.defined.indent = 1; } else if (propName == "margin-top") { - style.marginTop = static_cast(interpretLength(propValue)); + style.marginTop = interpretLength(propValue); style.defined.marginTop = 1; } else if (propName == "margin-bottom") { - style.marginBottom = static_cast(interpretLength(propValue)); + style.marginBottom = interpretLength(propValue); style.defined.marginBottom = 1; } else if (propName == "margin-left") { - style.marginLeft = static_cast(interpretLength(propValue)); + style.marginLeft = interpretLength(propValue); style.defined.marginLeft = 1; } else if (propName == "margin-right") { - style.marginRight = static_cast(interpretLength(propValue)); + style.marginRight = interpretLength(propValue); style.defined.marginRight = 1; } else if (propName == "margin") { // Shorthand: 1-4 values for top, right, bottom, left const auto values = splitWhitespace(propValue); if (!values.empty()) { - const auto top = static_cast(interpretLength(values[0])); - const int16_t right = values.size() >= 2 ? static_cast(interpretLength(values[1])) : top; - const int16_t bottom = values.size() >= 3 ? static_cast(interpretLength(values[2])) : top; - const int16_t left = values.size() >= 4 ? static_cast(interpretLength(values[3])) : right; + const CssLength top = interpretLength(values[0]); + const CssLength right = values.size() >= 2 ? interpretLength(values[1]) : top; + const CssLength bottom = values.size() >= 3 ? interpretLength(values[2]) : top; + const CssLength left = values.size() >= 4 ? interpretLength(values[3]) : right; style.marginTop = top; style.marginRight = right; style.marginBottom = bottom; @@ -396,25 +396,25 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) { style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1; } } else if (propName == "padding-top") { - style.paddingTop = static_cast(interpretLength(propValue)); + style.paddingTop = interpretLength(propValue); style.defined.paddingTop = 1; } else if (propName == "padding-bottom") { - style.paddingBottom = static_cast(interpretLength(propValue)); + style.paddingBottom = interpretLength(propValue); style.defined.paddingBottom = 1; } else if (propName == "padding-left") { - style.paddingLeft = static_cast(interpretLength(propValue)); + style.paddingLeft = interpretLength(propValue); style.defined.paddingLeft = 1; } else if (propName == "padding-right") { - style.paddingRight = static_cast(interpretLength(propValue)); + style.paddingRight = interpretLength(propValue); style.defined.paddingRight = 1; } else if (propName == "padding") { // Shorthand: 1-4 values for top, right, bottom, left const auto values = splitWhitespace(propValue); if (!values.empty()) { - const auto top = static_cast(interpretLength(values[0])); - const int16_t right = values.size() >= 2 ? static_cast(interpretLength(values[1])) : top; - const int16_t bottom = values.size() >= 3 ? static_cast(interpretLength(values[2])) : top; - const int16_t left = values.size() >= 4 ? static_cast(interpretLength(values[3])) : right; + const CssLength top = interpretLength(values[0]); + const CssLength right = values.size() >= 2 ? interpretLength(values[1]) : top; + const CssLength bottom = values.size() >= 3 ? interpretLength(values[2]) : top; + const CssLength left = values.size() >= 4 ? interpretLength(values[3]) : right; style.paddingTop = top; style.paddingRight = right; style.paddingBottom = bottom; diff --git a/lib/Epub/Epub/css/CssParser.h b/lib/Epub/Epub/css/CssParser.h index a1802369..7a847b35 100644 --- a/lib/Epub/Epub/css/CssParser.h +++ b/lib/Epub/Epub/css/CssParser.h @@ -89,7 +89,7 @@ class CssParser { static CssFontStyle interpretFontStyle(const std::string& val); static CssFontWeight interpretFontWeight(const std::string& val); static CssTextDecoration interpretDecoration(const std::string& val); - static float interpretLength(const std::string& val, float emSize = 16.0f); + static CssLength interpretLength(const std::string& val); static int8_t interpretSpacing(const std::string& val); // String utilities diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index b7f5e308..4b3b063f 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -5,6 +5,37 @@ // Text alignment options matching CSS text-align property enum class TextAlign : uint8_t { None = 0, Left = 1, Right = 2, Center = 3, Justify = 4 }; +// CSS length unit types +enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 }; + +// Represents a CSS length value with its unit, allowing deferred resolution to pixels +struct CssLength { + float value = 0.0f; + CssUnit unit = CssUnit::Pixels; + + CssLength() = default; + CssLength(const float v, const CssUnit u) : value(v), unit(u) {} + + // Convenience constructor for pixel values (most common case) + explicit CssLength(const float pixels) : value(pixels) {} + + // Resolve to pixels given the current em size (font line height) + [[nodiscard]] float toPixels(const float emSize) const { + switch (unit) { + case CssUnit::Em: + case CssUnit::Rem: + return value * emSize; + case CssUnit::Points: + return value * 1.33f; // Approximate pt to px conversion + default: + return value; + } + } + + // Resolve to int16_t pixels (for BlockStyle fields) + [[nodiscard]] int16_t toPixelsInt16(const float emSize) const { return static_cast(toPixels(emSize)); } +}; + // Font style options matching CSS font-style property enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 }; @@ -61,21 +92,22 @@ struct CssPropertyFlags { // Represents a collection of CSS style properties // Only stores properties relevant to e-ink text rendering +// Length values are stored as CssLength (value + unit) for deferred resolution struct CssStyle { TextAlign alignment = TextAlign::None; CssFontStyle fontStyle = CssFontStyle::Normal; CssFontWeight fontWeight = CssFontWeight::Normal; CssTextDecoration decoration = CssTextDecoration::None; - float indentPixels = 0.0f; // First-line indent in pixels - int16_t marginTop = 0; // Vertical spacing before block (in pixels) - int16_t marginBottom = 0; // Vertical spacing after block (in pixels) - int16_t marginLeft = 0; // Horizontal spacing left of block (in pixels) - int16_t marginRight = 0; // Horizontal spacing right of block (in pixels) - int16_t paddingTop = 0; // Padding before (in pixels) - int16_t paddingBottom = 0; // Padding after (in pixels) - int16_t paddingLeft = 0; // Padding left (in pixels) - int16_t paddingRight = 0; // Padding right (in pixels) + CssLength indent; // First-line indent (deferred resolution) + CssLength marginTop; // Vertical spacing before block + CssLength marginBottom; // Vertical spacing after block + CssLength marginLeft; // Horizontal spacing left of block + CssLength marginRight; // Horizontal spacing right of block + CssLength paddingTop; // Padding before + CssLength paddingBottom; // Padding after + CssLength paddingLeft; // Padding left + CssLength paddingRight; // Padding right CssPropertyFlags defined; // Tracks which properties were explicitly set @@ -99,7 +131,7 @@ struct CssStyle { defined.decoration = 1; } if (base.defined.indent) { - indentPixels = base.indentPixels; + indent = base.indent; defined.indent = 1; } if (base.defined.marginTop) { @@ -159,9 +191,9 @@ struct CssStyle { fontStyle = CssFontStyle::Normal; fontWeight = CssFontWeight::Normal; decoration = CssTextDecoration::None; - indentPixels = 0.0f; - marginTop = marginBottom = marginLeft = marginRight = 0; - paddingTop = paddingBottom = paddingLeft = paddingRight = 0; + indent = CssLength{}; + marginTop = marginBottom = marginLeft = marginRight = CssLength{}; + paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{}; defined.clearAll(); } }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 0ac10b41..13325df1 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -43,21 +43,28 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib return false; } -// Create a BlockStyle from CSS style properties -BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle) { +// 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 +BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle, const float emSize) { BlockStyle blockStyle; + // Resolve all CssLength values to pixels using the current font's em size + const int16_t marginTopPx = cssStyle.marginTop.toPixelsInt16(emSize); + const int16_t marginBottomPx = cssStyle.marginBottom.toPixelsInt16(emSize); + const int16_t paddingTopPx = cssStyle.paddingTop.toPixelsInt16(emSize); + const int16_t paddingBottomPx = cssStyle.paddingBottom.toPixelsInt16(emSize); + // Vertical: combine margin and padding for top/bottom spacing - blockStyle.marginTop = static_cast(cssStyle.marginTop + cssStyle.paddingTop); - blockStyle.marginBottom = static_cast(cssStyle.marginBottom + cssStyle.paddingBottom); - blockStyle.paddingTop = cssStyle.paddingTop; - blockStyle.paddingBottom = cssStyle.paddingBottom; + blockStyle.marginTop = static_cast(marginTopPx + paddingTopPx); + blockStyle.marginBottom = static_cast(marginBottomPx + paddingBottomPx); + blockStyle.paddingTop = paddingTopPx; + blockStyle.paddingBottom = paddingBottomPx; // Horizontal: store margin and padding separately for layout calculations - blockStyle.marginLeft = cssStyle.marginLeft; - blockStyle.marginRight = cssStyle.marginRight; - blockStyle.paddingLeft = cssStyle.paddingLeft; - blockStyle.paddingRight = cssStyle.paddingRight; + blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize); + blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize); + blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize); + blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize); // Text indent - blockStyle.textIndent = static_cast(cssStyle.indentPixels); + blockStyle.textIndent = cssStyle.indent.toPixelsInt16(emSize); blockStyle.textIndentDefined = cssStyle.defined.indent; return blockStyle; } @@ -244,7 +251,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } self->currentBlockStyle = cssStyle; - self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle)); + self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle, self->renderer.getLineHeight(self->fontId))); self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); self->updateEffectiveInlineStyle(); } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { @@ -277,7 +284,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } self->currentBlockStyle = cssStyle; - self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle)); + self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle, self->renderer.getLineHeight(self->fontId))); self->updateEffectiveInlineStyle(); if (strcmp(name, "li") == 0) {