Skip to content

Fix intrinsic sizing for SVG elements - #736

Open
nicoburns wants to merge 2 commits into
mainfrom
devin/1786973137-svg-intrinsic-sizing
Open

Fix intrinsic sizing for SVG elements#736
nicoburns wants to merge 2 commits into
mainfrom
devin/1786973137-svg-intrinsic-sizing

Conversation

@nicoburns

@nicoburns nicoburns commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

Fixes CSS intrinsic sizing for SVG per SVG2 "Sizing SVG in CSS" / css-images-3, closing the gaps left by #726/#730/#732:

  • Viewport-relative units (vw/vh/vmin/vmax) on the root width/height contribute no intrinsic dimension (like percentages). They fail to parse as svgtypes::Length, so SvgIntrinsicDimensions now simply records them as absent; intrinsic dims are resolved from the declared length via a new resolve_absolute helper (usvg unit factors) instead of trusting usvg::Tree::size().
  • Zero/negative root width/height: a zero intrinsic dimension (matching browsers), and rendering of the SVG content is disabled (SvgImageData::rendering_disabled(), checked in both draw_svg and background painting alongside degenerate_view_box). usvg refuses to parse non-positive sizes, so the offending attributes are blanked out (via their roxmltree byte ranges) before building the tree, keeping the image usable when CSS sizes it.
  • No tree-size ratio leak: aspect_ratio() now returns Option<f32> — declared width/height ratio only when both are positive absolute lengths, else the viewBox ratio, else None. The old fallback to usvg::Tree::size() ratio (and the layout fallback feeding tree size as intrinsic dims) is gone; layout now feeds IntrinsicSizes { width, height, ratio } straight from the accessors with the standard 300×150 default object size.
  • Painting via default sizing: draw_svg previously hardcoded object-fit: contain against the usvg tree size. It now resolves the natural object size with concrete_object_size(default = content box) (so a dimensionless SVG fills its box) and honors the actual object-fit property.
  • Replaced layout: when only one natural dimension exists, the missing one now transfers through the aspect-ratio property (converting through box-sizing), not just the intrinsic ratio:
// packages/blitz-dom/src/layout/replaced.rs
height = intrinsic_ratio.map(|r| w / r)
    .or(style_ratio.map(|r| (w + box_sizing_adj.width) / r - box_sizing_adj.height))
    .unwrap_or(default_size.height)
  • Inline <svg> painting is now viewport-aware: the CSS box is the SVG viewport, so percentage lengths inside the SVG and the viewBox/preserveAspectRatio mapping must resolve against it. SvgImageData::tree_for_viewport(w, h) re-parses the (cached) source with the root width/height overridden to the box size, and inline painting renders that tree at scale * effective_zoom (zoom scales the CSS box but not user units). SVG-as-image without a viewBox uses the same path with the concrete object size; with a viewBox the plain tree-size scale is kept.
  • UA stylesheet: added @namespace svg and svg|svg { overflow: hidden } per the SVG2 UA sheet (the bare svg selector was matching the HTML default namespace, so inline SVG overflow was never clipped).

Inline <svg> width/height attrs map to CSS width/height as presentational hints in stylo.rs; resolved_width/resolved_height are unused by layout.

WPT results (local per-test comparison vs base 06c4a704, WPT_DIR=~/repos/wpt)

Suite main this PR
css/css-backgrounds 471 471
css/css-images 135 135
css/css-sizing 321 328
css/CSS2 3862 3892
css/css-transforms 413 430
css/css-overflow 151 152
css/filter-effects 155 155
css/css-masking 164 164
svg 84 86

Net +58 / -1 across these suites. The earlier CI-reported replaced/filter/mask regressions came from the first pushed revision (inline SVG was sized via presentational hints but still painted with the old tree-size mapping); the viewport-aware painting above fixes all of them.

The single remaining regression is svg/render/reftests/nested-svg-overflow-clip.html: it passed on main only by accident (usvg sized the tree to its 250px content and blitz scaled it down into the 100px box). usvg flattens nested <svg> without implementing its viewport clipping, so fixing it properly requires upstream usvg support.

cargo test --workspace, cargo fmt, and cargo clippy --workspace are clean; unit tests cover viewport units, zero/negative dimensions, and the no-viewBox no-ratio case.

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/961449e5b9814de886799ea728946822
Requested by: @nicoburns

WPT results

56 newly passing, 1 newly failing (net +55).

Full diff (57 changed tests)
+ Fail => Pass css/CSS2/floats-clear/float-replaced-height-006.xht
+ Fail => Pass css/CSS2/floats-clear/float-replaced-width-007.xht
+ Fail => Pass css/CSS2/inline-svg-intrinsic-size-100-percent-2.html
+ Fail => Pass css/CSS2/normal-flow/block-replaced-height-006.xht
+ Fail => Pass css/CSS2/normal-flow/block-replaced-width-002.xht
+ Fail => Pass css/CSS2/normal-flow/inline-block-replaced-height-006.xht
+ Fail => Pass css/CSS2/normal-flow/inline-block-replaced-height-009.xht
+ Fail => Pass css/CSS2/normal-flow/inline-block-replaced-width-002.xht
+ Fail => Pass css/CSS2/normal-flow/inline-block-replaced-width-007.xht
+ Fail => Pass css/CSS2/normal-flow/inline-block-replaced-width-008.xht
+ Fail => Pass css/CSS2/normal-flow/inline-replaced-height-006.xht
+ Fail => Pass css/CSS2/normal-flow/inline-replaced-height-009.xht
+ Fail => Pass css/CSS2/normal-flow/inline-replaced-width-002.xht
+ Fail => Pass css/CSS2/normal-flow/inline-replaced-width-008.xht
+ Fail => Pass css/CSS2/normal-flow/inline-replaced-width-009.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-height-006.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-height-013.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-height-020.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-height-027.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-height-034.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-002.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-003a.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-003c.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-009.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-023.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-030.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-037.xht
+ Fail => Pass css/CSS2/positioning/absolute-replaced-width-051.xht
+ Fail => Pass css/CSS2/visudet/replaced-elements-max-width-40.html
+ Fail => Pass css/CSS2/visudet/replaced-elements-width-40.html
- Pass => Fail css/compositing/mix-blend-mode/mix-blend-mode-svg.html
+ Fail => Pass css/css-flexbox/svg-root-as-flex-item-002.html
+ Fail => Pass css/css-overflow/overflow-img-svg.html
+ Fail => Pass css/css-sizing/aspect-ratio/replaced-element-004.html
+ Fail => Pass css/css-sizing/aspect-ratio/replaced-element-010.html
+ Fail => Pass css/css-sizing/image-fractional-height-with-wide-aspect-ratio.html
+ Fail => Pass css/css-sizing/intrinsic-size-fallback-replaced.html
+ Fail => Pass css/css-sizing/svg-intrinsic-size-002.html
+ Fail => Pass css/css-sizing/svg-intrinsic-size-003.html
+ Fail => Pass css/css-sizing/svg-intrinsic-size-008.html
+ Fail => Pass css/css-transforms/css-skew-002.html
+ Fail => Pass css/css-transforms/gradientTransform/svg-gradientTransform-combination-001.html
+ Fail => Pass css/css-transforms/group/svg-transform-group-008.html
+ Fail => Pass css/css-transforms/group/svg-transform-group-011.html
+ Fail => Pass css/css-transforms/group/svg-transform-nested-008.html
+ Fail => Pass css/css-transforms/group/svg-transform-nested-013.html
+ Fail => Pass css/css-transforms/group/svg-transform-nested-018.html
+ Fail => Pass css/css-transforms/group/svg-transform-nested-021.html
+ Fail => Pass css/css-transforms/group/svg-transform-nested-029.html
+ Fail => Pass css/css-transforms/scale/svg-scale-006.html
+ Fail => Pass css/css-transforms/scale/svg-scale-007.html
+ Fail => Pass css/css-transforms/skewX/svg-skewx-001.html
+ Fail => Pass css/css-transforms/skewX/svg-skewx-006.html
+ Fail => Pass css/css-transforms/skewX/svg-skewx-011.html
+ Fail => Pass css/css-transforms/skewX/svg-skewx-016.html
+ Fail => Pass css/css-transforms/skewX/svg-skewx-021.html
+ Fail => Pass css/css-transforms/translate/svg-translate-with-units.html

Generated by the WPT workflow.

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

- Treat viewport-relative units (vw/vh/vmin/vmax) on the root svg
  width/height as non-intrinsic, like percentages
- Track zero/negative root width/height attributes: they are a zero
  intrinsic dimension and disable rendering of the SVG content, while
  the usvg tree is still built (with the degenerate attributes blanked
  out) so CSS-sized boxes can exist
- Remove the usvg tree-size fallback from intrinsic dimensions and
  aspect ratio: without a viewBox, the intrinsic ratio exists only when
  both width and height are absolute lengths
- Resolve SVG object-fit painting via the CSS default sizing algorithm
  with the content box as the default object size, and honor the actual
  object-fit property instead of hardcoding contain
- Transfer a missing natural dimension through the aspect-ratio
  property (accounting for box-sizing) in replaced layout
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1786973137-svg-intrinsic-sizing branch from fce3b7d to 0b81519 Compare August 17, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant