On this page:
parse-paint
paint-ref
parse-opacity
resolve-gradient-stops
combined-bounding-box

6 Paint and Color🔗ℹ

This group resolves SVG/CSS paint and color values — everything from a fill/stroke attribute’s string down to a color% object or a reference to a paint server (gradient or pattern) to look up elsewhere.

The central function is parse-paint, which recognizes every common CSS color syntax (#rgb, #rrggbb, rgb()/rgba(), hsl()/hsla(), named colors, currentcolor) plus SVG’s url(#id) paint-server references — returning either a color% directly, or a paint-ref struct for callers to resolve against a document’s id table (svg-doc-id-table) themselves, since parse-paint has no access to one on its own.

procedure

(parse-paint s [current-color])

  (or/c (is-a?/c color%) paint-ref? #f)
  s : (or/c string? #f)
  current-color : (or/c (is-a?/c color%) #f) = #f
Parses a fill, stroke, stop-color, or flood-color-style paint value into:

  • #f, for "none" or when s is itself #f;

  • a color%, for #rgb, #rrggbb, rgb(...)/rgba(...), hsl(...)/hsla(...), a named CSS color, or "currentcolor" (which resolves to current-color, or black if current-color is #f);

  • a paint-ref, for a "url(#id) fallback" reference — resolving the reference itself (looking id up and building a gradient or pattern brush from it) is the caller’s job, since parse-paint has no document context to resolve it against. fallback, if the url(...) has a trailing color/keyword after it, is itself already fully parsed (i.e. a color%, #f, or another paint-ref, never a raw string).

Named colors use Racket’s own X11-derived color database, except for 16 CSS1/HTML4 keyword names confirmed to differ from what CSS/SVG actually specify for the same name (most famously "purple", but also "green", "gray", "maroon", and "navy") — those 16 are overridden to their correct CSS values; every other name (including the full CSS Color 4 extended palette) passes through to the color database as-is.

url (...) is recognized whether its argument is unquoted ("url(#id)") or quoted ("url('#id')"/"url(\"#id\")"), and only leading/trailing whitespace around the reference as a whole is stripped — whitespace inside the fragment id itself ("url(' # x ')") is preserved verbatim as part of the id, which will then simply fail to match any real id and fall through to the fallback, since " x" (with a leading space) is a different id from "x".

(parse-paint "#ff0000")               ; (is-a? color%), red
(parse-paint "rgba(0, 0, 0, 0.5)")    ; (is-a? color%), 50%-alpha black
(parse-paint "none")                  ; #f
(parse-paint "url(#grad) blue")       ; (paint-ref "grad" (is-a? color%))

Four colors resolved by parse-paint a hex code, an rgba (...) with alpha, a named color, and hsl (...):

four rectangles: tomato red, a translucent blue-purple, sea green, and a purple from hsl()

struct

(struct paint-ref (id fallback)
    #:extra-constructor-name make-paint-ref)
  id : string?
  fallback : (or/c (is-a?/c color%) paint-ref? #f)
Represents an unresolved url(#id) paint reference, as returned by parse-paint. id is the fragment identifier (without the leading #) to look up in a document’s id table; fallback is the already-parsed paint to fall back to if id doesn’t resolve to a usable gradient or pattern (a missing id, an id that resolves to some other kind of element, or — for a gradient specifically — one with no stops or a degenerate geometry, such as a gradientTransform that collapses it to a single point).

(paint-ref-id (parse-paint "url(#g) red"))       ; "g"
(paint-ref-fallback (parse-paint "url(#g) red")) ; (is-a? color%), red

procedure

(parse-opacity s [default])  (real-in 0 1)

  s : (or/c string? #f)
  default : (real-in 0 1) = 1
Parses an opacity/fill-opacity/stroke-opacity/stop-opacity/ flood-opacity-style value — a bare number or a percentage — clamped to [0 ,1]. Returns default if s is #f or doesn’t parse as a number.

Examples:
> (parse-opacity "0.5")

0.5

> (parse-opacity "50%")

0.5

> (parse-opacity "150%")

1.0

> (parse-opacity #f)

1

procedure

(resolve-gradient-stops node)

  (listof (cons/c (real-in 0 1) (is-a?/c color%)))
  node : any/c
Reads a <linearGradient> or <radialGradient> element’s <stop> children (their offset/stop-color/stop-opacity, from either presentation attributes or an inline style="") into a list of (offset . color) pairs, each color’s alpha already combined with its stop-opacity, and offsets normalized to be non-decreasing (per spec — a <stop> with a smaller offset than the one before it is clamped up to match). If node has no <stop> children of its own, its stops are inherited from whatever element its own href/xlink:href points to (a common real-world pattern: define one gradient’s stops once and reuse them across several gradients that vary only in geometry) — which means this function consults the ambient current-id-table parameter (not itself exported) to resolve that reference, so it needs to be called during rendering (inside render-svg-doc!/render-node!’s dynamic extent) rather than standalone against an arbitrarily-obtained gradient element.

(resolve-gradient-stops
 '(linearGradient ()
   (stop ((offset "0") (stop-color "red")))
   (stop ((offset "1") (stop-color "blue") (stop-opacity "0.5")))))
; (list (cons 0.0 (is-a? color%)) (cons 1.0 (is-a? color%)))

The three stops above, rendered as an actual gradient bar (gold at 0, orange-red at 0.5, purple at 60%-opacity at 1):

a horizontal gradient bar from gold through orange-red to a translucent purple

procedure

(combined-bounding-box paths)

  (list/c real? real? real? real?)
  paths : (listof (is-a?/c dc-path%))
Returns the union bounding box across all of paths, as (list x y w h) (list 0.0 0.0 0.0 0.0) if paths is empty. Used internally to resolve objectBoundingBox-relative gradient/pattern coordinates and clip-path regions against a shape’s own geometry (always the shape’s fill geometry per spec, regardless of whether the paint server is actually being used for fill or stroke); exported since it’s a generally useful thing to compute given any list of dc-path%s, for instance the ones svg-path->dc-paths returns for a multi-subpath shape.

Example:
> (combined-bounding-box (path-data->dc-paths "M0,0 L50,0 L25,50 Z"))

'(0.0 0.0 50.0 50.0)

The same curved shape from earlier, with its bounding box outlined:

a rounded blue shape inside a dashed rectangle marking its bounding box