- Common Lisp 97.6%
- Shell 1.3%
- Python 1.1%
To the latest commit of cl-ffi, whose allocate boxes no pointer, of Pyssel's step 25. |
||
|---|---|---|
| .forgejo/workflows | ||
| native | ||
| src | ||
| tests | ||
| .gitattributes | ||
| .gitignore | ||
| cl-ft2.asd | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.org | ||
| stiftfile | ||
cl-ft2
Common Lisp bindings for FreeType 2.
cl-ft2 comes in two layers:
cl-ft2: a Lisp-idiomatic API. It returns Lisp values (strings, keywords, integers, exact rationals), copies bitmaps into Lisp arrays, and turns every FreeType error into afreetype-errorcondition.cl-ft2/ffi: the raw layer underneath, on cl-ffi, a direct transcription of the covered part of the C API (FT_Load_Glyph→ft-load-glyph,FT_FaceRec→(:struct ft-face-rec), …). It does no error checking and no memory management.
Only the libfreetype shared library (2.13.1 or newer) is needed at run time.
The struct layouts are written by hand against the public headers using
cl-ffi's C-sized types, so no C compiler is needed to build.
On Windows there is nothing to install: cl-ft2 carries its own
native/windows-x64/freetype.dll and loads it by its full path, so no other
freetype.dll on PATH gets in the way. An application saved from an image,
without cl-ft2's source tree, loads freetype.dll by name instead: ship the
DLL next to the executable. native/build-windows.sh rebuilds the DLL from
source, reproducibly, and native/build-windows.sh --check verifies the
committed one. That build has no PNG glyph bitmaps (sbix, CBDT colour emoji),
bzip2-compressed fonts or WOFF2. When you ship it, include
native/windows-x64/FTL.TXT: FreeType's licence asks for credit in your
documentation.
Usage
(asdf:load-system :cl-ft2)
(defpackage #:example
(:use #:cl)
(:local-nicknames (#:ft2 #:cl-ft2)))
(in-package #:example)
(ft2:with-library (lib)
(ft2:with-face (face lib "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
(setf (ft2:face-size face) 32) ; pixels per EM
(let ((bitmap (ft2:char-bitmap face #\A))) ; load, render and copy out
(ft2:bitmap-data bitmap)))) ; => #2A((0 0 ...) ...), (unsigned-byte 8)
The examples here use ft2 as a local nickname for cl-ft2, as above.
char-bitmap takes :mode (:normal, :mono, :lcd, :lcd-v, :sdf).
glyph-index-bitmap does the same for a glyph index, such as one that
shaping produced. A bitmap's left and top place it relative to the pen
position on the baseline.
Glyphs step by step
Each face has one glyph slot. load-char and load-glyph load a glyph into
it, and the readers report on the glyph loaded last:
(ft2:load-char face #\A)
(ft2:glyph-metrics face) ; => #S(GLYPH-METRICS :WIDTH 22 :HEIGHT 23 ...)
(ft2:glyph-advance face) ; => 22, 0
(ft2:render-glyph face :mode :mono)
(ft2:glyph-bitmap face) ; => #S(BITMAP :PIXEL-MODE :MONO ...)
;; Vector outlines, as contours of segments.
(ft2:load-char face #\O)
(ft2:glyph-outline face) ; => (((:MOVE-TO 807/64 20) (:CONIC-TO ...) ...) ...)
;; Kerning between two glyphs.
(ft2:kerning face (ft2:char-index face #\A) (ft2:char-index face #\V))
; => -2, 0
Glyph objects and strokers
;; Draw an outlined "A": keep a copy of the glyph, stroke it, render it.
(ft2:load-char face #\A)
(ft2:with-glyph (glyph face) ; a copy, independent of the face's slot
(ft2:with-stroker (stroker lib :radius 3/2 :line-join :round)
(ft2:stroke-glyph glyph stroker) ; :border :both, :outside or :inside
(ft2:glyph-bitmap (ft2:render-glyph glyph))))
The glyph-… functions (glyph-index, glyph-format, glyph-metrics,
glyph-advance, glyph-linear-advance, glyph-outline, glyph-control-box,
glyph-bitmap, render-glyph, copy-glyph) accept either a face, meaning
the glyph in its glyph slot, or a glyph object. The glyph-index-…
functions (glyph-index-bitmap, glyph-index-advance, glyph-index-name)
take a face and a glyph index instead, and leave the glyph slot alone.
Outlines of your own
An outline is a vector shape of your own, made from plain arrays as
FreeType's FT_Outline holds one: two coordinates for each point, in
pixels with y growing up, a tag for each point, and the index of each
contour's last point. stroke-outline turns it into the outline of its
stroke, and render-outline rasterizes it into coverage:
(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 ...) ...), 10 by 10
- A tag is
+on-curve+for a point the outline passes through,+conic-control+for the control point of a conic (quadratic) curve, or+cubic-control+for each of the two control points of a cubic one; they are FreeType's values, 1, 0 and 2.make-outlinecopies the sequences it is given, and signals afreetype-errornamed:invalid-outlinethat says what is wrong with one FreeType cannot use. stroke-outlinestrokes with the stroker's radius, caps, joins and miter limit, its radius in the outline's pixels.:opensays which contours are open paths, capped at their ends, and which are closed:nilfor none,tfor all, or a sequence with a boolean for each contour. Fill the stroke with the nonzero rule.render-outlinereturns a 2D array of(unsigned-byte 8)coverage, 0 to 255, rows top first, for the box of the width and height given whose bottom left corner is at:x,:yin the outline's pixels.:fill-ruleis:nonzero, as for glyphs and strokes, or:even-odd. FreeType draws curves as short lines a few hundredths of a pixel inside them.outline-points,outline-tagsandoutline-contour-endsread an outline back, andoutline-control-boxgives the box around its points.
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.
Variable fonts
(ft2:with-face (face lib "Inter.ttf")
(ft2:face-variation-axes face) ; => (#S(VAR-AXIS :TAG "wght" :MINIMUM 100 ...) ...)
(ft2:face-named-instances face) ; => (#S(NAMED-INSTANCE :NAME "Thin" ...) ...)
(setf (ft2:face-axis-coordinate face "wght") 650)
(setf (ft2:face-named-instance face) 2) ; or (ft2:open-face lib path :instance 2)
(setf (ft2:face-design-coordinates face) '()) ; back to the named instance
(setf (ft2:face-named-instance face) nil)) ; back to the font's defaults
Named instances are numbered from 0, in the order face-named-instances
lists them, and nil means none, as in cl-harfbuzz.
Design coordinates are in each axis's own units (wght 100–900, …);
normalized coordinates run from -1 to 1. Setting face-design-coordinates or
face-normalized-coordinates clamps values to the axis range and fills
unlisted axes with the font's defaults. The getters always report the
coordinates actually in effect. FreeType's own getters can answer stale
coordinates after a reset to the font's defaults, so there, with no named
instance selected and no :variation flag, cl-ft2 answers the defaults
itself. That FreeType quirk is handled by coordinates-in-effect in
src/variation.lisp, whose comment explains it, and the test
coordinates-after-a-reset covers it.
A FreeType bug that cl-ft2 does not work around: once a face has gone from
a named instance whose coordinates were changed back to the font's
defaults, selecting that named instance again, or setting its design
coordinates, does nothing. Glyphs go on loading at the defaults, and the
getters report the defaults. With the font of cl-ft2's tests,
tests/fonts/cl-ft2-test-variable.ttf:
(setf (ft2:face-named-instance face) 2) ; Black, wght 900
(setf (ft2:face-axis-coordinate face "wdth") 125) ; changed
(setf (ft2:face-named-instance face) nil) ; back to the defaults
(setf (ft2:face-named-instance face) 2) ; Black again: no effect
(ft2:face-design-coordinates face) ; => (400 100), not (900 100)
Selecting another named instance, or setting other coordinates, before
going back to it avoids the bug, as does opening the face again. It was
seen with FreeType 2.14.3, and the code responsible is the same in 2.13.1,
the oldest FreeType cl-ft2 takes, and in FreeType's development sources of
October 2026: when it is asked for no instance, FT_Set_Named_Instance
fills its copy of the design coordinates from the instance the face still
records, so a later request for that instance's coordinates finds nothing
changed and leaves the defaults in force.
Setting state
Everything about a face that can be read back is set with setf: its size,
strike, transform, charmap, variation coordinates and named instance.
(setf (ft2:face-size face) 21/2) ; or '(10 20); points: pt * dpi / 72
(setf (ft2:face-transform-matrix face) '(1 1/5 0 1)) ; oblique
(setf (ft2:face-transform-delta face) '(1/4 0)) ; subpixel offset
(setf (ft2:face-charmap face) :apple-roman)
A two-dimensional place, such as face-size, reads as two values and is set
from a number or a (x y) list.
Units
FreeType's 26.6 and 16.16 fixed-point numbers are turned into exact rationals:
807/64 is 12.609375 pixels. Face-wide metrics (face-ascender,
face-bounding-box, …) are in font units. Glyph metrics, advances and
outlines are in pixels, or in font units if the glyph was loaded with
:no-scale.
Lifetimes
Libraries, faces, glyphs and strokers are destroyed explicitly, with
destroy-library, destroy-face, destroy-glyph and destroy-stroker, or
with the with- macros. destroy-library first destroys every face, glyph
and stroker created in it. A glyph does not depend on the face it was copied
from. An outline is plain Lisp data and needs no destroying. Using a
destroyed object signals a freetype-error named
:invalid-library-handle, :invalid-face-handle, or :invalid-handle for
glyphs and strokers. It never touches freed memory. FreeType objects are not
thread-safe, so use one library per thread or serialise access to it.
Errors
(handler-case (ft2:open-face lib "/no/such/font.ttf")
(ft2:freetype-error (e)
(values (ft2:freetype-error-name e) ; => :CANNOT-OPEN-RESOURCE
(princ-to-string e))))
;; => "Could not open the font /no/such/font.ttf: the file does not exist or cannot be read.
;; Check the path and the file's permissions.
;; (FreeType: FT_New_Face returned cannot-open-resource)"
The report says what went wrong in terms of the call, then what to do about
it. freetype-error-message and freetype-error-hint return those two
parts. freetype-error-name classifies the error with a keyword.
freetype-error-function names the FreeType function that failed, or is
nil when cl-ft2 caught the problem itself: a destroyed face, a glyph index out
of range, an outline asked of a bitmap, a variation on a font without axes.
Arguments of the wrong type, such as an unknown load flag, render mode or
encoding, or a glyph where a face belongs, signal a standard type-error
before anything reaches FreeType. The types of the keyword arguments, such
as render-mode and line-join, are exported, as the API overview lists
them, so a program can test a value with typep.
API overview
Errors
freetype-error, freetype-error-message, freetype-error-hint,
freetype-error-name, freetype-error-code, freetype-error-function.
Library
make-library, destroy-library, with-library, library-live-p,
library-version.
Faces
open-face (from a filename, a pathname or a vector of octets, the type
font-source), destroy-face,
with-face, face-live-p, face-library, face-num-faces, face-index,
face-num-glyphs, face-family-name, face-style-name,
face-postscript-name, face-font-format, face-flags, face-style-flags,
face-scalable-p, face-fixed-width-p, face-has-kerning-p,
face-has-glyph-names-p, face-italic-p, face-bold-p, face-units-per-em,
face-ascender, face-descender, face-height, face-max-advance-width,
face-max-advance-height, face-underline-position,
face-underline-thickness, face-bounding-box.
Sizes and transforms
face-size (pixels per EM, a real or a (width height) list, the type
face-size-designator), face-strike (an index into face-strikes →
strike), face-size-metrics (→ size-metrics), face-transform-matrix
(a transform-matrix, (xx xy yx yy)), face-transform-delta (a
transform-delta, (x y)).
Character maps
face-charmaps, face-charmap (→ charmap; setf it to an encoding
keyword, the type encoding, or a charmap), char-index (of a character
or a code, the type character-or-code), map-characters,
do-characters, glyph-index-name, name-index.
Glyphs
load-glyph, load-char (their :target is a hinting-target),
glyph-index, glyph-format, glyph-metrics (→ glyph-metrics),
glyph-advance, glyph-linear-advance, glyph-index-advance, kerning
(its :mode is a kerning-mode).
Rendering and outlines
char-bitmap, glyph-index-bitmap, render-glyph (their :mode is a
render-mode), glyph-bitmap (→ bitmap, whose pixel mode is a
pixel-mode and whose data a pixel-data), glyph-outline,
glyph-control-box.
Glyph objects
copy-glyph, destroy-glyph, with-glyph, glyph-live-p,
glyph-library, transform-glyph (its :matrix is a transform-matrix
and its :delta a transform-delta, as render-glyph's :origin is).
Strokers
make-stroker (its :line-cap is a line-cap and its :line-join a
line-join), destroy-stroker, with-stroker, stroker-live-p,
stroker-library, stroker-radius, stroker-line-cap, stroker-line-join,
stroker-miter-limit, stroke-glyph (its :border is a stroke-border).
Outlines of your own
make-outline (→ outline), outline-p, outline-points, outline-tags,
outline-contour-ends, outline-control-box, +on-curve+,
+conic-control+, +cubic-control+, stroke-outline, render-outline
(its width and height are each a bitmap-dimension, up to 2^23 pixels, its
:x and :y each an outline-coordinate, within 2^23 pixels of 0, and its
:fill-rule a fill-rule).
Font variations
face-variable-p, face-variation-axes (→ var-axis),
face-named-instances (→ named-instance), face-design-coordinates,
face-axis-coordinate, face-normalized-coordinates, face-named-instance,
face-default-named-instance. The coordinates, the axis coordinate and the
named instance are set with setf.
Escape hatch and face dependents
library-handle and face-handle return the raw FT_Library and FT_Face
pointers so you can call cl-ft2/ffi functions on objects managed by
cl-ft2.
Other libraries can build objects on a face's FT_Face. HarfBuzz's hb-ft is
the motivating case, used by cl-harfbuzz/ft2. To keep such an object safe,
register it with register-face-dependent and give its class methods on two
generic functions:
release-face-dependent(required): called bydestroy-face, and so bydestroy-library, before theFT_Faceis destroyed.face-changed(optional): called after the face's size, transform or variation coordinates change.
unregister-face-dependent stops both, e.g. when the object is released
first.
Not covered
Some FreeType features are left to other libraries:
- Text shaping and GPOS kerning: use HarfBuzz.
kerningreads only the legacykerntable. - OpenType tables and color glyph layers (COLR/CPAL): use HarfBuzz's
typed OpenType APIs. COLRv0 glyphs already render to
:bgrabitmaps here if you load them with:color. - Bidi, script runs and line breaking: use ICU.
FreeType's cache subsystem and custom FT_Stream input are not bound. Caching
rendered bitmap objects in Lisp is simpler, and open-face reads fonts from
octet vectors.
Development
Dependencies are pinned in the stiftfile with
stift, and tests use
parachute.
stift test
The tests need libfreetype and DejaVu Sans (Debian/Ubuntu: libfreetype6
fonts-dejavu-core; Windows: DejaVu Sans only). To use DejaVu Sans from
another location, set CL_FT2_TEST_FONT to its path. The variable-font tests
use tests/fonts/cl-ft2-test-variable.ttf, a tiny font with exactly specified
geometry. It is committed to the repo; to rebuild it after changing
tests/fonts/make-test-variable-font.py, run that script, which needs
fontTools (pip install fonttools).
License
MIT. See LICENSE.