Conversation
blend-coverage composites in linear light — correct for images/gradients, but it leaves dark-on-light TEXT edges washed-out (~188 vs 127 grey at half coverage), much lighter than a browser. Browsers blend glyphs in an intermediate gamma (between linear ~2.2 and naive sRGB 1.0). Add *blend-gamma*: NIL keeps the exact sRGB-LUT linear path (default — image/gradient rendering byte-unchanged); a number G composites in power-G space so a text consumer can match browser weight. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Start of TrueType bytecode hinting (docs/HINTING.md): interpret the font's own instructions to grid-fit small text, validated at the point level against FreeType. inspect/hint-oracle.py dumps FreeType's hinted outline (native bytecode hinter, classic interpreter v35, F26Dot6 points per contour) as ground truth. Verified: LiberationSans 'l'@12px hints to a ~1px stem, y 0.00->9.00. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
The TrueType bytecode VM: F26.6/F2.14 fixed point, the rounding engine, graphics state, stack/storage/CVT, ~85 opcodes (push, arith/logic, flow with IF/ELSE/EIF + JMPR/JROT/JROF, FDEF/CALL/LOOPCALL, storage, CVT r/w, GETINFO, ROUND family, DELTAC/DELTAP, all the GS + rounding-state setters). make-hinter sizes stack/ storage from maxp and scales the CVT to the target ppem. Milestone: runs LiberationSans-Regular's full fpgm (71 functions) AND prep at 12px with a balanced stack and no unimplemented opcode — the fixed-point core is proven on real font programs. Still WIP: the glyph pipeline (zones, phantom points) and the point-mover opcodes (MDAP/MIAP/MDRP/MIRP/IP/IUP/…) needed to hint an actual glyph and diff it against the FreeType point-oracle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…WIP) Full end-to-end hinting: load-glyph-zone scales a glyph's points into zone 1, appends the 4 phantom points (origin/advance/top from hmtx+bbox), sets up the twilight zone; hint-glyph resets per-glyph graphics state, runs the glyph program, and extracts the moved points. Point-movers implemented: MDAP, MIAP, MDRP/MIRP (all 32 flag variants each), IP, IUP[x/y], SHP, ALIGNRP, GC, MD, SCFS, CINDEX/MINDEX, the SPVFS/SFVFS/GPV/GFV vector ops, and hmtx-metrics. Milestone: the interpreter runs LiberationSans 'l' at 12px end-to-end and produces a recognizable grid-fit stem — x within ~0.1px of FreeType, stem HEIGHT exact (y 0->9). Remaining to bit-exact: the IUP interpolation (one baseline point lands wrong) and the sub-pixel cut-in/rounding details. This proves the approach: a from-scratch fixed-point TT interpreter hints real glyphs; the rest is oracle-driven debugging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Driving M1 to bit-exact. Three bugs found via point-level diff against the
FreeType oracle:
1. muldiv (FT_MulDiv) ignored the divisor's sign — result sign is sign(a*b*c),
not sign(a*b). Corrupted every projected interpolation (IUP put a baseline
point at y=17).
2. hround applied the "don't collapse to zero" rule to a ZERO input, so
round(0) returned 1px — the baseline landed at y=1. That rule is
min-distance (MDRP/MIRP), not the round function.
3. Phantom points weren't grid-fit: the advance point stayed at 2.67px, so IP
couldn't shift a stem when the advance rounds to 3.00. FT_PIX_ROUND the
phantoms' CURRENT position (ORG stays unrounded for correct measurement).
Result: LiberationSans 'l' @12px hints BIT-EXACT vs FreeType —
(0.91,0)(0.91,9)(1.95,9)(1.95,0), matching the oracle to the F26.6 unit. This
proves the whole pipeline: fixed-point VM + phantom points + MIAP/IP/SHP/IUP.
Other glyphs are within ~1px; the residual is cap/x-height blue-zone snapping in
the MDAP path (FT rounds an 8.25px cap up to 9 via the font's alignment zones),
the next debug target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…n item) Bank the checkpoint: P0-P3 done, M1 proof achieved (l bit-exact vs FreeType), remaining glyphs ~1px off pending cap/x-height blue-zone snapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
M1 closed on the proof: LiberationSans 'l' hints bit-exact vs FreeType. M2's first task is sharply scoped: H's cap MIAPs to cvt[3]=528 (8.25px)->rounds to 8, but FreeType yields 9, so its post-prep CVT differs — a prep-correctness issue (alignment-zone CVT adjustment), not a mover bug (all x-hinting is already exact). Next: diff FreeType's post-prep CVT against ours at 12px. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Durable oracle-diff tooling for M2 (out of /tmp): hint-weftdump.lisp dumps weft's hinted F26.6 points (or the post-prep CVT with "cvt"); hint-diff.py runs it beside the FreeType oracle and reports EXACT / max-Δ per glyph. Bit-exact gate for the interpreter work. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
… bit-exact
Two fixed-point/opcode bugs in prep were pushing the cap/x-height CVTs (and
hence H/o/i/n/e/a) off by up to a full pixel vertically:
1. RUTG (round-up-to-grid) used threshold=64 in the super-round formula, so
hround(0) returned 64 instead of 0 (an exact-grid value wrongly gained a
pixel). Round-up = ceil, whose super-round threshold is period-1 = 63.
2. DELTAC/DELTAP popped the exception spec before the cvt/point index. Per the
TrueType spec the stack after n is "... arg1 idx1 n" with the index nearer the
top, so the index must be popped first. With the swap every per-ppem
exception targeted the wrong entry / never matched ppem, so the cap-height and
x-height CVT deltas at 12px never fired. ('l' stayed exact because no DELTA
fires for it at 12px.)
Post-prep CVT for LiberationSans @12px now matches FreeType's classic v35
interpreter bit-for-bit (verified by reading the scaled cvt array out of
TT_Size via /proc/self/mem). Point error on the cap group collapses:
H 64->4, i 64->1, o 78->14, n 76->12, e 78->14, a 77->19 (F26.6). 'l' still
exact; scribe suite still 10/10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Two more systematic errors that kept every glyph ~1 subpixel off: 1. IP measured original distances from the scaled `org` coords, losing precision (font-unit 136 vs 137 both scale to 51 @12px), so interpolated stems landed a unit off. FreeType's IP ratio uses `orus` (original UNSCALED font-unit coords). Added orus-x/orus-y to the zone, populate for real + phantom points, and use them (dual-projected) for IP's range/dist ratio. 2. MDRP/MIRP had the round and keep-minimum-distance flag bits swapped: the spec (and FreeType) use bit2 (0x04)=round, bit3 (0x08)=min-distance. We used bit3 for round, so e.g. the i-dot MDRP[01001] (min-dist, no round) was wrongly rounding the dot height (511->512). LiberationSans @12px: l H i now bit-exact; o n e a down to 1/64px. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's IUP interpolates untouched interior points using the orus (original font-unit) ratio, not the scaled org, so sub-pixel spacing is preserved; points outside the touched span still shift rigidly by the nearest touched delta (scaled org). Switched iup-axis to take an orus accessor and use it for the interior case + range test. LiberationSans @12px: o, n now bit-exact. Printable-ASCII exact counts jump to ppem7 69/94, ppem10 43/94, ppem12 26/94, ppem16 25/94. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
reset-glyph-gs only reset the vectors/rp/zp/loop, leaking a glyph program's round mode / SMD / SDB / cut-in changes into the next glyph (e.g. 'c' after 'b' went 0.95px off). Snapshot the whole GS after prep and restore it before every glyph, matching FreeType. 'c' and 'g' fixed. ppem12 26->31/94 exact. 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's MDRP/MIRP compute the original distance by scaling the ORUS (font-unit) difference between the two points once (MulFix), not by subtracting two independently-rounded scaled coords. The latter double-rounds and lands ~1/64px off on unrounded (MDRP no-round / min-distance) moves. Added orus-dist and use it for both movers (twilight still falls back to scaled org). ppem exact counts: 7 70->78, 10 47->73, 12 31->54, 16 28->38 (of 94). Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…A axis bits Two bugs, both hit glyphs whose programs act before their first SVTCA: 1. reset-glyph-gs restored the vectors/rp/zp/loop from the post-prep snapshot, but FreeType resets those to the FIXED default (projection=freedom=dual=x-axis, zone 1, rp 0, loop 1) every glyph, keeping only the policy fields (round mode, cut-ins, delta, ...) from prep. This font's prep leaves the vectors on the y axis, so glyphs that DELTAP the advance phantom before any SVTCA (v, M, A, y, z, m, s, w, ...) moved it in y instead of x — the advance and every IP-referenced point came out ~1px wide. Now bit-exact. 2. SPVTCA/SFVTCA (0x02-0x05) had their axis bit inverted (only SVTCA was right); opcode&1 selects the axis (1=x, 0=y). ppem exact: 7 78->84, 10 73->81, 12 54->62, 16 38->55 (of 94). Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
'$' (and other glyphs) call a fpgm function using SHPIX, which was unimplemented and trapped — crashing the dump and making every later glyph look composite. SHPIX shifts each of the loop points by a pixel amount along the freedom vector. ppem exact (of 94 printable ASCII): 7 84, 10 82, 12 62->89, 16 55->79. 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType bases the interior interpolation on the endpoint with the smaller orus coordinate; using the contour-order endpoint rounds the single MulDiv differently (off by 1/64). Base on the smaller-orus reference to match. ppem exact (of 94): 7 87, 10 83, 12 90, 16 90. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…our movers) DejaVu Sans exercises opcodes Liberation didn't: SPVTL/SFVTL (0x06-0x09, set projection/freedom vector along or perpendicular to a line of two points, with FreeType-style unit-vector normalization), ISECT (0x0F, move a point to the intersection of two lines), and SHC/SHZ (0x34-0x37, shift a whole contour / zone by a reference point's displacement). Each was trapping and aborting the dump. DejaVuSans printable-ASCII exact: ppem 7 64, 10 63, 12 65, 16 63 (was 2, crashing on SPVTL). LiberationSans unchanged (90/94 @12). Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…MSIRP Implemented composite-glyph hinting: each component is hinted standalone (its own zone + instructions, via hint-subglyph), then transformed (2x2 F2.14) and offset into the parent (xy offsets grid-fit per ROUND_XY_TO_GRID), components merged, and the composite's own instruction stream run on the assembled outline. parse- composite-components + composite-aware glyph-instructions added; load-glyph-zone routes both simple and composite through glyph-points. Also implemented MSIRP (0x3A/0x3B, move stack indirect relative point), used by accented glyphs. LiberationSans: accented Latin-1 29/52 bit-exact @12px (bases always exact; some accents 1px off on the component offset — refinement pending). ASCII unchanged (90/94 @12). Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…DejaVu) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
No bit-exact fix landed this round; documenting the precise root-cause analysis of the three remaining residual classes so the next pass can resume without redoing the investigation. Key results: DejaVu post-prep CVT confirmed bit-exact vs FreeType; the diagonal blow-up is a projection-amplified MIRP whose divergence needs FreeType interpreter introspection; the LiberationSans 1/64 misses and the composite-accent 1px offsets both trace to the same IUP interior interpolation edge. Suite 10/10; Liberation ASCII unchanged (87/90/90 @7/12/16), DejaVu ~63-65/94. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…er blow up) Found by per-instruction execution diff against a trace-enabled FreeType (see inspect/hint-fttrace.md): at DejaVu '/'s first diagonal MIRP, weft's projection after SPVTL was FreeType's rotated 90° — (4734,15697) vs (-15686,4730). The parallel/perpendicular bit was inverted: FreeType rotates when `opcode & 1` (SPVTL[1]/0x07 = perpendicular), weft rotated on the EVEN opcode. So weft built a near-vertical projection where FreeType's is near-horizontal, and the freedom-x MIRP move amplified ×(16384/4734)=3.46 → '/' pt0 landed at -937 vs FT 189. Fix: rotate on the odd opcode. DejaVu '/' \\ A y: 17.6px off -> 1/64px; DejaVu ASCII @12 65 -> 69/94; LiberationSans unchanged (its diagonals already used the even/parallel path). Adds the weft-side per-instruction STATE trace (env WEFT_HINT_TRACE) mirroring the patched FreeType build, for execution diffs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…ation norm14 used isqrt, giving projection vectors ~9/16384 too long; that cascaded into 1-unit MIRP errors on diagonal glyphs (DejaVu / \ A y off by 1/64 even after the SPVTL parallel/perp fix). Ported FreeType's Normalize + FT_Vector_NormLen exactly (Newton-Raphson normalize to 0x10000, then /4) so projection vectors match bit-for-bit — verified via the per-instruction STATE trace (weft proj (15721,-4643) -> FreeType's (15713,-4640)). DejaVu V, X now bit-exact; / \ A y down to 1/64. Also trace into CALL'd fpgm functions (weft's call-fn now emits STATE) so the per-instruction diff aligns with FreeType. Liberation unchanged (90/94 @12); 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
The projection dot product summed two mul2.14 results, rounding each term separately; FreeType's TT_DotFix14 sums ax*bx + ay*by and rounds ONCE. The double rounding differed by 1 on diagonal projections (e.g. pt1 of DejaVu '/': 83 vs FreeType's 82), cascading into 1-unit MIRP moves. Found via the per-instruction STATE trace. DejaVu ASCII @12 70->75, @16 68->77; 'w' now bit-exact; Liberation unchanged (90/94 @12). Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…nce vector
FreeType's MDRP/MIRP compute cur_dist = PROJECT(cur[point] - cur[rp0]) — projecting
the difference vector once — whereas weft subtracted two independently projected
points (project(p) - project(rp0)), which differs by a rounding unit on diagonals
(DejaVu '/': -60 vs -61) and cascaded into 1-unit mover errors. Added cur-dist
(and org-dist) helpers and rewrote the MDRP/MIRP move as (distance - cur_dist).
Root-caused via the per-instruction STATE trace.
DejaVu ASCII @7/12/16: 71/75/77 -> 90/88/88 ('/' '\' A w y V X now bit-exact).
LiberationSans unchanged (87/90/90). Suite 10/10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…ction Applied the same PROJECT/DUALPROJ-of-the-difference fix to the remaining distance ops: IP's ranges/dists, MD[0]/MD[1], ALIGNRP, and MSIRP now project the difference vector once (org via ORUS font units, current via scaled cur) instead of subtracting two independently-projected points. Added orus-diff helper. DejaVu ASCII @7/12/16: 90/88/88 -> 92/93/92. LiberationSans unchanged (87/90/90). Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's iup_worker_interpolate_ is a hybrid: it sorts the two touched refs by ORUS, but classifies each untouched point (below ref1 / above ref2 / interior) using the SCALED org coordinate, while the interior interpolation ratio uses ORUS (font units) via DivFix->MulFix. weft used orus for the boundary too, so a point sharing a ref's scaled position (e.g. b pt6: orus 316 vs ref 318, but both scale to 119) was interpolated instead of shifting with the ref -> off by 1/64. Ported FreeType's routine exactly. LiberationSans ASCII @7/12/16: 87/90/90 -> 94/94/93 (b g % & now bit-exact); DejaVuSans 92/93/92 -> 93/93/94. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
MIRP applied the control-value cut-in using the un-flipped cvt, so a cvt and org of opposite sign (|cvt-org| huge) wrongly triggered the cut-in (cvt=org) — e.g. LiberationSans '%'@16 pt22 came out 1px low. FreeType auto-flips cvt to org's sign FIRST, then tests the cut-in (same-zone) on the flipped value. Reordered to match, and switched MDRP/MIRP min-distance to FreeType's asymmetric clamp (min-distance helper). Kept orus-dist for the original distance. LiberationSans ASCII now 94/94 at every ppem 7-16. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…PROJ,scale)) FreeType's MDRP/MIRP/IP original distance dual-projects the ORUS (font-unit) difference first, then scales (MulFix by x_scale, for x_scale==y_scale); weft scaled each component before dual-projecting, which rounds differently for diagonal projection vectors. Now uses hscale(orus-diff). No count change but FreeType-exact; Liberation 94/94 all ppem, suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
FreeType's Compute_Point_Displacement projects the reference point's (cur - org) difference with the PROJECTION vector; weft computed project(cur) - dualproj(org) (mixing projection/dual and subtracting two projections), off by 1 on diagonals. Fixed sh-ref to dot14(cur-org, projVector). DejaVuSans ASCII @10/12: 93 -> 94/94. Liberation 94/94 all ppem. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
When the freedom and projection vectors are near-perpendicular, F_dot_P (their dot product) is ~0; FreeType's Compute_Funcs clamps |F_dot_P| < 0x400 to 0x4000 and still moves. weft skipped the move when fdotp was 0, so a diagonal MIRP with freedom perpendicular to the projection (DejaVu '$' @7px) didn't move the point (off by 1px). Added the clamp. DejaVuSans ASCII @7 -> 94/94. Liberation 94/94 all ppem. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
DIV (0x62) used muldiv(a,b,64) — identical to MUL — instead of TrueType's (n1*64)/n2 = muldiv(a,64,b). This corrupted any fpgm arithmetic using DIV; found via the STATE trace on LiberationSans 'é', whose accent-alignment function branched the wrong way (a DIV gave -46 vs FreeType's -12), leaving the accent 1px off. LiberationSans accented Latin-1 now 53/53 bit-exact at ppem 7/12/16 (composites fully exact). Printable ASCII still 94/94 both fonts. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
… + caching (M3) rasterize-glyph gains a hinted path: when *hinting* applies (T = all sizes, or a number N = only ppem<=N), the glyph is grid-fit via the bytecode interpreter and rasterized at scale 1 on the integer pixel grid, with the grid-fit (integer) advance so painted widths stay even — no sub-pixel drift. Falls back to the geometric path for composites we can't hint, variations, or any error. - hint-glyph now also returns the hinted advance (phantom pp2-pp1). - hinted-segments / hinted-advance: memoized per (font,gid,ppem) over a per- (font,ppem) hinter cache (fpgm+prep run once); installed into raster via the *hinted-segments-fn* hook so raster.lisp needn't depend on the interpreter. - Default *hinting* nil: the geometric path and the whole suite are unchanged (10/10). Small text renders crisp: stems/baseline land on pixel boundaries instead of smearing across fractional rows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
… spacing The first M3 cut used integer advances + integer placement for hinted glyphs, which quantized inter-glyph spacing and read as bad kerning at small sizes. Match the browser instead: keep the grid-fit (it matters vertically — crisp stems/baseline) but rasterize the hinted outline with the sub-pixel x-shift and advance by the fractional (geometric) advance, so horizontal spacing stays smooth. hint-glyph's advance value is no longer used by the raster path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
otl-lookup-indices reparsed the script/feature/lookup tables on every call; shaping a page word-by-word made that quadratic. Split into a cached wrapper + core, keyed by (tag . features) on a new font %otl slot (like %gdef/%cmap). Shape a page's lookups once, not once per word. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
…al X *hint-light* returns the outline with hinted Y but the original (unhinted) scaled X, so baseline/x-height/stem heights snap crisp while horizontal stems stay sub-pixel. Full hinting's X grid-fit is crisp but choppy — uneven inter-letter rhythm; light matches the browser's default Linux rendering. Measured on a controlled sample vs Chromium, light halves the per-pair ink-width error (21 -> 12 over 30 kern pairs). Default nil (full); the hinted-glyph cache keys on the mode. Suite 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JA1saK7BkgavHNeurmK65k
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a from-scratch TrueType bytecode interpreter so scribe can grid-fit glyph
outlines before rasterizing. At small ppem (~7–12px) unhinted outlines land on
fractional pixel rows and stems smear; hinting snaps stems/baseline/x-height to the
grid so small body text renders crisp.
What's here
src/hint.lisp— the interpreter: F26Dot6/F2Dot14 fixed-point, graphicsstate, stack, storage, scaled CVT, the rounding engine (RTG/RTHG/RTDG/RUTG/ROFF/
SROUND…), ~110 opcodes (push/arith/logic, GS setters, CVT r/w, flow with
FDEF/CALL/LOOPCALL, DELTAP/DELTAC, and the point movers MDAP/MIAP/MDRP/MIRP/IP/
IUP/SHP/SHPIX/ALIGNRP/ISECT/…). Phantom points, twilight + glyph zones, composites
(component placement + ROUND_XY_TO_GRID + the composite's own instructions).
*hint-light*grid-fits vertically but keeps theoriginal (fractional) X, so horizontal stem rhythm stays smooth (like the browser's
default Linux rendering); full mode grid-fits both axes (crisper, choppier).
rasterize-glyphintegration —*hinting*(T, nil, or a max-ppem); hintedoutlines rasterize at scale 1, sub-pixel positioned, memoized per (font, gid, ppem,
mode) over a per-(font, ppem) hinter cache. Default off; the geometric path is
unchanged.
otl.lisp— cache GSUB/GPOS lookup indices on the font (new%otlslot), soshaping a page parses the feature/lookup tables once, not once per word.
blend.lisp— optional gamma-space glyph compositing (*blend-gamma*).Validation
level: all 94 printable ASCII glyphs hint bit-exact on both LiberationSans and
DejaVuSans across ppem 7–16. Harness + FreeType oracle under
inspect/(
hint-oracle.py,hint-diff.py,hint-weftdump.lisp); plan + notes indocs/HINTING.md.inspect/run-all.sh, 10/10).diacritics remain ~1/64px off (documented).
Notes
Grayscale smooth-mode target (not ClearType). Interpreter-only where possible;
rasterizer/shaping/compositing changes are additive and behind flags.