9 Raster Backend
dxtr edited this page 2026-10-19 19:21:23 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Raster Backend

The optional system cl-textrender/raster draws layouts and glyphs on the CPU with FreeType, through cl-ft2. It is for debugging, for images, and for small text where an outline-based GPU renderer looks soft. The glyph atlas builds on it.

(asdf:load-system :cl-textrender/raster)

(tr:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 16)
  (let ((layout (tr:layout-text "Hi" font))
        (r (cl-textrender/raster:make-rasterizer)))
    (array-dimensions (cl-textrender/raster:render-layout r layout :scale 2))))
;; => (38 33)

The image is 38 rows by 33 columns: the layout, 16.48 by 18.625 layout units, at 2 pixels per layout unit, rounded up.

Rasterizers

Make a rasterizer with make-rasterizer:

(cl-textrender/raster:make-rasterizer &key (hinting :light))

A rasterizer is a plain Lisp object. What it makes, it keeps with the faces and fonts it draws, and that goes with them, or with the rasterizer once the collector takes it (see Fonts below), so it holds nothing foreign and needs no releasing. Make one and keep it for the life of the program.

hinting is :light or :none:

  • :light, the default, is FreeType's light hinting: glyphs are aligned to the pixel grid vertically only, which sharpens horizontal stems and the baseline without changing advances. The layout's unhinted positions stay right.
  • :none turns hinting off: glyphs are exactly their outlines, scaled.

Full hinting is not offered, because it changes advances and would no longer match the layout. rasterizer-hinting returns the setting.

Coverage

Everything this system draws is coverage: a 2D array of octets indexed by row, then column, 0 where the glyph does not cover a pixel and 255 where it covers it fully. Composite it yourself, in any color: coverage is the alpha of the text's color.

Drawing a layout

(cl-textrender/raster:render-layout rasterizer layout &key (scale 1))

render-layout returns the whole layout as one coverage image, rows from the layout's top: layout-height by layout-width layout units, times scale, rounded up. Glyphs are combined by taking the larger coverage. Ink outside the layout's box, such as hanging spaces or a descender of the last line that reaches past the line box, is clipped.

(cl-textrender/raster:map-layout-glyphs function rasterizer layout
                                       &key (scale 1) (x 0) (y 0))

map-layout-glyphs rasterizes each glyph and calls function with:

  1. the glyph's coverage, as rasterize-glyph returns it;
  2. the pixel column of the coverage's top-left corner, a whole number;
  3. the pixel row of that corner, a whole number;
  4. the cl-textrender:glyph it came from, for its font, style data and so on.

The layout is drawn at scale pixels per layout unit with its top left at pixel (x, y), which may be fractional. The glyph atlas has a map-layout-glyphs of the same shape, which gives regions of a texture instead of coverage.

Compositing is yours, so text can take each span's color:

(cl-textrender/raster:map-layout-glyphs
 (lambda (coverage x y glyph)
   (blend coverage x y (tr:glyph-data glyph)))    ; BLEND is yours
 r layout :scale 2)

At scale 1, "Hi" calls function twice: the H's 12-row by 10-column coverage at column 1, row 3, and the i's 12 by 2 at column 13, row 3. Glyphs without ink, such as spaces, are skipped. Positions may be negative, or past the layout's size, for ink that strays out of it. Recipes has a complete example.

Positions and scale

scale is pixels per layout unit: a font of size 16 at scale 2 draws 32-pixel text. Each glyph's x is rounded to the nearest quarter of a pixel and its y to the nearest pixel:

  • Subpixel positioning. The glyph is drawn at its quarter of a pixel right of the pixel boundary, so spacing stays even, and the bitmap is cached per quarter.
  • No allocation. Once every glyph of a layout has been drawn, drawing it again allocates nothing.
  • Rows grow downwards from the layout's top, whether it was laid out :y-axis :down or :up. An :up layout renders the same image.
  • Rotated glyphs in vertical text are drawn turned 90° clockwise.

Drawing one glyph

(cl-textrender/raster:rasterize-glyph rasterizer font glyph-id
                                      &key (scale 1) (subpixel 0) rotated)

rasterize-glyph draws one glyph and returns three values: its coverage, and the left and top offsets, in pixels, of the coverage's top-left corner from the glyph's origin, with top growing upwards:

(cl-textrender/raster:rasterize-glyph r font (tr:font-glyph-id font #\H))
;; => a 12 by 10 coverage array, 1, 12

The H's image starts one pixel right of its origin and twelve above it. To draw a glyph whose origin is at pixel (x, y), put the coverage's top-left corner at (x + left, y − top). subpixel is the origin's offset in quarters of a pixel, 0 to 3, and rotated turns the glyph 90° clockwise.

The result is cached, and the same array is returned on later calls without allocating: do not modify it. (rasterizer-glyph-count rasterizer font) returns how many glyph images the rasterizer keeps of a font. A font keeps every glyph, scale, quarter and rotation drawn while it lives.

rasterize-glyph-afresh takes the same arguments and draws the same image, into a new array the rasterizer does not keep, for a cache of your own: the glyph atlas packs its glyphs so, and frees them by scale.

Fonts

A rasterizer opens each face itself with FreeType, from face-source, face-index and face-variations, and sets it to each glyph's size as it draws. It keeps that FreeType library and face with the cl-textrender face, and each font's glyph images with the font, through face-cache and font-cache, under a cache key of its own: they are freed when the face or font is, or when the collector takes the rasterizer, whichever comes first, and a dropped font takes its images with it. Faces made from octet vectors work too. Glyphs from fallback fonts are drawn from the font they came from.

Threads

A rasterizer may be used from any number of threads at once. FreeType draws with a face from one thread at a time, so the glyphs of one face are drawn one at a time, and glyphs of different faces in parallel. Drawing a glyph drawn before takes no lock.

Drawing with a destroyed face or font signals textrender-error, as anything else does; see Fonts. So does a glyph FreeType cannot draw, such as one whose outline is damaged: FreeType's errors are reported in cl-textrender's terms (see Errors). A glyph ID the font does not have is no error: it is drawn as the font's .notdef, glyph 0, one image for them all.

Color glyphs

Color bitmap glyphs, such as those of emoji fonts, are drawn as their alpha only: the shape of the emoji, in the text's color. Monochrome bitmaps become 0 or 255.