Chromium SVG Coordinate Systems
Chromium SVG Coordinate Systems
How Blink tracks transforms and coordinate spaces from the outer <svg> down
to a leaf shape. This is the conceptual difference between HTML layout
(rectangular boxes in CSS pixels) and SVG layout (user units, viewBoxes,
per-element transforms).
The coordinate-space hierarchy
For a deeply nested shape, the full transform chain is:
Shape local (user units)
│ LocalSVGTransform() (the shape's own `transform` attribute)
▼
Parent SVG coords
│ chain of ancestor LocalToSVGParentTransform() up to nearest <svg>
▼
Nearest <svg> viewport coords (user units inside that <svg>)
│ viewBoxToViewTransform (viewBox → viewport)
▼
Nearest <svg> viewport (CSS-px inside that <svg>)
│ if nested, repeat for outer <svg>
▼
Outer <svg> CSS box
│ LocalToBorderBoxTransform() on LayoutSVGRoot
▼
CSS layout tree (HTML border box)
│ standard HTML transforms
▼
ScreenBlink exposes this via SVGElement::LocalCoordinateSpaceTransform(CTMScope),
which JavaScript's getCTM() and getScreenCTM() call:
// third_party/blink/renderer/core/svg/svg_element.h
enum CTMScope {
kNearestViewportScope, // getCTM() — up to nearest <svg>
kScreenScope, // getScreenCTM() — all the way to the screen
kAncestorScope, // getEnclosureList()
};
virtual AffineTransform LocalCoordinateSpaceTransform(CTMScope) const;LayoutSVGRoot — the bridge
LayoutSVGRoot is the single object responsible for translating between CSS
box layout and SVG's internal coordinate space.
// third_party/blink/renderer/core/layout/svg/layout_svg_root.h
class LayoutSVGRoot final : public LayoutReplaced {
public:
void LayoutRoot(const PhysicalRect& content_rect);
const AffineTransform& LocalToBorderBoxTransform() const {
return local_to_border_box_transform_;
}
gfx::RectF ViewBoxRect() const;
gfx::SizeF ViewportSize() const;
};- CSS side:
LayoutSVGRootextendsLayoutReplaced, so CSS treats it like an<img>— it haswidth/height/aspect-ratio, participates in flex/grid/block layout, and has a border box. - SVG side: inside its border box, the
<svg>establishes an SVG viewport in user units.viewBoxandpreserveAspectRatiomap that viewport to the border box.
LocalToBorderBoxTransform() is the pre-composed transform that encodes:
- Translation to the SVG viewport origin within the border box (CSS padding + border offset).
- Scaling + translation from
viewBoxto viewport size, accounting forpreserveAspectRatioalignment (xMidYMid meet,xMinYMin slice, …). - CSS
zoomand device-pixel scale where applicable.
viewBox and preserveAspectRatio
The implementation is in SVGFitToViewBox:
// third_party/blink/renderer/core/svg/svg_fit_to_view_box.h
static AffineTransform ViewBoxToViewTransform(
const gfx::RectF& view_box,
const SVGPreserveAspectRatio&,
const gfx::SizeF& viewport_size);This function is called in at least three places:
LayoutSVGRoot— outer<svg>to border box.LayoutSVGViewportContainer— nested<svg>inside another.LayoutSVGResourcePattern::BuildPatternData()—<pattern>with aviewBox(see paint-servers.md).LayoutSVGResourceMarker—<marker>with aviewBox.
preserveAspectRatio values: none, xMin/xMid/xMax × YMin/YMid/YMax, each
paired with meet (fit fully, letterbox) or slice (fill fully, crop).
Default: xMidYMid meet.
Direct nested viewport measurements
A deterministic Chromium 149 matrix makes several nested-viewport details observable beyond the class diagram:
- Omitted
x/yuse zero. Omitted or explicit-autowidth/heightuse 100% of the parent viewport. Zero and negative extents paint no descendant content, while negative positions remain valid. - The effective mapping order is ancestor transform, the nested
<svg>'s own computed transform,x/yplacement, thenviewBoxmapping. Percentage transforms on the nested element itself use the parent viewport; descendant percentages switch to the child's user-space axes and normalized diagonal. - A non-root
<svg>starts with the SVG user-agentoverflow:hiddenrule. The presentation attribute takes the CSS shorthand's one- or two-value grammar. Blink's coupled computed axes make overflow-xhidden,clip, orscrollclip, whilevisibleorautoleave it open. A fractional viewport edge is byte-identical to an antialiased geometric clip, not a hard pixel mask. - Descendant effects paint before the viewport clip, while a filter on the
nested
<svg>itself wraps that clip. Its remaining same-element mask, opacity, and authored-clip order follows the ordinary SVG effect stack. - In this browser build, authored CSS
x/y/width/heightvalues can compute on a direct inner<svg>without changing its used viewport geometry. The corresponding presentation attributes do change it. <use>targeting an<svg>is a distinct path: use-sitewidthandheightestablish the instance viewport. A 32×16 use instance was byte-identical to the same direct nested viewport and differed from an omitted-size instance by 2,880 pixels at maximum channel delta 233.
These are measured rendering facts, not conclusions drawn only from API names.
LocalToSVGParentTransform()
Every LayoutSVGModelObject carries an AffineTransform mapping its own
coordinate space to its parent's. Sources that contribute:
- The element's
transformattribute. - CSS
transform(SVG 2 merged these with CSS). x/yattributes on elements like<svg>,<use>,<foreignObject>(treated as a translate).viewBox→ viewport for nested<svg>(handled inside the viewport container's transform).animateMotionoffset (SMIL motion path).- Non-scaling-stroke correction (not the element's own transform, but a stroke-time un-scale; see path-geometry.md).
// LayoutSVGModelObject exposes:
virtual AffineTransform LocalSVGTransform() const; // this element's transform
virtual AffineTransform LocalToSVGParentTransform() const; // composedThe painter calls LocalSVGTransform() before recording child paint ops
(via ScopedSVGTransformState), so the composed CTM naturally accumulates
down the tree without requiring a separate property tree traversal.
Percentage length resolution
SVG percentages resolve differently from CSS. A <rect width="50%"> resolves
against the nearest viewport-establishing ancestor — the nearest
<svg> or <symbol>, not the immediate parent. This is handled by
SVGLengthContext:
// third_party/blink/renderer/core/svg/svg_length_context.h
class SVGLengthContext {
public:
explicit SVGLengthContext(const SVGElement* context);
float ValueForLength(const Length&, SVGLengthMode) const;
// …
};SVGLengthMode is one of kWidth, kHeight, kOther (diagonal =
sqrt(w² + h²) / sqrt(2)) — the spec requires different resolution axes for
different attributes.
Pattern & gradient units
patternUnits, patternContentUnits, gradientUnits use one of:
userSpaceOnUse— coordinates are in the user space of the element referencing the paint server.objectBoundingBox— coordinates are fractions of the referencing element's bounding box (sox="0"tox="1"spans the whole shape).
This is resolved in LayoutSVGResourcePattern::BuildPatternData() and the
equivalent for gradients, producing an AffineTransform that is composed
into the shader's local matrix.
Non-scaling stroke
vector-effect: non-scaling-stroke decouples stroke width from the element's
transform. Implementation:
// third_party/blink/renderer/core/layout/svg/layout_svg_shape.cc
if (HasNonScalingStroke()) {
root_transform.Scale(StyleRef().EffectiveZoom())
.PreConcat(NonScalingStrokeTransform());
path = &NonScalingStrokePath();
}The path is pre-transformed into a coordinate space where scale has been factored out, then stroked at the nominal width, then projected back. The result is a stroke whose visual width is independent of the element's transform.
Hit-test coordinate mapping
Hit tests start in CSS-pixel screen space and walk down:
LayoutSVGRoot::NodeAtPoint()applies the inverse ofLocalToBorderBoxTransform()to get SVG viewport coords.LayoutSVGContainer::NodeAtPoint()iterates children in paint order, recursively applying each child'sLocalSVGTransform().Inverse().LayoutSVGShape::NodeAtPoint()tests the transformed location against the cachedPath(fill via winding rule, stroke via the cachedstroke_path_cache_).
pointer-events gates whether fill/stroke count:
auto | none | visiblePainted | visibleFill | visibleStroke | visible | painted | fill | stroke | all.
Source files
| File | Role |
|---|---|
third_party/blink/renderer/core/svg/svg_element.h | LocalCoordinateSpaceTransform, CTMScope |
third_party/blink/renderer/core/svg/svg_fit_to_view_box.h | ViewBoxToViewTransform() |
third_party/blink/renderer/core/svg/svg_preserve_aspect_ratio.h | Alignment / meet-or-slice enum |
third_party/blink/renderer/core/svg/svg_length_context.h | Percentage and unit resolution |
third_party/blink/renderer/core/layout/svg/layout_svg_root.h | Outer <svg>; LocalToBorderBoxTransform |
third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.h | Nested <svg> |
third_party/blink/renderer/core/layout/svg/layout_svg_transformable_container.h | <g transform=…> |
third_party/blink/renderer/core/layout/svg/layout_svg_model_object.h | LocalSVGTransform, LocalToSVGParentTransform |