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: Fixed Image Sizing When No Width is Set (#1002)

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)
When no width is set for an image, the image currently automatically
sets to the width of the page. However, with this fix, the parser will
use the height and aspect ratio of the image to properly set a height
for it. See below example:


Before:

![IMG_8862](https://github.com/user-attachments/assets/64b3b92f-1165-45ca-8bdb-8e69613d9725)

After:

![IMG_8863](https://github.com/user-attachments/assets/5cb99b12-d150-4b37-ae4c-c8a20eb9f3a0)


* **What changes are included?✱
Changes to the CSS parser

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).

---

### 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, Cursor

authored by

DestinySpeaker and committed by
GitHub
5da23eed 388fbf20

+171 -32
+16 -7
lib/Epub/Epub.cpp
··· 339 339 340 340 // Try to load existing cache first 341 341 if (bookMetadataCache->load()) { 342 - if (!skipLoadingCss && !cssParser->hasCache()) { 343 - LOG_DBG("EBP", "Warning: CSS rules cache not found, attempting to parse CSS files"); 344 - // to get CSS file list 345 - if (!parseContentOpf(bookMetadataCache->coreMetadata)) { 346 - LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files"); 347 - // continue anyway - book will work without CSS and we'll still load any inline style CSS 342 + if (!skipLoadingCss) { 343 + // Rebuild CSS cache when missing or when cache version changed (loadFromCache removes stale file) 344 + bool needCssRebuild = !cssParser->hasCache(); 345 + if (cssParser->hasCache() && !cssParser->loadFromCache()) { 346 + needCssRebuild = true; 348 347 } 349 - parseCssFiles(); 348 + if (needCssRebuild) { 349 + LOG_DBG("EBP", "CSS rules cache missing or stale, attempting to parse CSS files"); 350 + if (!parseContentOpf(bookMetadataCache->coreMetadata)) { 351 + LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files"); 352 + // continue anyway - book will work without CSS and we'll still load any inline style CSS 353 + } 354 + parseCssFiles(); 355 + // Invalidate section caches so they are rebuilt with the new CSS 356 + Storage.removeDir((cachePath + "/sections").c_str()); 357 + } 350 358 } 351 359 LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str()); 352 360 return true; ··· 447 455 if (!skipLoadingCss) { 448 456 // Parse CSS files after cache reload 449 457 parseCssFiles(); 458 + Storage.removeDir((cachePath + "/sections").c_str()); 450 459 } 451 460 452 461 LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
+1
lib/Epub/Epub/Section.cpp
··· 4 4 #include <Logging.h> 5 5 #include <Serialization.h> 6 6 7 + #include "Epub/css/CssParser.h" 7 8 #include "Page.h" 8 9 #include "hyphenation/Hyphenator.h" 9 10 #include "parsers/ChapterHtmlSlimParser.h"
+43 -13
lib/Epub/Epub/css/CssParser.cpp
··· 189 189 } 190 190 191 191 CssLength CssParser::interpretLength(const std::string& val) { 192 + CssLength result; 193 + tryInterpretLength(val, result); 194 + return result; 195 + } 196 + 197 + bool CssParser::tryInterpretLength(const std::string& val, CssLength& out) { 192 198 const std::string v = normalized(val); 193 - if (v.empty()) return CssLength{}; 199 + if (v.empty()) { 200 + out = CssLength{}; 201 + return false; 202 + } 194 203 195 - // Find where the number ends 196 204 size_t unitStart = v.size(); 197 205 for (size_t i = 0; i < v.size(); ++i) { 198 206 const char c = v[i]; ··· 205 213 const std::string numPart = v.substr(0, unitStart); 206 214 const std::string unitPart = v.substr(unitStart); 207 215 208 - // Parse numeric value 209 216 char* endPtr = nullptr; 210 217 const float numericValue = std::strtof(numPart.c_str(), &endPtr); 211 - if (endPtr == numPart.c_str()) return CssLength{}; // No number parsed 218 + if (endPtr == numPart.c_str()) { 219 + out = CssLength{}; 220 + return false; // No number parsed (e.g. auto, inherit, initial) 221 + } 212 222 213 - // Determine unit type (preserve for deferred resolution) 214 223 auto unit = CssUnit::Pixels; 215 224 if (unitPart == "em") { 216 225 unit = CssUnit::Em; ··· 221 230 } else if (unitPart == "%") { 222 231 unit = CssUnit::Percent; 223 232 } 224 - // px and unitless default to Pixels 225 233 226 - return CssLength{numericValue, unit}; 234 + out = CssLength{numericValue, unit}; 235 + return true; 227 236 } 237 + 228 238 // Declaration parsing 229 239 230 240 void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& style, std::string& propNameBuf, ··· 295 305 style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft = 296 306 1; 297 307 } 308 + } else if (propNameBuf == "height") { 309 + CssLength len; 310 + if (tryInterpretLength(propValueBuf, len)) { 311 + style.imageHeight = len; 312 + style.defined.imageHeight = 1; 313 + } 314 + } else if (propNameBuf == "width") { 315 + CssLength len; 316 + if (tryInterpretLength(propValueBuf, len)) { 317 + style.imageWidth = len; 318 + style.defined.imageWidth = 1; 319 + } 298 320 } 299 321 } 300 322 ··· 561 583 562 584 // Cache serialization 563 585 564 - // Cache format version - increment when format changes 565 - constexpr uint8_t CSS_CACHE_VERSION = 2; 586 + // Cache file name (version is CssParser::CSS_CACHE_VERSION) 566 587 constexpr char rulesCache[] = "/css_rules.cache"; 567 588 568 589 bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); } ··· 578 599 } 579 600 580 601 // Write version 581 - file.write(CSS_CACHE_VERSION); 602 + file.write(CssParser::CSS_CACHE_VERSION); 582 603 583 604 // Write rule count 584 605 const auto ruleCount = static_cast<uint16_t>(rulesBySelector_.size()); ··· 613 634 writeLength(style.paddingBottom); 614 635 writeLength(style.paddingLeft); 615 636 writeLength(style.paddingRight); 637 + writeLength(style.imageHeight); 638 + writeLength(style.imageWidth); 616 639 617 640 // Write defined flags as uint16_t 618 641 uint16_t definedBits = 0; ··· 629 652 if (style.defined.paddingBottom) definedBits |= 1 << 10; 630 653 if (style.defined.paddingLeft) definedBits |= 1 << 11; 631 654 if (style.defined.paddingRight) definedBits |= 1 << 12; 655 + if (style.defined.imageHeight) definedBits |= 1 << 13; 656 + if (style.defined.imageWidth) definedBits |= 1 << 14; 632 657 file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits)); 633 658 } 634 659 ··· 652 677 653 678 // Read and verify version 654 679 uint8_t version = 0; 655 - if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) { 656 - LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u)", version, CSS_CACHE_VERSION); 680 + if (file.read(&version, 1) != 1 || version != CssParser::CSS_CACHE_VERSION) { 681 + LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u), removing stale cache for rebuild", version, 682 + CssParser::CSS_CACHE_VERSION); 657 683 file.close(); 684 + Storage.remove((cachePath + rulesCache).c_str()); 658 685 return false; 659 686 } 660 687 ··· 730 757 731 758 if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) || 732 759 !readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) || 733 - !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) { 760 + !readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) || 761 + !readLength(style.imageHeight) || !readLength(style.imageWidth)) { 734 762 rulesBySelector_.clear(); 735 763 file.close(); 736 764 return false; ··· 756 784 style.defined.paddingBottom = (definedBits & 1 << 10) != 0; 757 785 style.defined.paddingLeft = (definedBits & 1 << 11) != 0; 758 786 style.defined.paddingRight = (definedBits & 1 << 12) != 0; 787 + style.defined.imageHeight = (definedBits & 1 << 13) != 0; 788 + style.defined.imageWidth = (definedBits & 1 << 14) != 0; 759 789 760 790 rulesBySelector_[selector] = style; 761 791 }
+5
lib/Epub/Epub/css/CssParser.h
··· 30 30 */ 31 31 class CssParser { 32 32 public: 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; 35 + 33 36 explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {} 34 37 ~CssParser() = default; 35 38 ··· 113 116 static CssFontWeight interpretFontWeight(const std::string& val); 114 117 static CssTextDecoration interpretDecoration(const std::string& val); 115 118 static CssLength interpretLength(const std::string& val); 119 + /** Returns true only when a numeric length was parsed (e.g. 2em, 50%). False for auto/inherit/initial. */ 120 + static bool tryInterpretLength(const std::string& val, CssLength& out); 116 121 117 122 // String utilities 118 123 static std::string normalized(const std::string& s);
+21 -2
lib/Epub/Epub/css/CssStyle.h
··· 69 69 uint16_t paddingBottom : 1; 70 70 uint16_t paddingLeft : 1; 71 71 uint16_t paddingRight : 1; 72 + uint16_t imageHeight : 1; 73 + uint16_t imageWidth : 1; 72 74 73 75 CssPropertyFlags() 74 76 : textAlign(0), ··· 83 85 paddingTop(0), 84 86 paddingBottom(0), 85 87 paddingLeft(0), 86 - paddingRight(0) {} 88 + paddingRight(0), 89 + imageHeight(0), 90 + imageWidth(0) {} 87 91 88 92 [[nodiscard]] bool anySet() const { 89 93 return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom || 90 - marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight; 94 + marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight || 95 + imageWidth; 91 96 } 92 97 93 98 void clearAll() { 94 99 textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0; 95 100 marginTop = marginBottom = marginLeft = marginRight = 0; 96 101 paddingTop = paddingBottom = paddingLeft = paddingRight = 0; 102 + imageHeight = imageWidth = 0; 97 103 } 98 104 }; 99 105 ··· 115 121 CssLength paddingBottom; // Padding after 116 122 CssLength paddingLeft; // Padding left 117 123 CssLength paddingRight; // Padding right 124 + CssLength imageHeight; // Height for img (e.g. 2em) – width derived from aspect ratio when only height set 125 + CssLength imageWidth; // Width for img when both or only width set 118 126 119 127 CssPropertyFlags defined; // Tracks which properties were explicitly set 120 128 ··· 173 181 paddingRight = base.paddingRight; 174 182 defined.paddingRight = 1; 175 183 } 184 + if (base.hasImageHeight()) { 185 + imageHeight = base.imageHeight; 186 + defined.imageHeight = 1; 187 + } 188 + if (base.hasImageWidth()) { 189 + imageWidth = base.imageWidth; 190 + defined.imageWidth = 1; 191 + } 176 192 } 177 193 178 194 [[nodiscard]] bool hasTextAlign() const { return defined.textAlign; } ··· 188 204 [[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; } 189 205 [[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; } 190 206 [[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; } 207 + [[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; } 208 + [[nodiscard]] bool hasImageWidth() const { return defined.imageWidth; } 191 209 192 210 void reset() { 193 211 textAlign = CssTextAlign::Left; ··· 197 215 textIndent = CssLength{}; 198 216 marginTop = marginBottom = marginLeft = marginRight = CssLength{}; 199 217 paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{}; 218 + imageHeight = imageWidth = CssLength{}; 200 219 defined.clearAll(); 201 220 } 202 221 };
+85 -10
lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp
··· 257 257 if (decoder && decoder->getDimensions(cachedImagePath, dims)) { 258 258 LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height); 259 259 260 - // Scale to fit viewport while maintaining aspect ratio 261 - int maxWidth = self->viewportWidth; 262 - int maxHeight = self->viewportHeight; 263 - float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f; 264 - float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f; 265 - float scale = (scaleX < scaleY) ? scaleX : scaleY; 266 - if (scale > 1.0f) scale = 1.0f; 260 + int displayWidth = 0; 261 + int displayHeight = 0; 262 + const float emSize = 263 + static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; 264 + CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{}; 265 + // Merge inline style (e.g. style="height: 2em") so it overrides stylesheet rules 266 + if (!styleAttr.empty()) { 267 + imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr)); 268 + } 269 + const bool hasCssHeight = imgStyle.hasImageHeight(); 270 + const bool hasCssWidth = imgStyle.hasImageWidth(); 267 271 268 - int displayWidth = (int)(dims.width * scale); 269 - int displayHeight = (int)(dims.height * scale); 272 + if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) { 273 + // Both CSS height and width set: resolve both, then clamp to viewport preserving requested ratio 274 + displayHeight = static_cast<int>( 275 + imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f); 276 + displayWidth = static_cast<int>( 277 + imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f); 278 + if (displayHeight < 1) displayHeight = 1; 279 + if (displayWidth < 1) displayWidth = 1; 280 + if (displayWidth > self->viewportWidth || displayHeight > self->viewportHeight) { 281 + float scaleX = (displayWidth > self->viewportWidth) 282 + ? static_cast<float>(self->viewportWidth) / displayWidth 283 + : 1.0f; 284 + float scaleY = (displayHeight > self->viewportHeight) 285 + ? static_cast<float>(self->viewportHeight) / displayHeight 286 + : 1.0f; 287 + float scale = (scaleX < scaleY) ? scaleX : scaleY; 288 + displayWidth = static_cast<int>(displayWidth * scale + 0.5f); 289 + displayHeight = static_cast<int>(displayHeight * scale + 0.5f); 290 + if (displayWidth < 1) displayWidth = 1; 291 + if (displayHeight < 1) displayHeight = 1; 292 + } 293 + LOG_DBG("EHP", "Display size from CSS height+width: %dx%d", displayWidth, displayHeight); 294 + } else if (hasCssHeight && !hasCssWidth && dims.width > 0 && dims.height > 0) { 295 + // Use CSS height (resolve % against viewport height) and derive width from aspect ratio 296 + displayHeight = static_cast<int>( 297 + imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f); 298 + if (displayHeight < 1) displayHeight = 1; 299 + displayWidth = 300 + static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f); 301 + if (displayHeight > self->viewportHeight) { 302 + displayHeight = self->viewportHeight; 303 + // Rescale width to preserve aspect ratio when height is clamped 304 + displayWidth = 305 + static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f); 306 + if (displayWidth < 1) displayWidth = 1; 307 + } 308 + if (displayWidth > self->viewportWidth) { 309 + displayWidth = self->viewportWidth; 310 + // Rescale height to preserve aspect ratio when width is clamped 311 + displayHeight = 312 + static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f); 313 + if (displayHeight < 1) displayHeight = 1; 314 + } 315 + if (displayWidth < 1) displayWidth = 1; 316 + LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight); 317 + } else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) { 318 + // Use CSS width (resolve % against viewport width) and derive height from aspect ratio 319 + displayWidth = static_cast<int>( 320 + imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f); 321 + if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth; 322 + if (displayWidth < 1) displayWidth = 1; 323 + displayHeight = 324 + static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f); 325 + if (displayHeight > self->viewportHeight) { 326 + displayHeight = self->viewportHeight; 327 + // Rescale width to preserve aspect ratio when height is clamped 328 + displayWidth = 329 + static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f); 330 + if (displayWidth < 1) displayWidth = 1; 331 + } 332 + if (displayHeight < 1) displayHeight = 1; 333 + LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight); 334 + } else { 335 + // Scale to fit viewport while maintaining aspect ratio 336 + int maxWidth = self->viewportWidth; 337 + int maxHeight = self->viewportHeight; 338 + float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f; 339 + float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f; 340 + float scale = (scaleX < scaleY) ? scaleX : scaleY; 341 + if (scale > 1.0f) scale = 1.0f; 270 342 271 - LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale); 343 + displayWidth = (int)(dims.width * scale); 344 + displayHeight = (int)(dims.height * scale); 345 + LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale); 346 + } 272 347 273 348 // Create page for image - only break if image won't fit remaining space 274 349 if (self->currentPage && !self->currentPage->elements.empty() &&