13 Scope and Known Limitations🔗ℹ

svg/svg covers most of static SVG 1.1/2 rendering to a genuinely deep level — full path-data and transform grammars, a real paint model (gradients, patterns, objectBoundingBox, and both able to paint strokes as well as fills), clipping and masking, markers (including paint-order and the context-fill/context-stroke paint keywords), text-as-path-geometry including <textPath> and xml:space="preserve", textLength/lengthAdjust, CSS inline-size/shape-inside text wrapping, <image>, a CSS engine with real specificity and !important (including SVG2’s CSS geometry properties), true (not approximated) group opacity, mix-blend-mode, and essentially the full SVG filter primitive set — feGaussianBlur, feOffset, feColorMatrix, feFlood, feMerge, feComposite, feBlend, feComponentTransfer, feMorphology, feDropShadow, feConvolveMatrix, feDisplacementMap, feImage, feTurbulence, feDiffuseLighting/feSpecularLighting, and feTile. It has been cross-validated pixel-for-pixel against librsvg throughout its own development — except where librsvg itself turned out to have no usable reference to check against (see below) — and against the subset of the Web Platform Tests SVG suite that’s expressible as a static reftest (no live DOM, no script) — 243 such tests, currently passing about 60%, with most of the remainder attributable to the specific gaps below (several of which — full bidirectional text, vertical writing modes, CJK line-breaking, arbitrary-shape text flow — are substantial features genuinely out of scope) rather than to unexplained discrepancies.

Not implemented at all:

  • Animation neither SMIL (<animate>, <animateTransform>, <animateMotion>) nor CSS animations/transitions. This is a static, single-frame renderer; there is no time dimension anywhere in the pipeline. (pict output was built instead, as a different, complementary direction — see Top-Level Rendering.)

  • External/cross-document references a gradient, pattern, marker, or <use> referencing an id defined in a different file. Everything must be defined within the same document.

  • Custom @font-face web fonts only fonts already installed on the system are used; a document that assumes a specific downloaded font for visual consistency will render with a substitute instead.

  • CSS custom properties (var(...)), z-index/CSS isolation, and CSS pseudo-classes/sibling combinators (:hover, :nth-child, +, ~) in <style> stylesheets — a selector using any of these never matches, rather than silently over-matching.

  • Bidirectional text, vertical writing modes (writing-mode: tb-rl/tb-lr), and CJK-aware line-breaking rules text is laid out left-to-right, horizontally, using simple whitespace-delimited word breaks throughout, including within inline-size/shape-inside wrapping.

Implemented with a disclosed, narrower simplification:

  • Percentages resolve correctly against the current (possibly nested) viewport’s own width, height, or diagonal (for orientation-agnostic lengths like r and stroke-width) for geometry properties and stroke-width confirmed against a WPT test using rx: 25% on an ellipse. Elsewhere, parse-length resolves a percentage against an explicit reference length only when the caller provides one (see its #:reference parameter); other call sites that don’t thread a containing-viewport length through still treat a percentage there as a literal number.

  • Gradient spreadMethod="reflect"/"repeat" fall back to "pad"; a pattern’s own tile doesn’t rescale under an ambient dc scale. Gradients and patterns can paint a stroke as well as a fill — racket/draw’s pen% has no gradient/pattern support at all, so this needed a different technique (render the stroke geometry as an alpha mask, fill the same area with the brush, combine) — verified against librsvg across plain/transformed/dashed/pattern-stroke cases, all within 1-3% pixel diff (ordinary curve-antialiasing variance). Pattern x/y (a pure grid translation) is also supported, verified against librsvg with an exact, zero-pixel-difference match — patternTransform specifically (rotation/skew, not just translation) remains unsupported, since it can’t be reduced to the same pixel-domain shift technique. Separately, a pattern’s own fill-opacity/opacity and any transparency within the pattern’s own content (gaps that should let whatever’s behind the pattern show through) are both now handled correctly too, verified against librsvg with an exact match for the transparency case.

  • clip-path’s objectBoundingBox units only work on leaf shapes, not groups; a single <clipPath> combining children with different clip-rule values uses one fill-rule for the whole combined region. maskContentUnits="objectBoundingBox" isn’t implemented. An individual filter primitive’s own explicit x/y/width/height subregion is enforced as a clip (verified against librsvg: an exact, zero-pixel-difference match for a feFlood with an explicit subregion smaller than the canvas) — but the <mask>/<filter> element’s own overall region as a whole still isn’t; content there is still computed across the full canvas rather than a tighter region.

  • A nested <svg>, a <use>-instantiated <symbol>/<svg>, and a <marker> instance all default to overflow: hidden per spec, clipping their content to their own viewport — overflow="visible" disables it. All three verified against librsvg with exact, zero-pixel-difference matches. (Previously none of these were enforced at all.)

  • The CSS paint-order property (reordering fill/stroke/markers) is supported, including markers moving before or between fill and stroke — verified against librsvg with exact, zero-pixel-difference matches for both a fill/stroke reordering case and a markers-before-stroke case.

  • The pathLength attribute is supported: it rescales stroke-dasharray/stroke-dashoffset and <textPath>’s own startOffset (including a percentage startOffset, which is "X% of pathLength," and the pathLength="0" edge case, which per spec means an infinite scale factor). Neither librsvg nor several major browser engines implement pathLength for stroke-dashing at all, so this was verified via hand-derivation and direct pixel checks against WPT’s own reference files instead; all 8 non-tentative pathLength WPT tests pass.

  • The context-fill/context-stroke paint keywords (resolving to whatever fill/stroke was active on the referencing element — most useful so a <marker> can automatically match its path’s own stroke color) are supported for <use>, <marker>, and <pattern> content — verified against librsvg for the marker case with an exact, zero-pixel-difference match.

  • feBlend/mix-blend-mode implement the core SVG1.1 modes plus the per-channel CSS Compositing set (multiply, screen, darken, lighten, overlay, hard-light, color-dodge, color-burn, difference, exclusion) — soft-light and the four HSL-based modes (hue, saturation, color, luminosity) fall back to normal.

  • Per-primitive color-interpolation-filters="sRGB" (opting a single filter primitive out of the default linearRGB color space) isn’t supported; the whole filter chain always runs in linearRGB.

  • A data: URI for <image> must be base64-encoded; the plainer, percent-encoded-text form isn’t decoded. An SVG file can’t itself be used as an <image> source (only raster formats).

  • <textPath> and xml:space="preserve" are both implemented (see Text) — verified against resvg (librsvg has no textPath support at all) for <textPath> specifically, since no other part of this library could serve as that particular cross-check. <textPath>’s path attribute (SVG2’s inline path data, as an alternative to href referencing a separate shape, taking precedence when both are given) is also supported — verified via self-consistency instead, since neither external renderer available during development could validate this specific, fairly recent feature either (the resvg build available was too old to support path= correctly). The one remaining narrow gap: a <tspan> nested inside a <textPath> isn’t given its own distinct styling — the <textPath>’s own resolved font/paint context applies to all of its text uniformly.

  • textLength/lengthAdjust are implemented (both "spacing", the default, and "spacingAndGlyphs") — verified via hand-derivation rather than against librsvg, which was confirmed empirically (and via two independent, long-standing bug reports) not to implement textLength at all. Scoped to a single run’s own direct text content, not combined with multi-value x/y list positioning.

  • CSS inline-size and shape-inside (text wrapping) are implemented for the common horizontal, left-to-right case, including text-align (left/center/right/justify, with the last line of a justified paragraph correctly left unjustified) — verified via hand-derivation, since the actual WPT reference files for this feature depend on a font not installed here. shape-inside is scoped to referencing a plain <rect> specifically (functionally equivalent to inline-size for a rectangle, since its width doesn’t vary by line) — any other referenced shape falls back to not wrapping. Wrapping a <tspan> with its own distinct styling (e.g. a different font-size) within the wrapped flow isn’t supported; its text is still included in the wrap, just using the outer element’s own font. Vertical writing modes, bidi, and CJK line-breaking are out of scope entirely.

  • feConvolveMatrix, feDisplacementMap, feImage, feTurbulence, feDiffuseLighting/feSpecularLighting, and feTile are all implemented, each independently verified given how much they differ in character (see "svg.rkt"’s own notes for exactly what was checked for each — librsvg itself turned out to have no usable feConvolveMatrix support at all in the version available during development, and feTurbulence’s own noise field isn’t expected to match another renderer pixel-for-pixel even when both implementations are correct). Narrower gaps within these six: feTurbulence’s stitchTiles="stitch" always behaves as noStitch; the lighting primitives use simple edge-pixel clamping rather than the spec’s nine distinct edge/corner Sobel-kernel variants (affecting only the outermost 1px border); feImage referencing another element in the same document (rather than a raster image) doesn’t support subregion-based rescaling; and feTile can only tile a primitive that specified an explicit subregion of its own, since this library doesn’t track per-primitive subregions more generally (except for that one narrow case).

None of these are silent: every one is called out in a comment at the relevant place in "svg.rkt" itself, along with what was checked to confirm it’s a real gap rather than a guess (a cross-check against librsvg, a specific failing test, or both) — this section is a summary of those notes, not the primary source for them.