2 Drawings and Shapes
dxtr edited this page 2026-10-18 16:44:24 +02:00

Drawings and Shapes

Drawings

read-svg returns a drawing:

  • drawing-width and drawing-height are its size: its width and height attributes in px, the view box's size where they are left out, or 100 by 100 without either.
  • drawing-shapes is a simple vector of its shapes in paint order, the first under the rest. Shapes of groups and of use elements are in their place; what SVG does not render where it is, such as the contents of defs or what has display: none, is not there.
  • drawing-view-box is the view box as given, four single-floats #(min-x min-y width height), or nil. The shapes' transforms already map it onto the drawing, so it is there to read, not to apply.

The drawing covers 0, 0 to its width and height, in px. What lies outside is not part of the drawing, and a renderer clips it away.

Shapes

Each path element and basic shape becomes a shape:

Reader What it is
shape-path Its path, in the shape's own coordinates.
shape-transform The transform from the shape's coordinates to the drawing's: its own transform, its groups', and the view box's.
shape-fill, shape-stroke Its paints: nil for none, a colour, or :current-color.
shape-fill-opacity, shape-stroke-opacity From 0 to 1, multiplying the alpha of the paint's colour.
shape-fill-rule :nonzero or :even-odd.
shape-stroke-width The stroke's width, in the shape's own coordinates.
shape-stroke-line-join :miter, :miter-clip, :round or :bevel.
shape-stroke-line-cap :butt, :round or :square.
shape-stroke-miter-limit The ratio of a miter's length to the stroke's width, at least 1.
shape-paint-order :fill-stroke, or :stroke-fill where paint-order puts the stroke under the fill.
shape-id The element's id, or nil.

(shape-bounds shape) returns the box around what the shape paints, in the drawing's coordinates, its stroke included, as four values xmin ymin xmax ymax. The box holds the shape, though it may be larger than the shape is: it allows for the longest miter the miter limit allows. read-svg leaves out a shape that reaches beyond 10^30 from the drawing's origin, so the box always fits single-floats.

A shape whose stroke is nil draws no stroke, whatever its width, and a stroke of no width is nil.

Paints and colours

A colour is four single-floats from 0 to 1, red, green, blue and alpha, in sRGB with straight alpha, not premultiplied, as SVG gives colours. Opacity stays apart from colours: the alpha of a colour is its own, and the shape's opacities multiply it.

(cl-svgread:read-svg "<svg><g opacity='0.5'>
                        <rect width='4' height='4' fill='red' fill-opacity='0.5'/>
                      </g></svg>")
;; Its one shape:  shape-fill => #(1.0 0.0 0.0 1.0),  shape-fill-opacity => 0.25

shape-fill-opacity is the product of the shape's fill-opacity and opacity and of the opacity of every group around it; shape-stroke-opacity likewise with stroke-opacity. The product is exact for a shape painted with a fill or a stroke alone; see Reading SVG for where SVG would fade a layer as a whole.

currentColor

currentColor inherits as itself and takes the colour of the shape it paints, as CSS Color 4 has it. Where an element sets the color property, the shape's paint is that colour:

(cl-svgread:read-svg "<svg><g color='#ff0000'>
                        <rect width='4' height='4' fill='currentColor'/>
                      </g></svg>")
;; Its one shape:  shape-fill => #(1.0 0.0 0.0 1.0)

Where no element sets it, the paint is :current-color, and whoever draws the shape chooses the colour: icons are drawn this way, so that a theme colours them. Rasterizing takes the colour as :current-color, opaque black by default.

Transforms and the view box

A transform is six single-floats #(a b c d e f), in the order of SVG's matrix(a b c d e f), which maps a point x, y to a x + c y + e, b x + d y + f. That is also the order of cl-gmath's mat3x2: column by column, the x axis, the y axis and the origin.

shape-transform includes the view box's mapping, so a shape's points, transformed, land in the drawing's px:

(let* ((drawing (cl-svgread:read-svg
                 "<svg width='32' height='32' viewBox='0 0 16 16'>
                    <rect x='2' y='2' width='4' height='4' transform='translate(1 0)'/>
                  </svg>"))
       (shape (aref (cl-svgread:drawing-shapes drawing) 0)))
  (list (cl-svgread:drawing-width drawing)
        (cl-svgread:drawing-view-box drawing)
        (cl-svgread:shape-transform shape)))
;; => (32.0 #(0.0 0.0 16.0 16.0) #(2.0 0.0 0.0 2.0 2.0 0.0))

The view box doubles everything, and the translate(1 0) becomes 2 px.

Strokes belong in the shape's coordinates

Stroke a shape's path in its own coordinates, with shape-stroke-width, then transform the stroke's outline with shape-transform. A transform that stretches one way more than the other then stretches the stroke with it, as SVG draws it. Rasterizing does this, at the image's resolution.

Threads

A drawing, its shapes and their paths never change once read, and may be shared between threads. Do not modify the arrays they return.