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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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 =
|
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
|
||||||
1;
|
1;
|
||||||
}
|
}
|
||||||
|
} else if (propNameBuf == "height") {
|
||||||
|
style.imageHeight = interpretLength(propValueBuf);
|
||||||
|
style.defined.imageHeight = 1;
|
||||||
} else if (propNameBuf == "width") {
|
} else if (propNameBuf == "width") {
|
||||||
style.width = interpretLength(propValueBuf);
|
style.width = interpretLength(propValueBuf);
|
||||||
style.defined.width = 1;
|
style.defined.width = 1;
|
||||||
@@ -565,7 +568,7 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par
|
|||||||
// Cache serialization
|
// Cache serialization
|
||||||
|
|
||||||
// Cache format version - increment when format changes
|
// 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";
|
constexpr char rulesCache[] = "/css_rules.cache";
|
||||||
|
|
||||||
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
||||||
@@ -616,6 +619,8 @@ bool CssParser::saveToCache() const {
|
|||||||
writeLength(style.paddingBottom);
|
writeLength(style.paddingBottom);
|
||||||
writeLength(style.paddingLeft);
|
writeLength(style.paddingLeft);
|
||||||
writeLength(style.paddingRight);
|
writeLength(style.paddingRight);
|
||||||
|
writeLength(style.imageHeight);
|
||||||
|
writeLength(style.width);
|
||||||
|
|
||||||
// Write defined flags as uint16_t
|
// Write defined flags as uint16_t
|
||||||
uint16_t definedBits = 0;
|
uint16_t definedBits = 0;
|
||||||
@@ -632,6 +637,8 @@ bool CssParser::saveToCache() const {
|
|||||||
if (style.defined.paddingBottom) definedBits |= 1 << 10;
|
if (style.defined.paddingBottom) definedBits |= 1 << 10;
|
||||||
if (style.defined.paddingLeft) definedBits |= 1 << 11;
|
if (style.defined.paddingLeft) definedBits |= 1 << 11;
|
||||||
if (style.defined.paddingRight) definedBits |= 1 << 12;
|
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<const uint8_t*>(&definedBits), sizeof(definedBits));
|
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -733,7 +740,8 @@ bool CssParser::loadFromCache() {
|
|||||||
|
|
||||||
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
|
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
|
||||||
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
|
!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();
|
rulesBySelector_.clear();
|
||||||
file.close();
|
file.close();
|
||||||
return false;
|
return false;
|
||||||
@@ -759,6 +767,8 @@ bool CssParser::loadFromCache() {
|
|||||||
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
|
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
|
||||||
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
|
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
|
||||||
style.defined.paddingRight = (definedBits & 1 << 12) != 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;
|
rulesBySelector_[selector] = style;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ struct CssPropertyFlags {
|
|||||||
uint16_t paddingLeft : 1;
|
uint16_t paddingLeft : 1;
|
||||||
uint16_t paddingRight : 1;
|
uint16_t paddingRight : 1;
|
||||||
uint16_t width : 1;
|
uint16_t width : 1;
|
||||||
|
uint16_t imageHeight : 1;
|
||||||
|
|
||||||
CssPropertyFlags()
|
CssPropertyFlags()
|
||||||
: textAlign(0),
|
: textAlign(0),
|
||||||
@@ -85,18 +86,20 @@ struct CssPropertyFlags {
|
|||||||
paddingBottom(0),
|
paddingBottom(0),
|
||||||
paddingLeft(0),
|
paddingLeft(0),
|
||||||
paddingRight(0),
|
paddingRight(0),
|
||||||
width(0) {}
|
width(0),
|
||||||
|
imageHeight(0) {}
|
||||||
|
|
||||||
[[nodiscard]] bool anySet() const {
|
[[nodiscard]] bool anySet() const {
|
||||||
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
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() {
|
void clearAll() {
|
||||||
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||||||
marginTop = marginBottom = marginLeft = marginRight = 0;
|
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||||||
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||||||
width = 0;
|
width = imageHeight = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,7 +121,8 @@ struct CssStyle {
|
|||||||
CssLength paddingBottom; // Padding after
|
CssLength paddingBottom; // Padding after
|
||||||
CssLength paddingLeft; // Padding left
|
CssLength paddingLeft; // Padding left
|
||||||
CssLength paddingRight; // Padding right
|
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
|
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||||
|
|
||||||
@@ -181,6 +185,10 @@ struct CssStyle {
|
|||||||
width = base.width;
|
width = base.width;
|
||||||
defined.width = 1;
|
defined.width = 1;
|
||||||
}
|
}
|
||||||
|
if (base.hasImageHeight()) {
|
||||||
|
imageHeight = base.imageHeight;
|
||||||
|
defined.imageHeight = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
|
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
|
||||||
@@ -197,6 +205,7 @@ struct CssStyle {
|
|||||||
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
||||||
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
||||||
[[nodiscard]] bool hasWidth() const { return defined.width; }
|
[[nodiscard]] bool hasWidth() const { return defined.width; }
|
||||||
|
[[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; }
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
textAlign = CssTextAlign::Left;
|
textAlign = CssTextAlign::Left;
|
||||||
@@ -207,6 +216,7 @@ struct CssStyle {
|
|||||||
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||||
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||||
width = CssLength{};
|
width = CssLength{};
|
||||||
|
imageHeight = CssLength{};
|
||||||
defined.clearAll();
|
defined.clearAll();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -418,7 +418,47 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
if (decoder->getDimensions(cachedImagePath, dims)) {
|
if (decoder->getDimensions(cachedImagePath, dims)) {
|
||||||
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
|
||||||
|
|
||||||
// Scale to fit viewport while maintaining aspect ratio
|
int displayWidth = 0;
|
||||||
|
int displayHeight = 0;
|
||||||
|
const float emSize =
|
||||||
|
static_cast<float>(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();
|
||||||
|
|
||||||
|
if (hasCssHeight && dims.width > 0 && dims.height > 0) {
|
||||||
|
displayHeight = static_cast<int>(
|
||||||
|
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
|
||||||
|
if (displayHeight < 1) displayHeight = 1;
|
||||||
|
displayWidth =
|
||||||
|
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
|
||||||
|
if (displayWidth > self->viewportWidth) {
|
||||||
|
displayWidth = self->viewportWidth;
|
||||||
|
displayHeight =
|
||||||
|
static_cast<int>(displayWidth * (static_cast<float>(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<int>(
|
||||||
|
imgStyle.width.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
|
||||||
|
if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth;
|
||||||
|
if (displayWidth < 1) displayWidth = 1;
|
||||||
|
displayHeight =
|
||||||
|
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
|
||||||
|
if (displayHeight > self->viewportHeight) {
|
||||||
|
displayHeight = self->viewportHeight;
|
||||||
|
displayWidth =
|
||||||
|
static_cast<int>(displayHeight * (static_cast<float>(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 maxWidth = self->viewportWidth;
|
||||||
int maxHeight = self->viewportHeight;
|
int maxHeight = self->viewportHeight;
|
||||||
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
|
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
|
||||||
@@ -426,10 +466,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
float scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
if (scale > 1.0f) scale = 1.0f;
|
if (scale > 1.0f) scale = 1.0f;
|
||||||
|
|
||||||
int displayWidth = (int)(dims.width * scale);
|
displayWidth = (int)(dims.width * scale);
|
||||||
int displayHeight = (int)(dims.height * scale);
|
displayHeight = (int)(dims.height * scale);
|
||||||
|
|
||||||
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, 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
|
// Create page for image - only break if image won't fit remaining space
|
||||||
if (self->currentPage && !self->currentPage->elements.empty() &&
|
if (self->currentPage && !self->currentPage->elements.empty() &&
|
||||||
|
|||||||
Reference in New Issue
Block a user