Table of Contents
- Outlines
- glyph-outline source → list of contours
- glyph-control-box source → xmin, ymin, xmax, ymax
- Outlines of your own
- make-outline points tags contour-ends → outline
- outline-control-box outline → xmin, ymin, xmax, ymax
- render-outline library outline width height &key (x 0) (y 0) (fill-rule :nonzero) → coverage
- stroke-outline outline stroker &key open → outline
- Examples
Outlines
Glyphs from scalable fonts are vector outlines until they are rendered. cl-ft2 returns them as plain Lisp lists, ready for your own rasteriser, an SDF/MSDF generator, a GPU curve renderer, triangulation, or export to SVG. Outlines of your own, made from arrays, FreeType strokes and rasterizes for you (see below).
glyph-outline source → list of contours
Returns the outline of the glyph in a face's slot, or of a glyph object. The result is a list of contours. Each contour is a list of segments that starts with :move-to:
| Segment | Meaning |
|---|---|
(:move-to x y) |
start a contour at (x, y) |
(:line-to x y) |
straight line to (x, y) |
(:conic-to cx cy x y) |
quadratic Bézier with control point (cx, cy) to (x, y) |
(:cubic-to c1x c1y c2x c2y x y) |
cubic Bézier with control points (c1x, c1y), (c2x, c2y) to (x, y) |
Each segment starts where the previous one ended. Contours are closed: when a font's contour does not end at its start, FreeType adds a final segment back to the :move-to point.
(setf (ft2:face-size face) 32)
(ft2:load-char face #\O)
(ft2:glyph-outline face)
;; => (((:MOVE-TO 807/64 20)
;; (:CONIC-TO 587/64 20 457/64 567/32)
;; (:CONIC-TO 41/8 247/16 41/8 23/2)
;; …
;; (:CONIC-TO 1027/64 20 807/64 20)) ; the counter (inner contour)
;; ((:MOVE-TO 807/64 23) …)) ; the outer contour
Coordinates are in pixels, as exact rationals with y growing up from the baseline, or in font units if the glyph was loaded with :no-scale. Hinting applies: for a raw, size-independent outline, load with :no-scale (font units) or :no-hinting (scaled but not grid-fitted).
Curve types: TrueType (glyf) fonts produce :conic-to segments. CFF/CFF2 (.otf) and Type 1 fonts produce :cubic-to. Implied on-curve points between consecutive conic control points are made explicit, so every segment is a complete Bézier.
Contour order is the font's order: here the counter comes before the outer contour. Do not rely on any order.
Winding: TrueType outlines wind outer contours clockwise, PostScript ones counter-clockwise. Fill with the non-zero rule, which is correct for both.
The glyph must be in :outline format. A rendered glyph, a glyph loaded with :render, or one from a bitmap strike signals :invalid-glyph-format:
Glyph 36 is in bitmap format, not an outline.
Only outline glyphs have outlines: load the glyph without the :RENDER
flag, from a scalable face. A rendered glyph is read with GLYPH-BITMAP.
The outline reflects the face's transform (face-transform-matrix, face-transform-delta) and variation coordinates at load time. For a glyph object, it also reflects any transform-glyph or stroke-glyph applied since.
glyph-control-box source → xmin, ymin, xmax, ymax
The control box, as four values in the same units as glyph-outline. It encloses every outline point, control points included, so it can be slightly larger than the tight bounding box. It is cheap and exact enough for layout and atlas packing.
(ft2:load-char face #\O)
(ft2:glyph-control-box face) ; => 115/64, 0, 1497/64, 23
For the glyph slot, the glyph must be an outline. A glyph object may also be a bitmap, whose box is its pixel extent.
Outlines of your own
An outline is a vector shape you make yourself, such as a shape of a vector drawing, from plain arrays as FreeType's FT_Outline holds one. stroke-outline turns it into the outline of its stroke, and render-outline rasterizes it into coverage, through FreeType's stroker and anti-aliasing rasterizer:
(ft2:with-library (lib)
(let ((tick (ft2:make-outline '(2 5 4 3 8 7) ; x, y of each point
(list ft2:+on-curve+ ft2:+on-curve+ ft2:+on-curve+)
'(2)))) ; each contour's last point
(ft2:with-stroker (stroker lib :radius 1 :line-cap :round :line-join :round)
(let ((stroke (ft2:stroke-outline tick stroker :open t))) ; capped, not closed
(ft2:render-outline lib stroke 10 10)))))
;; => #2A((0 0 0 0 0 0 0 0 0 0)
;; (0 0 0 0 0 0 0 0 0 0)
;; (0 0 0 0 0 0 21 194 179 0)
;; (0 0 0 0 0 21 211 255 194 0)
;; (0 178 195 21 22 212 255 210 21 0)
;; (0 195 255 212 213 255 210 21 0 0)
;; (0 21 211 255 255 210 21 0 0 0)
;; (0 0 21 195 195 21 0 0 0 0) …)
An outline is plain Lisp data: it is never destroyed, and threads may share it. Each call copies it into memory FreeType allocates and frees that memory before it returns.
make-outline points tags contour-ends → outline
points: a sequence of reals, two for each point, x then y, in pixels with y growing up, as glyph outlines are. They are kept as single-floats, which hold FreeType's 26.6 coordinates exactly up to 262144 pixels.tags: one for each point.+on-curve+(1) marks a point the outline passes through,+conic-control+(0) the control point of a conic, or quadratic Bézier, curve, and+cubic-control+(2) each of the two control points of a cubic one. These are FreeType'sFT_CURVE_TAG_…values. Two conic control points in a row meet on the curve midway between them, as in TrueType glyphs.contour-ends: the index of each contour's last point, in increasing order, the last that of the last point.
The sequences are copied. An outline FreeType cannot use signals a freetype-error named :invalid-outline that says what is wrong: cubic control points come in pairs, followed by a point on the curve or by the end of the contour; a contour does not start with one; coordinates are within 8388608 (2^23) pixels of 0, so that FreeType's 26.6 numbers fit 32 bits; an outline holds at most 32767 points and contours.
(ft2:make-outline '(0 0 1 1 2 2) '(1 2 1) '(2))
;; freetype-error: The outline cannot be made: contour 0 is malformed: its
;; cubic control point 1 is not one of a pair.
;; Give two coordinates and a tag for each point, and each contour's last point.
outline-points, outline-tags and outline-contour-ends return the arrays, a (simple-array single-float (*)), a (simple-array (unsigned-byte 8) (*)) and a (simple-array (unsigned-byte 16) (*)); do not modify them. outline-p tests for an outline.
outline-control-box outline → xmin, ymin, xmax, ymax
The box around every point, control points included, in the outline's pixels; all zero for an outline of no points.
render-outline library outline width height &key (x 0) (y 0) (fill-rule :nonzero) → coverage
Rasterizes outline with FreeType's anti-aliasing rasterizer and returns how much of each pixel it covers: a 2D (unsigned-byte 8) array of height rows of width pixels, row 0 at the top, from 0 for none to 255 for all, as bitmap-data of a :gray bitmap. The pixels are those of the width by height box whose bottom left corner is at x, y in the outline's pixels; what lies outside is left out.
fill-rule::nonzerofills where the contours wind around in total any number of times, which is right for glyphs and strokes whatever their direction;:even-oddwhere they wind around an odd number of times, so a contour inside another is a hole whichever way it runs. The two make up the typefill-rule.widthandheightare at most 8388608 (2^23) pixels, the typebitmap-dimension, andxandywithin as many pixels of 0, the typeoutline-coordinate, as an outline's coordinates are; others are atype-error.- Coverage is the exact area of each pixel covered, without gamma. FreeType draws curves as short lines, which cut inside them by a few hundredths of a pixel on average: a circle of radius 4 pixels loses about 2.6 % of its area, one of radius 32 about 0.3 %.
(ft2:render-outline lib (ft2:make-outline '(1 1 3 1 3 3 1 3) '(1 1 1 1) '(3)) 4 4)
;; => #2A((0 0 0 0) (0 255 255 0) (0 255 255 0) (0 0 0 0))
stroke-outline outline stroker &key open → outline
Returns the outline of the stroke stroker draws along outline, with its radius, caps, joins and miter limit; see Strokers. The radius is in the outline's pixels. Fill the result with the nonzero rule, render-outline's default: where a stroke turns sharply, its contours overlap.
open: which contours are open paths, ended with caps, rather than closed ones, joined where they close:nilfor none,tfor all, or a sequence with a generalized boolean for each contour.- A closed contour's stroke is two contours, its outer and its inner border; an open one's is one.
- A contour of one point, or of points all in one place, makes no stroke, as FreeType's stroker makes none for it. To draw a dot there, as SVG's round caps do, add a circle yourself.
- A stroke with more points than an outline holds signals
:array-too-large.
Examples
Flattening to an SVG path
(defun outline->svg-path (contours &key (height 0))
"Return an SVG path string for CONTOURS, flipped to SVG's Y-down axis
around HEIGHT."
(with-output-to-string (s)
(flet ((pt (x y) (format s " ~,3F ~,3F" x (- height y))))
(dolist (contour contours)
(dolist (segment contour)
(destructuring-bind (kind &rest xy) segment
(write-string (ecase kind
(:move-to "M") (:line-to "L")
(:conic-to "Q") (:cubic-to "C"))
s)
(loop for (x y) on xy by #'cddr do (pt x y))))
(write-string " Z " s)))))
(ft2:load-char face #\g :flags '(:no-scale))
(outline->svg-path (ft2:glyph-outline face) :height (ft2:face-ascender face))
Converting quadratics to cubics
(defun conic->cubic (x0 y0 cx cy x y)
"Return the cubic control points of the quadratic from (X0, Y0) through
control (CX, CY) to (X, Y)."
(list :cubic-to
(+ x0 (* 2/3 (- cx x0))) (+ y0 (* 2/3 (- cy y0)))
(+ x (* 2/3 (- cx x))) (+ y (* 2/3 (- cy y)))
x y))
Because the coordinates are exact rationals, conversions like this lose no precision.