From c8ba4fe9734bf53b815de39d7ffd0de0b3319969 Mon Sep 17 00:00:00 2001 From: cottongin Date: Thu, 19 Feb 2026 14:21:31 -0500 Subject: [PATCH] fix: Port upstream CSS-aware image sizing (PR #1002) Parse CSS height/width into CssStyle for images and use aspect-ratio- preserving logic when CSS dimensions are set. Falls back to viewport-fit scaling when no CSS dimensions are present. Includes divide-by-zero guards and viewport clamping with aspect ratio rescaling. - Add imageHeight field to CssStyle/CssPropertyFlags - Parse CSS height declarations into imageHeight - Add imageHeight + width to cache serialization (bump cache v2->v3) - Replace viewport-fit-only image scaling with CSS-aware sizing Co-authored-by: Cursor --- lib/Epub/Epub/css/CssParser.cpp | 14 ++++- lib/Epub/Epub/css/CssStyle.h | 18 ++++-- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 60 +++++++++++++++---- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index d7662917..72f4de32 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -295,6 +295,9 @@ void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& sty style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft = 1; } + } else if (propNameBuf == "height") { + style.imageHeight = interpretLength(propValueBuf); + style.defined.imageHeight = 1; } else if (propNameBuf == "width") { style.width = interpretLength(propValueBuf); style.defined.width = 1; @@ -565,7 +568,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 = 2; +constexpr uint8_t CSS_CACHE_VERSION = 3; constexpr char rulesCache[] = "/css_rules.cache"; bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); } @@ -616,6 +619,8 @@ bool CssParser::saveToCache() const { writeLength(style.paddingBottom); writeLength(style.paddingLeft); writeLength(style.paddingRight); + writeLength(style.imageHeight); + writeLength(style.width); // Write defined flags as uint16_t uint16_t definedBits = 0; @@ -632,6 +637,8 @@ bool CssParser::saveToCache() const { if (style.defined.paddingBottom) definedBits |= 1 << 10; if (style.defined.paddingLeft) definedBits |= 1 << 11; if (style.defined.paddingRight) definedBits |= 1 << 12; + if (style.defined.width) definedBits |= 1 << 13; + if (style.defined.imageHeight) definedBits |= 1 << 14; file.write(reinterpret_cast(&definedBits), sizeof(definedBits)); } @@ -733,7 +740,8 @@ bool CssParser::loadFromCache() { if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) || !readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) || - !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) { + !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) || + !readLength(style.imageHeight) || !readLength(style.width)) { rulesBySelector_.clear(); file.close(); return false; @@ -759,6 +767,8 @@ bool CssParser::loadFromCache() { style.defined.paddingBottom = (definedBits & 1 << 10) != 0; style.defined.paddingLeft = (definedBits & 1 << 11) != 0; style.defined.paddingRight = (definedBits & 1 << 12) != 0; + style.defined.width = (definedBits & 1 << 13) != 0; + style.defined.imageHeight = (definedBits & 1 << 14) != 0; rulesBySelector_[selector] = style; } diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index 2fb4f6ee..f3178f21 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -70,6 +70,7 @@ struct CssPropertyFlags { uint16_t paddingLeft : 1; uint16_t paddingRight : 1; uint16_t width : 1; + uint16_t imageHeight : 1; CssPropertyFlags() : textAlign(0), @@ -85,18 +86,20 @@ struct CssPropertyFlags { paddingBottom(0), paddingLeft(0), paddingRight(0), - width(0) {} + width(0), + imageHeight(0) {} [[nodiscard]] bool anySet() const { return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom || - marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width; + marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width || + imageHeight; } void clearAll() { textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0; marginTop = marginBottom = marginLeft = marginRight = 0; paddingTop = paddingBottom = paddingLeft = paddingRight = 0; - width = 0; + width = imageHeight = 0; } }; @@ -118,7 +121,8 @@ struct CssStyle { CssLength paddingBottom; // Padding after CssLength paddingLeft; // Padding left CssLength paddingRight; // Padding right - CssLength width; // Element width (used for table columns/cells) + CssLength width; // Element width (used for table columns/cells and image sizing) + CssLength imageHeight; // Height for img (e.g. 2em) -- width derived from aspect ratio when only height set CssPropertyFlags defined; // Tracks which properties were explicitly set @@ -181,6 +185,10 @@ struct CssStyle { width = base.width; defined.width = 1; } + if (base.hasImageHeight()) { + imageHeight = base.imageHeight; + defined.imageHeight = 1; + } } [[nodiscard]] bool hasTextAlign() const { return defined.textAlign; } @@ -197,6 +205,7 @@ struct CssStyle { [[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; } [[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; } [[nodiscard]] bool hasWidth() const { return defined.width; } + [[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; } void reset() { textAlign = CssTextAlign::Left; @@ -207,6 +216,7 @@ struct CssStyle { marginTop = marginBottom = marginLeft = marginRight = CssLength{}; paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{}; width = CssLength{}; + imageHeight = CssLength{}; defined.clearAll(); } }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index e8c02cf4..13a33338 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -418,18 +418,58 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (decoder->getDimensions(cachedImagePath, dims)) { LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height); - // Scale to fit viewport while maintaining aspect ratio - int maxWidth = self->viewportWidth; - int maxHeight = self->viewportHeight; - float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f; - float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f; - float scale = (scaleX < scaleY) ? scaleX : scaleY; - if (scale > 1.0f) scale = 1.0f; + int displayWidth = 0; + int displayHeight = 0; + const float emSize = + static_cast(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; + CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{}; + if (!styleAttr.empty()) { + imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr)); + } + const bool hasCssHeight = imgStyle.hasImageHeight(); + const bool hasCssWidth = imgStyle.hasWidth(); - int displayWidth = (int)(dims.width * scale); - int displayHeight = (int)(dims.height * scale); + if (hasCssHeight && dims.width > 0 && dims.height > 0) { + displayHeight = static_cast( + imgStyle.imageHeight.toPixels(emSize, static_cast(self->viewportHeight)) + 0.5f); + if (displayHeight < 1) displayHeight = 1; + displayWidth = + static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); + if (displayWidth > self->viewportWidth) { + displayWidth = self->viewportWidth; + displayHeight = + static_cast(displayWidth * (static_cast(dims.height) / dims.width) + 0.5f); + if (displayHeight < 1) displayHeight = 1; + } + if (displayWidth < 1) displayWidth = 1; + LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight); + } else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) { + displayWidth = static_cast( + imgStyle.width.toPixels(emSize, static_cast(self->viewportWidth)) + 0.5f); + if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth; + if (displayWidth < 1) displayWidth = 1; + displayHeight = + static_cast(displayWidth * (static_cast(dims.height) / dims.width) + 0.5f); + if (displayHeight > self->viewportHeight) { + displayHeight = self->viewportHeight; + displayWidth = + static_cast(displayHeight * (static_cast(dims.width) / dims.height) + 0.5f); + if (displayWidth < 1) displayWidth = 1; + } + if (displayHeight < 1) displayHeight = 1; + LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight); + } else { + int maxWidth = self->viewportWidth; + int maxHeight = self->viewportHeight; + float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f; + float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f; + float scale = (scaleX < scaleY) ? scaleX : scaleY; + if (scale > 1.0f) scale = 1.0f; - LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale); + displayWidth = (int)(dims.width * scale); + displayHeight = (int)(dims.height * scale); + LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale); + } // Create page for image - only break if image won't fit remaining space if (self->currentPage && !self->currentPage->elements.empty() &&