6 Glyph Outlines
dxtr edited this page 2026-10-19 01:12:07 +02:00

Glyph Outlines

Renderers that draw from outlines start from a glyph's contours: Slug-style curve renderers, MSDF generators, vector output such as SVG and PDF, and meshes for 3D text. glyph-outline gives them.

Getting an outline

(tr:with-font (font "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 1)
  (tr:glyph-outline font (tr:font-glyph-id font #\l)))
;; => #S(CL-TEXTRENDER:OUTLINE
;;       :CONTOURS (((:LINE 0.09423828 0.7597656 0.18408203 0.7597656)
;;                   (:LINE 0.18408203 0.7597656 0.18408203 0.0)
;;                   (:LINE 0.18408203 0.0 0.09423828 0.0)
;;                   (:LINE 0.09423828 0.0 0.09423828 0.7597656)))
;;       :XMIN 0.09423828 :YMIN 0.0 :XMAX 0.18408203 :YMAX 0.7597656)

In the rest of this page, l-outline is that outline.

An outline is in layout units of the font's size, in font orientation (y up), relative to the glyph's origin on the baseline. Make a font of size 1, as here, to get outlines in ems. To draw one at a glyph's position, see Coordinates and Units.

A glyph ID the font does not have gives the outline of its .notdef, glyph 0, usually a box: see Fonts.

Contours and segments

outline-contours is a list of contours. Each contour is a closed list of segments, each with its start point given explicitly:

(:line      x0 y0                 x1 y1)
(:quadratic x0 y0  cx  cy         x1 y1)
(:cubic     x0 y0  c1x c1y c2x c2y x1 y1)

Each segment starts where the previous one ended, and the last ends where the first started; a contour the font left open is closed with a line. TrueType fonts (.ttf) give lines and quadratics; CFF fonts (.otf) give lines and cubics.

outline-xmin, outline-ymin, outline-xmax and outline-ymax bound every point, control points included, so the box always contains the ink. A glyph without ink, such as a space, has no contours (outline-empty-p), and all four bounds are zero.

Quadratics only

Some renderers, such as Slug, take only quadratic curves. With :quadratic t, glyph-outline replaces each cubic with quadratics within :tolerance layout units of it, a thousandth of an em by default:

(tr:with-font (font "/usr/share/fonts/opentype/urw-base35/URWBookman-Light.otf" :size 1)
  (let ((o (tr:font-glyph-id font #\o)))
    (list (mapcar #'length (tr:outline-contours (tr:glyph-outline font o)))
          (mapcar #'length (tr:outline-contours
                            (tr:glyph-outline font o :quadratic t))))))
;; => ((6 4) (10 8))

The cubic is cut into equal pieces, as few as keep every piece within the tolerance, and each piece becomes one quadratic. TrueType outlines are already quadratic, so :quadratic changes nothing for them.

Packed for Slug

outline-quadratics packs an outline into one vector of single-floats, six per curve, x0 y0 cx cy x1 y1, contour after contour. Lines become quadratics with their control point at their middle:

(tr:outline-quadratics l-outline)
;; => #(0.09423828 0.7597656 0.13916016 0.7597656 0.18408203 0.7597656
;;      0.18408203 0.7597656 0.18408203 0.3798828 0.18408203 0.0
;;      0.18408203 0.0 0.13916016 0.0 0.09423828 0.0
;;      0.09423828 0.0 0.09423828 0.3798828 0.09423828 0.7597656)

An outline with cubics needs :tolerance to pack, or outline-quadratics signals textrender-error; make it with :quadratic t instead. cl-slug builds Slug's band and curve textures from glyph shapes like these.

Orientation

TrueType glyphs draw their outer contours clockwise and their holes counterclockwise; CFF glyphs do the opposite. Fill rules don't care, but signed distance fields take their sign from the direction, so an MSDF generator wants every glyph the same way round.

  • outline-signed-area returns the area the outline encloses, positive where outer contours run counterclockwise (with y up), negative where clockwise. Holes count against their contours. It is exact for all three segment kinds.
  • outline-orientation returns :clockwise or :counterclockwise from that sign, or nil for an empty outline.
  • orient-outline returns the outline running in the direction you ask for. If it ran the other way, every contour is reversed, which keeps holes opposite to their outer contours. It returns the same outline if it already runs that way.
(tr:outline-orientation l-outline)          ; DejaVu Sans, TrueType
;; => :CLOCKWISE
(tr:outline-signed-area l-outline)
;; => -0.06826019
(tr:outline-contours (tr:orient-outline l-outline :counterclockwise))
;; => (((:LINE 0.09423828 0.7597656 0.09423828 0.0)
;;      (:LINE 0.09423828 0.0 0.18408203 0.0)
;;      (:LINE 0.18408203 0.0 0.18408203 0.7597656)
;;      (:LINE 0.18408203 0.7597656 0.09423828 0.7597656)))

orient-outline assumes the glyph is consistent with itself, as font glyphs are; it does not repair contours that disagree with each other. (cl-sdf resolves either winding and overlapping contours itself, so the distance field atlas needs no orienting.)

Caching outlines

Computing an outline asks HarfBuzz for the glyph each time. A renderer that draws the same glyphs every frame asks the font to keep them:

(tr:cached-quadratics font glyph-id)   ; => #(x0 y0 cx cy x1 y1 ...)
(tr:cached-outline font glyph-id)      ; => OUTLINE, for MSDF
  • cached-outline returns the outline as glyph-outline does with the default tolerance; :quadratic t asks for the quadratic form.
  • cached-quadratics returns the packed quadratics, from the quadratic outline.
  • Each is computed on the first request, and the same object is returned after, without allocating. Do not modify it.
  • The font keeps them, with its other caches, and they go with it. Two fonts, even of one face, keep their own.
  • Both may be called from any thread.