メインコンテンツへスキップ

Chromium SVG Path Geometry and Stroking

How <path d="..."> data becomes an SkPath, and how SVG stroke properties (stroke-width, stroke-linecap, stroke-dasharray, …) are mapped to Skia.

Path data parsing

Byte stream representation

SVGPath stores parsed path data in an SVGPathByteStream — a compact binary format that represents each segment as a command byte plus its float parameters. This is the canonical internal representation; the ASCII d="M 10 10 L 50 50" string is parsed once and cached.

// third_party/blink/renderer/core/svg/svg_path.h
class SVGPath final : public SVGPropertyBase {
public:
const SVGPathByteStream& ByteStream() const;
SVGPath* Clone() const;
String ValueAsString() const override;
SVGParsingError SetValueAsString(const String&);
};

Parser — consumer pattern

The parser is a template-based producer/consumer:

// third_party/blink/renderer/core/svg/svg_path_parser.h
namespace svg_path_parser {
template <typename SourceType, typename ConsumerType>
inline bool ParsePath(SourceType& source, ConsumerType& consumer) {
while (source.HasMoreData()) {
PathSegmentData segment = source.ParseSegment();
if (segment.command == kPathSegUnknown) return false;
consumer.EmitSegment(segment);
}
return true;
}
}

Sources: SVGPathStringSource (ASCII d=), SVGPathByteStreamSource (compact binary).

Consumers:

  • SVGPathByteStreamBuilder — writes into a new byte stream.
  • SVGPathNormalizer — converts relative commands to absolute; keeps arcs.
  • SVGPathStringBuilder — serializes back to ASCII.
  • SVGPathBuilderthe renderer consumer: builds a Path (SkPath wrapper) by emitting moveTo, lineTo, cubicTo, etc.
  • SVGMarkerDataBuilder — walks to compute marker positions (see resources-and-effects.md).
  • SVGPathAbsolutizer — variant of normalizer.

Arcs (A/a command) are typically converted to cubic Béziers at build time via the standard endpoint-to-center-parameterization + arc-to-cubic decomposition.

Path — the Skia wrapper

Path (third_party/blink/renderer/platform/graphics/path.h) is Blink's wrapper around SkPath. It adds helpers that SVG needs:

  • BoundingRect(), StrokeBoundingRect(const StrokeData&)
  • Contains(const gfx::PointF&, WindRule) — point-in-path hit testing
  • StrokeContains(const gfx::PointF&, const StrokeData&)
  • ApplyTransform(const AffineTransform&)

Shapes build their Path lazily via SVGGeometryElement::AsPath(), which dispatches to element-specific construction:

// third_party/blink/renderer/core/layout/svg/layout_svg_shape.cc
void LayoutSVGShape::CreatePath() {
if (!path_)
path_ = std::make_unique<Path>();
*path_ = To<SVGGeometryElement>(GetElement())->AsPath();
DCHECK(!stroke_path_cache_);
}

<rect>, <circle>, <ellipse>, <line>, <polygon>, <polyline> each build their own Path directly (e.g., SVGRectElement::AsPath() constructs a rectangle, or a rounded rect if rx/ry are set). <path> replays the byte stream through SVGPathBuilder.

Fill rule and winding

fill-rule: nonzero | evenodd (and clip-rule for clip paths) maps directly to Skia's SkPathFillType::kWinding / kEvenOdd. The fill rule is not stored on the SkPath itself when it's used for stroking — it's only applied at fill time.

Stroke

Stroke properties mapping

// third_party/blink/renderer/core/layout/svg/svg_layout_support.h
static void ApplyStrokeStyleToStrokeData(StrokeData&,
const ComputedStyle&,
const LayoutObject&,
float dash_scale_factor);

StrokeData (platform/graphics/stroke_data.h) maps as follows:

SVG / CSS propertyStrokeData fieldSkia / SkPaint equivalent
stroke-widththickness_SkPaint::setStrokeWidth
stroke-linecapline_cap_SkPaint::Cap — Butt / Round / Square
stroke-linejoinline_join_SkPaint::Join — Miter / Round / Bevel
stroke-miterlimitmiter_limit_SkPaint::setStrokeMiter
stroke-dasharraydash_SkDashPathEffect::Make(intervals, …)
stroke-dashoffsetphase arg of dash effectsame

Dash scaling for transforms

// layout_svg_shape.cc
StrokeData stroke_data;
SVGLayoutSupport::ApplyStrokeStyleToStrokeData(stroke_data, StyleRef(), *this,
DashScaleFactor());

DashScaleFactor() accounts for uniform scale components of the element's transform so that stroke-dasharray intervals remain visually consistent when the shape is scaled. For non-uniform scales, the approximation can diverge from the spec.

Non-scaling stroke

vector-effect: non-scaling-stroke un-scales the path before stroking. See coordinate-systems.md.

Stroke-path cache

// layout_svg_shape.h
mutable std::unique_ptr<Path> stroke_path_cache_;

The actual stroked Path (result of applying stroke width to the geometry) is cached for hit testing — computing a stroke outline is expensive, so hit tests reuse it across pointer events until the geometry, transform, or stroke properties change.

Stroke bounds

// layout_svg_shape.cc
gfx::RectF LayoutSVGShape::StrokeBoundingBox() const {
if (!StyleRef().HasStroke() || IsShapeEmpty())
return fill_bounding_box_;
if (!HasPath()) {
DCHECK(CanUseSimpleStrokeApproximation(geometry_type_));
return ApproximateStrokeBoundingBox(fill_bounding_box_);
}
StrokeData stroke_data;
SVGLayoutSupport::ApplyStrokeStyleToStrokeData(stroke_data, StyleRef(),
*this, DashScaleFactor());
DashArray dashes;
stroke_data.SetLineDash(dashes, 0); // dashes don't affect bounds per spec
const gfx::RectF stroke_bounds = GetPath().StrokeBoundingRect(stroke_data);
return gfx::UnionRects(fill_bounding_box_, stroke_bounds);
}

Two fast paths:

  • Empty shape → fill bbox only (no stroke).
  • Simple geometry (rect, circle, ellipse, line) where bounds can be computed analytically → ApproximateStrokeBoundingBox() inflates fill bbox by ~stroke-width / 2 * miter_factor.

Otherwise, Skia computes stroke bounds from the actual stroke outline.

Dashes are explicitly cleared before computing bounds — the SVG spec says bounds should reflect the un-dashed stroke envelope, not the gap regions.

Shape rendering modes

shape-rendering: auto | optimizeSpeed | crispEdges | geometricPrecision maps to Skia anti-aliasing and hinting:

  • crispEdges — disable antialiasing (SkPaint::setAntiAlias(false)).
  • optimizeSpeed — implementation-defined; Blink treats as crispEdges when beneficial.
  • geometricPrecision, auto — full anti-aliasing.

Hit testing fill and stroke

// layout_svg_shape.cc
bool LayoutSVGShape::ShapeDependentFillContains(
const HitTestLocation& location,
const WindRule fill_rule) const {
return location.Intersects(GetPath(), fill_rule);
}

bool LayoutSVGShape::ShapeDependentStrokeContains(
const HitTestLocation& location) {
if (!stroke_path_cache_) {
const Path* path = path_.get();
AffineTransform root_transform;
if (HasNonScalingStroke()) {
root_transform.Scale(StyleRef().EffectiveZoom())
.PreConcat(NonScalingStrokeTransform());
path = &NonScalingStrokePath();
}
StrokeData stroke_data;
SVGLayoutSupport::ApplyStrokeStyleToStrokeData(
stroke_data, StyleRef(), *this, DashScaleFactor());
stroke_path_cache_ = std::make_unique<Path>(
path->StrokePath(stroke_data, root_transform));
}
return stroke_path_cache_->Contains(location.TransformedPoint());
}

Hit testing follows pointer-events to decide whether to test fill, stroke, or both.

Source files

FileRole
core/svg/svg_path.hParsed path wrapper; byte stream
core/svg/svg_path_parser.hTemplate parser; consumer pattern
core/svg/svg_path_byte_stream.hCompact binary representation
core/svg/svg_path_builder.hByte stream → Path (SkPath) construction
core/svg/svg_path_string_source.hASCII d= tokenizer
core/svg/svg_path_normalizer.hRelative → absolute, arc-to-cubic
core/layout/svg/layout_svg_shape.hShape base; owns Path, stroke_path_cache_
core/layout/svg/svg_layout_support.hApplyStrokeStyleToStrokeData()
platform/graphics/path.hBlink Path wrapper around SkPath
platform/graphics/stroke_data.hStroke properties value object