Skip to content

Commit daa1181

Browse files
committed
feat(preview): explain kerning, and correct the tips against FreeType
kerning gets the ? badge the other tuning controls have: what a kern pair is, why the control is a factor rather than a switch (the device stores an adjustment in sixteenths of a pixel, so a face kerned for print over-tightens), and that turning it down shrinks the table, since the pairs are scaled before they are grouped into classes. Reading FreeType 2.14 to check the tips written yesterday corrected four of them and turned up one thing the panel had wrong. - light hinting preserves inter-glyph spacing, it does not leave the widths to fall where they may (freetype.h, FT_LOAD_TARGET_LIGHT). - Dropout control corrects the bitmap for shape continuity, which is wider than a stroke vanishing (ftimage.h). - Stem darkening is meant for a screen that blends in linear light, and FreeType's own note warns it looks heavy and fuzzy elsewhere, which is what four grey levels are (ftdriver.h, no-stem-darkening). - Mono changes the fit rather than only rounding advances. The panel left grayscale hinting live while mono rasterizing was on, where it does nothing: FreeType turns backward compatibility off for a monochrome render (ttgload.c), which ttinterp.h calls falling back to version 35 behaviour, and those x-axis hacks are the whole difference between the two interpreters. The row now greys with that reason, checked in a browser on a face that carries bytecode. The docstring in cpfont/convert.py, the config reference and the greying section of docs/preview.md say it too. Verified as read: the auto-hinter darkens only at a light render mode (afloader.c), CF2 darkens a scaled load (psft.c), normal runs the font's own bytecode unless the TTF has no fpgm or prep (ftobjs.c), and a tricky face is exempt from the auto-hinter entirely.
1 parent 84bead9 commit daa1181

6 files changed

Lines changed: 77 additions & 12 deletions

File tree

docs/fonts.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ bolditalic = NotoSans-BoldItalic.ttf
127127
| `weight` | `0` | outline emboldening in pixels. Advance widths do not move, so text gets heavier at the same spacing |
128128
| `slant` | `0` | shear as a tangent. `0.25` is about 14 degrees, for synthesizing an oblique a family lacks |
129129
| `hinting` | `normal` | `normal`, `light` (vertical only, softer), `none`, or `auto` (FreeType's auto-hinter, worth trying when a font looks muddy at small sizes) |
130-
| `grayscale_hinting` | `no` | run FreeType's interpreter version 35, which fits stems on both axes rather than hinting for a subpixel display. Only reaches a TrueType face carrying bytecode, under `hinting = normal`. See [Tuning how glyphs look](#tuning-how-glyphs-look) |
130+
| `grayscale_hinting` | `no` | run FreeType's interpreter version 35, which fits stems on both axes rather than hinting for a subpixel display. Only reaches a TrueType face carrying bytecode, under `hinting = normal` and with `mono` off. See [Tuning how glyphs look](#tuning-how-glyphs-look) |
131131
| `mono` | `no` | rasterize each glyph as one bit per pixel, with FreeType's dropout control, instead of thresholding coverage. The font then draws in two levels whatever the reader's anti-aliasing setting is. See [Tuning how glyphs look](#tuning-how-glyphs-look) |
132132
| `stem_darkening` | `no` | FreeType stem darkening. Narrow: a CFF or OTF face under any hinting but `auto`, and a TrueType face only under `hinting = light`. See [Tuning how glyphs look](#tuning-how-glyphs-look) |
133133

@@ -539,6 +539,12 @@ anyway, and so does any family under `light`, `auto` or `none`. So this is a
539539
`hinting = normal` control on a TrueType face, and the preview greys the row
540540
everywhere else.
541541

542+
`mono` takes it as well, for a reason of its own. FreeType turns backward
543+
compatibility off whenever the raster is monochrome, which its own source
544+
calls falling back to version 35 behaviour, and that behaviour is the whole
545+
difference between the two interpreters. So under `mono = yes` both settings
546+
draw the same glyph, and the row greys there too.
547+
542548
### Mono rasterizing
543549

544550
`mono` builds a font with no greys in it at all: every pixel comes out black

docs/preview.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ switch that works is the worse mistake.
298298
The first picks FreeType's other bytecode interpreter, so it is out of reach
299299
for a face with no bytecode to run: a CFF family, a TrueType family with no
300300
instructions, and any family under `light`, `auto` or `none`, where the
301-
auto-hinter draws instead. The second leaves a pixel empty or full. While it is
301+
auto-hinter draws instead. It is out of reach while **mono rasterizing** is on
302+
as well, since FreeType hints the way that interpreter does whenever the
303+
raster is monochrome, whichever of the two you have picked. The second leaves a pixel empty or full. While it is
302304
on there is no coverage in between for **gamma** or the thresholds to act on,
303305
so those two rows are greyed.
304306

src/crossglyph/cpfont/convert.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,13 @@ def apply_interpreter(grayscale):
10051005
so does any face under `light`, `auto` or `none`, unless FreeType calls it
10061006
tricky, which exempts it from that dispatch entirely.
10071007
1008+
`mono` takes it too, for a reason of its own. FreeType turns backward
1009+
compatibility off for a monochrome render (truetype/ttgload.c, the
1010+
TT_INTERPRETER_VERSION_40 gate on `mode != FT_RENDER_MODE_MONO`), and
1011+
truetype/ttinterp.h calls that falling back to version 35 behaviour. What
1012+
backward compatibility gates is the set of x-axis hacks that separate the
1013+
two interpreters, so under `mono` both values draw the same glyph.
1014+
10081015
Nothing reports a build that does not carry the property: the binding
10091016
discards FreeType's error, so the call does nothing and the page is drawn
10101017
with whichever interpreter the build defaults to. See apply_stem_darkening.

src/crossglyph/preview/static/index.html

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,31 @@
185185

186186

187187
<div class="row num">
188-
<label class="name" for="kerning">kerning <span class="unit">×</span></label>
188+
<span class="name"><label for="kerning">kerning <span class="unit">×</span></label><span
189+
class="explain">
190+
<button type="button" class="info" aria-label="What kerning does"
191+
aria-describedby="kerning-tip">?</button>
192+
<span class="tip" id="kerning-tip" role="tooltip">
193+
How hard particular pairs of letters are pulled together. A font
194+
carries a table of them, so that the A and the V in "AV" tuck under
195+
each other instead of leaving a hole, and the build reads that table
196+
and writes the answers into the file.
197+
<br><br>
198+
This is a factor and not a switch, because the useful setting is
199+
often part of the way. 1 is the amount the designer drew, 0 leaves
200+
the pairs alone, and 0.5 keeps the shape of their decisions at half
201+
the amount. A face kerned for print often over-tightens on a screen
202+
this size, where the device stores each adjustment in sixteenths of
203+
a pixel and one of those is already a good fraction of a stem.
204+
<br><br>
205+
Turning it down makes the file smaller as well. The pairs are scaled
206+
before they are grouped, so pairs that flatten into each other are
207+
then grouped together, and those that round away to nothing are
208+
dropped. A style with more kerning entries than the reader will load
209+
is refused outright, and turning this down is one of the two ways
210+
out. Narrower coverage is the other.
211+
</span>
212+
</span></span>
189213
<input type="range" data-slider-for="kerning" min="0" max="2" step="0.05" value="1" tabindex="-1" aria-hidden="true">
190214
<span class="stepper">
191215
<button type="button" class="step mono" data-for="kerning" data-dir="-1" aria-label="decrease kerning"></button>
@@ -260,8 +284,8 @@
260284
<br><br>
261285
<strong>normal</strong> runs the font's own hinting instructions
262286
where it carries them, and lets FreeType work it out where it does
263-
not. <strong>light</strong> fits vertically only, which keeps the
264-
shapes the designer drew and lets the widths fall where they may.
287+
not. <strong>light</strong> snaps to the grid vertically only, which
288+
keeps the horizontal shapes and the spacing the designer drew.
265289
<strong>none</strong> fits nothing. <strong>auto</strong> has
266290
FreeType fit the font whatever instructions it carries, and is worth
267291
trying on a face that looks muddy at small sizes.
@@ -314,6 +338,10 @@
314338
<strong>none</strong> hinting, is fitted by FreeType's own
315339
auto-hinter instead. The row greys itself in those cases and says
316340
which one it is.
341+
<br><br>
342+
One case it does not grey: with <strong>mono rasterizing</strong>
343+
on, FreeType drops back to this older engine's behaviour of its
344+
own accord, so the switch changes nothing either way.
317345
</span>
318346
</span></span>
319347
<input id="grayscale_hinting" name="grayscale_hinting" type="checkbox">
@@ -332,8 +360,8 @@
332360
<span class="tip" id="mono-tip" role="tooltip">
333361
Builds the font with no greys in it at all. Every pixel comes out
334362
paper or ink, chosen by FreeType's dropout control, a rule that
335-
keeps a stroke too thin to land on a pixel from disappearing
336-
altogether. Nothing measures coverage and cuts it at the
363+
turns a pixel on where a stroke would otherwise break up or vanish
364+
between two of them. Nothing measures coverage and cuts it at the
337365
thresholds.
338366
<br><br>
339367
Worth trying for a reader with <strong>anti-aliasing</strong> off.
@@ -345,9 +373,10 @@
345373
It belongs to the font and not to the page: one built this way
346374
draws in two levels whatever the reader's anti-aliasing is set to.
347375
<strong>gamma</strong> and <strong>thresholds</strong> have no
348-
coverage left to work on while it is on, so both grey out. Fitting
349-
to whole pixels also moves some advance widths, so lines break in
350-
slightly different places.
376+
coverage left to work on while it is on, so both grey out. The
377+
outline is fitted differently as well, so some advance widths land
378+
on other whole pixels and the text sets to slightly different
379+
lines.
351380
</span>
352381
</span></span>
353382
<input id="mono" name="mono" type="checkbox">
@@ -380,6 +409,11 @@
380409
can still come out unchanged, its stems falling where the curve
381410
rounds to nothing, and <strong>weight</strong> is the control that
382411
will move it.
412+
<br><br>
413+
FreeType means this for a screen that blends in linear light,
414+
which this one does not: it has four levels and nothing between
415+
them. Its own documentation warns that the result is heavy and
416+
fuzzy elsewhere. Judge it on the page rather than leaving it on.
383417
</span>
384418
</span></span>
385419
<input id="stem_darkening" name="stem_darkening" type="checkbox">

src/crossglyph/preview/static/js/dom.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,14 @@ export function darkeningReason(outlines, hinting) {
7373
// does any face under light, auto or none. A tricky font is FreeType's own
7474
// exception to that last one -- it is never handed to the auto-hinter, so its
7575
// bytecode runs in every mode that hints at all.
76-
export function grayscaleReason(outlines, bytecode, tricky, hinting) {
76+
//
77+
// Mono rasterizing takes it away for a reason of its own. FreeType turns
78+
// backward compatibility off for a monochrome render (ttgload.c, the
79+
// TT_INTERPRETER_VERSION_40 gate on `mode != FT_RENDER_MODE_MONO`), and
80+
// ttinterp.h calls that falling back to version 35 behaviour. The x-axis
81+
// hacks that separate the two interpreters are what backward compatibility
82+
// gates, so with mono on both settings draw the same glyph.
83+
export function grayscaleReason(outlines, bytecode, tricky, hinting, mono) {
7784
if (outlines && outlines !== "truetype" && outlines !== "mixed") {
7885
return "These are not TrueType outlines, so there is no bytecode for "
7986
+ "either interpreter to run.";
@@ -92,6 +99,10 @@ export function grayscaleReason(outlines, bytecode, tricky, hinting) {
9299
return "The auto-hinter draws it under " + hinting + " hinting, so the "
93100
+ "bytecode interpreter has nothing to do.";
94101
}
102+
if (mono) {
103+
return "FreeType hints as this interpreter does whenever the raster is "
104+
+ "monochrome, so mono rasterizing already gets you it.";
105+
}
95106
return "";
96107
}
97108

@@ -134,7 +145,8 @@ export function syncFeatures() {
134145
const dark = darkeningReason(outlines, hinting.value);
135146
darkening.disabled = Boolean(dark);
136147
darkening.title = dark;
137-
const grey = grayscaleReason(outlines, bytecode, tricky, hinting.value);
148+
const grey = grayscaleReason(outlines, bytecode, tricky, hinting.value,
149+
Boolean(form.elements.mono?.checked));
138150
grayscale.disabled = Boolean(grey);
139151
grayscale.title = grey;
140152
}

tests/preview_persistence.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4200,6 +4200,10 @@ for (const deferred of [
42004200
grayscaleReason("truetype", true, false, "light") !== "");
42014201
check("a family whose faces disagree on format is judged on its bytecode",
42024202
grayscaleReason("mixed", true, false, "normal") === "");
4203+
// FreeType turns backward compatibility off for a monochrome render, which
4204+
// is the difference between the two interpreters, so the choice is moot.
4205+
check("and mono rasterizing takes the row whatever the font is",
4206+
grayscaleReason("truetype", true, false, "normal", true) !== "");
42034207
}
42044208

42054209
// 36. Mono rasterizing leaves a pixel empty or full, so the two knobs that

0 commit comments

Comments
 (0)