calculate em based on font line height

This commit is contained in:
Jake Kenneally 2026-01-31 14:19:28 -05:00
parent 9dac5bf27e
commit 6796989247
4 changed files with 99 additions and 60 deletions

View File

@ -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<int16_t>(interpretLength(propValue));
style.marginTop = interpretLength(propValue);
style.defined.marginTop = 1;
} else if (propName == "margin-bottom") {
style.marginBottom = static_cast<int16_t>(interpretLength(propValue));
style.marginBottom = interpretLength(propValue);
style.defined.marginBottom = 1;
} else if (propName == "margin-left") {
style.marginLeft = static_cast<int16_t>(interpretLength(propValue));
style.marginLeft = interpretLength(propValue);
style.defined.marginLeft = 1;
} else if (propName == "margin-right") {
style.marginRight = static_cast<int16_t>(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<int16_t>(interpretLength(values[0]));
const int16_t right = values.size() >= 2 ? static_cast<int16_t>(interpretLength(values[1])) : top;
const int16_t bottom = values.size() >= 3 ? static_cast<int16_t>(interpretLength(values[2])) : top;
const int16_t left = values.size() >= 4 ? static_cast<int16_t>(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<int16_t>(interpretLength(propValue));
style.paddingTop = interpretLength(propValue);
style.defined.paddingTop = 1;
} else if (propName == "padding-bottom") {
style.paddingBottom = static_cast<int16_t>(interpretLength(propValue));
style.paddingBottom = interpretLength(propValue);
style.defined.paddingBottom = 1;
} else if (propName == "padding-left") {
style.paddingLeft = static_cast<int16_t>(interpretLength(propValue));
style.paddingLeft = interpretLength(propValue);
style.defined.paddingLeft = 1;
} else if (propName == "padding-right") {
style.paddingRight = static_cast<int16_t>(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<int16_t>(interpretLength(values[0]));
const int16_t right = values.size() >= 2 ? static_cast<int16_t>(interpretLength(values[1])) : top;
const int16_t bottom = values.size() >= 3 ? static_cast<int16_t>(interpretLength(values[2])) : top;
const int16_t left = values.size() >= 4 ? static_cast<int16_t>(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;

View File

@ -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

View File

@ -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<int16_t>(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();
}
};

View File

@ -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<int16_t>(cssStyle.marginTop + cssStyle.paddingTop);
blockStyle.marginBottom = static_cast<int16_t>(cssStyle.marginBottom + cssStyle.paddingBottom);
blockStyle.paddingTop = cssStyle.paddingTop;
blockStyle.paddingBottom = cssStyle.paddingBottom;
blockStyle.marginTop = static_cast<int16_t>(marginTopPx + paddingTopPx);
blockStyle.marginBottom = static_cast<int16_t>(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<int16_t>(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) {