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.
Styled Text and Fallback
Every character of a layout has a style: a list of fonts to draw it from,
a list of OpenType features, and a piece of data for the renderer. The
font argument of layout-text sets the base style. :spans change it for
ranges of the text.
In the examples, font is DejaVu Sans at size 16.
Font fallback
No font has every character. Wherever cl-textrender takes a font, it also takes a list of fonts: the first is the primary, and the rest are fallbacks for characters it lacks.
(tr:with-font (latin "/usr/share/fonts/opentype/urw-base35/URWBookman-Light.otf" :size 16)
(tr:with-font (dejavu "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" :size 16)
(tr:with-font (cjk "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf" :size 16)
(let ((layout (tr:layout-text "Tokyo 東京 שלום" (list latin dejavu cjk))))
(map 'list (lambda (g)
(list (char (tr:layout-string layout) (tr:glyph-text-index g))
(cond ((eq (tr:glyph-font g) latin) :bookman)
((eq (tr:glyph-font g) dejavu) :dejavu)
(t :droid))))
(tr:layout-glyphs layout))))))
;; => ((#\T :BOOKMAN) (#\o :BOOKMAN) (#\k :BOOKMAN) (#\y :BOOKMAN) (#\o :BOOKMAN)
;; (#\ :BOOKMAN) (#\東 :DROID) (#\京 :DROID) (#\ :DROID)
;; (#\ם :DEJAVU) (#\ו :DEJAVU) (#\ל :DEJAVU) (#\ש :DEJAVU))
The Hebrew comes out in visual order, so the word reads backwards here.
(SBCL prints these characters by name, such as #\U6771; they are shown
as themselves.) The rules:
- Per grapheme. Each grapheme is drawn from the first font that has
glyphs for all of it, so a letter is never split from its accents. If
Bookman has
ebut not a combining accent after it, the wholeécomes from DejaVu. - Stay in the previous font. A grapheme stays in the previous grapheme's font when that font is in its list and has it. The space after 東京 stays in Droid Sans rather than going back to Bookman, so the run is not split.
- Invisible characters don't count. Joiners, variation selectors and other default-ignorable characters, and control characters, need no glyph for a font to qualify.
- No font has it. A grapheme no font in the list has comes from the
list's first font, which draws
.notdef(glyph 0), usually an empty box. A malformed font whose character map points past its last glyph counts as not having that character.
Each glyph records the font it actually came from, glyph-font, and runs
split wherever the font changes. A single font is the same as a list of
one.
Spans
:spans styles ranges of the text. Each span is a list:
(start end &key font features data)
- start and end are text indexes,
0 <= start <= end <= (length text). :fontis a font or a fallback list for the range.:featuresis a list of OpenType feature strings.:datais anything: a color, a link target, a style object. It is passed through to every glyph the span covers (glyph-data) and to its runs (run-data).
(tr:with-font (bold "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" :size 16)
(let ((layout (tr:layout-text "Warning: disk full" font
:spans `((0 8 :font ,bold :data :red)
(9 18 :features ("smcp"))))))
(map 'list (lambda (run)
(list (tr:run-start run) (tr:run-end run)
(tr:run-data run) (eq (tr:run-font run) bold)))
(tr:line-runs (aref (tr:layout-lines layout) 0)))))
;; => ((0 8 :RED T) (8 9 NIL NIL) (9 18 NIL NIL))
Overlapping spans
Later spans override earlier ones where they overlap, key by key: a span
changes only the keys it gives. With '((0 4 :data :red) (2 4 :font bold)),
characters 2 and 3 are bold and red. With '((0 4 :data :red) (2 6 :data :blue)), characters 2 to 5 are blue.
Ranges with equal styles, compared with equal, share one style, so runs do
not split between them.
OpenType features
:features takes feature strings in HarfBuzz's syntax:
| String | Meaning |
|---|---|
"smcp" or "+smcp" |
Turn small capitals on. |
"-liga" |
Turn standard ligatures off. |
"aalt=2" |
Set a feature to a value, such as the second alternate. |
A feature applies only where the font has it. Turning off ligatures, for instance, keeps "office" as six glyphs instead of four:
(length (tr:layout-glyphs (tr:layout-text "office" font)))
;; => 4
(length (tr:layout-glyphs
(tr:layout-text "office" font :spans '((0 6 :features ("-liga"))))))
;; => 6
A string HarfBuzz cannot parse signals textrender-error, with the syntax
in its hint. Use spans, not HarfBuzz's [start:end] ranges, to limit a
feature to part of the text.
Mixed sizes and metrics
Fonts in a list or in spans may differ in size and design. Each line takes the largest ascent, descent and line gap among the fonts its glyphs come from, so nothing overlaps:
line-ascentandline-descentgive the values a line used. In the fallback example, the line's ascent is Droid Sans's (16.69) and its descent Bookman's (4.53).- A span with a larger font raises its line: with a 4096-unit span on a 2048-unit font, that line's ascent doubles and the lines below move down.
- Wrapping measures each character in its own font, and carets and selections follow the larger glyphs.
Runs split wherever the font or style changes, so every run has one font
(run-font) and one piece of data (run-data).
Errors
A malformed span signals textrender-error, naming the span and saying
what a span looks like: a span that is not (start end &key ...), an
unknown key, :features that is not a list of strings, a span that ends
past the text or starts after its end. So does an empty font list, or one
with something other than fonts in it:
(tr:layout-text "abc" font :spans '((2 9 :data :red)))
;; signals TEXTRENDER-ERROR:
;; The span (2 9 :DATA :RED) ends at 9, past the end of the text, which has 3 characters.
;; A span is (START END &key FONT FEATURES DATA), where START and END are
;; character indexes with 0 <= START <= END <= the length of the text.