5 Rasterizing
dxtr edited this page 2026-10-18 17:09:30 +02:00

Rasterizing

The optional system cl-svgread/ft2 draws a drawing into an image, with FreeType's stroker and rasterizer, through cl-ft2:

(asdf:load-system :cl-svgread/ft2)

(multiple-value-bind (image width height)
    (cl-svgread/ft2:rasterize
     (cl-svgread:read-svg "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'>
                             <circle cx='8' cy='8' r='6' fill='currentColor'/>
                           </svg>")
     :scale 1.5 :current-color '(0.9 0.9 0.9 1))
  (list width height (array-dimensions image)
        (loop for channel below 4 collect (aref image 12 (+ (* 4 12) channel)))))
;; => (24 24 (24 96) (230 230 230 255))

rasterize

(rasterize drawing &key (scale 1) width height (current-color '(0 0 0 1)) (alpha :straight)) returns three values: the image, its width and its height in pixels.

  • :scale is how many pixels a unit of the drawing's size takes, such as a user interface's scale: an icon 16 units square at 1.5 fills 24 by 24 pixels. The image is the drawing's size times the scale, rounded up to whole pixels.
  • :width and :height give the image's size in pixels instead. The drawing is drawn at :scale from the top left corner all the same, cut off or with room to spare.
  • :current-color is the colour of the shapes painted with :current-color, an rgba: four reals from 0 to 1, red, green, blue and alpha, as a list or a vector. It is opaque black, CSS's initial colour, by default. Declare a colour of your own as a cl-svgread/ft2:rgba, and the compiler checks what it can of it before rasterize does; a colour that is no rgba is a type-error.
  • :alpha is :straight, as image files hold colours, or :premultiplied, the colours multiplied by their alpha, as GPU blending and filtering want them.

The image

The image is a 2D array of (unsigned-byte 8), one row of the image to each of its rows, the top row first, four octets to a pixel: red, green, blue and alpha. That is the layout a 4-channel cl-atlas atlas takes, and the one Pyssel's make-image takes. Colours are sRGB, composited as SVG composites them, in sRGB, over a transparent background:

(cl-svgread/ft2:rasterize
 (cl-svgread:read-svg "<svg width='4' height='4'>
                         <rect width='4' height='4' fill='red' fill-opacity='0.5'/>
                       </svg>")
 :alpha :premultiplied)
;; A pixel of the image => 128 0 0 128, red at half, premultiplied.

How shapes are drawn

  • Fills are rasterized into coverage by their fill rule.
  • A stroke is made in the shape's own coordinates, scaled up uniformly by the most its transform stretches, so that the stroker works at the image's resolution, then transformed with the shape: a transform that stretches one way more than the other stretches the stroke too, as SVG draws it.
  • SVG's miter join becomes FreeType's fixed miter, which bevels past the miter limit, and miter-clip its variable one, which clips there. FreeType's miter limit is the same number as SVG's.
  • A subpath of no length with round or square caps is drawn as a dot, as SVG draws it; FreeType's stroker would leave it out.
  • Shapes are painted in order, each fill and stroke in the shape's paint order, with its opacity.

Accuracy

FreeType draws curves as short lines, a few hundredths of a pixel inside them. Measured on a filled circle: it loses 2.6 % of its area at a radius of 4 pixels, 1.5 % at 8, 0.6 % at 24 and 0.2 % at 32. Small icons look a touch lighter than a renderer that follows the curves exactly would draw them.

Threads and limits

Each call makes a FreeType library of its own, in a few microseconds, so any number of threads may rasterize at once, sharing drawings.

A shape that lies farther than 8388608 pixels from the image, whose stroke, its caps and miters included, would be made farther than that from its origin, whose transform :scale takes a unit of farther than that, or that holds more than 32767 points, is beyond what FreeType rasterizes, and signals an svg-error in the drawing's terms. A stroke is made at the image's resolution in its shape's own coordinates, so a transform that stretches far more one way than the other takes it out that far sooner.

An image is at most 8388608 pixels, as 4096 by 2048 has, enough for 3840 by 2160: rasterize paints into a buffer of 16 octets a pixel and returns 4 more, so a larger image would take much of a Lisp's heap. One beyond it is refused before anything is made, with an svg-error that gives the largest :scale at which the drawing fits, or, where :width and :height were given, the most pixels they may make.

FreeType's place

FreeType does this work for now: a stroker and rasterizer in Lisp may take its place. FreeType is used only in ft2/freetype.lisp, through a function that fills a path into coverage, one that strokes a path into another, and a macro that makes the FreeType library they use, so that one in Lisp can replace that file and leave the rest as it is.