10 Development
dxtr edited this page 2026-10-18 15:46:31 +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.

Development

Layout

cl-ft2.asd                  systems cl-ft2 and cl-ft2/test
CLAUDE.md                   notes and code standards for working on cl-ft2
native/
  build-windows.sh          builds windows-x64/freetype.dll from FreeType's source
  windows-x64/
    freetype.dll            the FreeType that cl-ft2 loads on Windows
    FTL.TXT  FREETYPE-LICENSE.TXT
src/
  package.lisp              both package definitions and export lists
  conditions.lisp           FREETYPE-ERROR, the error table and explanations
  ffi-library.lisp          loading libfreetype, or the bundled DLL on Windows
  ffi-types.lisp            scalar types, enums, bitfields
  ffi-structs.lisp          record layouts, written by hand
  ffi-functions.lisp        defcfun entry points
  units.lisp                26.6 / 16.16 conversion, keyword types, load flags
  library.lisp              LIBRARY
  face.lisp                 FACE, face properties, the face-dependent protocol
  size.lisp                 sizes, strikes, size metrics, transforms
  charmap.lisp              charmaps, char-index, iteration, glyph names
  glyph.lisp                loading, metrics, advances, kerning, RENDER-GLYPH
  bitmap.lisp               BITMAP, GLYPH-BITMAP, CHAR-BITMAP, GLYPH-INDEX-BITMAP
  outline.lisp              GLYPH-OUTLINE, GLYPH-CONTROL-BOX
  glyph-object.lisp         GLYPH objects
  stroker.lisp              STROKER, STROKE-GLYPH
  outline-object.lisp       OUTLINEs of your own, STROKE-OUTLINE, RENDER-OUTLINE
  variation.lisp            variable fonts, and FreeType's coordinate quirks
tests/
  package.lisp              test package, test font lookup
  library-test.lisp  face-test.lisp  glyph-test.lisp
  glyph-object-test.lisp  outline-object-test.lisp  variation-test.lisp
  fonts/
    cl-ft2-test-variable.ttf        tiny variable font with exact geometry
    make-test-variable-font.py      script that builds it (fontTools)

Running the tests

Dependencies are pinned in the stiftfile with stift, and the tests use parachute.

stift test
;; Summary:
Passed:   551
Failed:     0
Skipped:    0

asdf:test-system signals an error if any test fails, so the command exits unsuccessfully.

The tests need:

  • libfreetype (Debian/Ubuntu: libfreetype6). On Windows, the bundled DLL is used.
  • DejaVu Sans (Debian/Ubuntu: fonts-dejavu-core), looked up at /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf. To use a copy elsewhere, set CL_FT2_TEST_FONT to its path; on Windows, where there is no such default, that is how the tests find it. Some assertions check DejaVu Sans's exact metrics, so another font will fail them.

The test variable font

tests/fonts/cl-ft2-test-variable.ttf is committed to the repository. It is deliberately tiny and exactly specified, so that the variation tests can assert precise values:

  • axes wght 100–400–900 and wdth 75–100–125
  • named instances Thin (100, 100), Regular (400, 100) and Black (900, 100)
  • one real glyph, I (U+0049): a rectangle whose geometry is linear in the normalized axis values
    Axis (normalized) Effect
    wght −1 … 0 … +1 right edge x 200 … 300 … 500, advance 300 … 400 … 600
    wdth 0 … +1 top edge y 700 … 800

To change it, edit tests/fonts/make-test-variable-font.py and rerun it. It needs fontTools (pip install fonttools):

python3 tests/fonts/make-test-variable-font.py

The Windows DLL

native/windows-x64/freetype.dll is FreeType 2.14.3, cross-compiled with MinGW-w64 by native/build-windows.sh from FreeType's release tarball, whose SHA-256 the script pins. The build is reproducible: paths and timestamps are kept out of the binary, so the same source and toolchain give the same bytes.

# Debian: apt install mingw-w64 cmake curl xz-utils
native/build-windows.sh            # rebuild native/windows-x64/freetype.dll
native/build-windows.sh --check    # fail unless the committed DLL is what the source builds to

To move to a new FreeType release, change FREETYPE_VERSION and FREETYPE_SHA256 in the script, run it, and commit the DLL and the licence files it copies. A MinGW-w64 update can change the bytes too; then the CI check fails, and the fix is the same: rebuild and commit.

CI

.forgejo/workflows/ci.yml runs on every push to main and on pull requests. Both jobs run in a debian:forky container:

  • test uses the shared actions fakehacker/actions/checkout@v1.0.0 and fakehacker/actions/stift-ci@v1, the latter's major tag, which follows its compatible releases, with sbcl-version: "2.6.9", lisps: sbcl and packages: libfreetype6 fonts-dejavu-core; it runs stift test on SBCL. No C toolchain or FreeType headers are installed, because none are needed.
  • windows-dll installs MinGW-w64 and CMake and runs native/build-windows.sh --check, so the committed DLL always matches its source.

Conventions

  • Two layers. cl-ft2/ffi is a pure transcription: no error checks and no Lisp conveniences. All policy lives in cl-ft2.
  • Lisp values at the boundary. Strings, keywords, integers and exact rationals go out. Bitmaps are copied into Lisp arrays. No foreign types leak out of cl-ft2.
  • One flat condition. Every failure is a freetype-error, with context in its slots. Messages are written in terms of the cl-ft2 call, with a hint on what to do. Bad keywords and types are type-errors raised before any foreign call.
  • Explicit lifetimes with idempotent destroy-… functions, named as in cl-harfbuzz. A library destroys its dependents. Destroyed objects are detected and reported, never dereferenced.
  • Settable state is a setf place, with no set- functions: face-design-coordinates, face-normalized-coordinates, face-axis-coordinate, face-named-instance and face-charmap. So are face-size, face-strike, face-transform-matrix and face-transform-delta, which the face remembers because FreeType cannot report them back exactly. Two-dimensional places such as face-size read as two values and are set from a number or an (x y) list.
  • glyph-… takes a glyph source (a face's slot or a glyph) and is generic; glyph-index-… takes a face and a glyph index.
  • Docstrings: struct and class fields are documented in the struct or class docstring, not per accessor. Every other function, macro, type and package has one, internal ones included.
  • Types: every exported function declaims its ftype directly above its defun, every struct and class slot has a type, and the keywords FreeType's enumerations take are types made from the raw layer's enumerations. Tests that pass a deliberately wrong argument pass it through opaque, since SBCL refuses a constant of the wrong type at compile time. api-audit.lisp at the workspace's root lists missing docstrings and ftypes.
  • Names: internal names are plain, with no % prefix; a raw reader beside a checked public one says what it returns, as face-handle-or-nil beside face-handle.
  • Changes to cl-ft2 that cl-harfbuzz depends on: once they are pushed, move cl-harfbuzz's pin with stift work sync at the workspace's root, or stift update https://fakehacker.cc/dxtr/cl-ft2.git in the cl-harfbuzz checkout.