code wiki / _hdl_build / nx_ttf_fontlib.nx
nx_ttf_fontlib.nx source
↩ module page · 601 lines · 29060 B
1// nx_ttf_fontlib.nx -- the sovereign TrueType FONT ENGINE as a LIBRARY (the browser's text renderer).
2// Canonical home of the TTF read path first proven in the nx_ttf_raster demo (which has a main() and so
3// cannot be imported): sfnt dir -> head/cmap/loca/glyf/hhea/hmtx(+kern), cmap format-4 (BOTH the delta
4// path and the idRangeOffset/glyphIdArray path), glyf outline decode (flags/short/same/repeat), COMPOSITE
5// glyphs (component offsets + F2Dot14 scale, bounded recursion), quadratic Bezier flattening, NONZERO-
6// WINDING scan conversion with SSxSS supersampled ANTI-ALIASED coverage, a per-(char,size) COVERAGE CACHE
7// (each glyph rasterizes once per size), REAL per-glyph advances from hmtx, pair KERNING from the 'kern'
8// table (format 0, binary search), a mild contrast curve (stem darkening at small sizes), and an
9// RGBA-framebuffer ALPHA-BLEND draw (nx_framebuffer px = R,G,B,A).
10//
11// 2026-09-02 (BR4 first half, /compare/browser rung "Real font files + shaping"): EVERY METRIC IS NOW
12// READ FROM THE FONT, NEVER ASSUMED. The first cut hard-coded unitsPerEm=1000 and ascender=760 -- true
13// of our own emitted nishi_sans.ttf and FALSE of every professional font (Liberation Sans, Arial, Noto:
14// 2048), so a real face would have rasterized at HALF size with its advances halved, and nothing would
15// have failed loudly. unitsPerEm comes from head, ascender/descender/lineGap from hhea, the cell height
16// is DERIVED from (ascender - descender), and the browser's 1000-em advance unit is a conversion at the
17// API edge (tf_adv_em1000), not an assumption inside the rasterizer. A negative left side bearing
18// ('j', '_', 'w' in Liberation) is carried as the record's left margin so no ink is clipped, and ink
19// wider than the advance ('f', 'k') widens the cell instead of being cut at the advance.
20// Fonts proven: our own emitted web_assets/nishi_sans.ttf (96 glyphs, upem 1000) and Liberation Sans
21// (SIL-OFL, upem 2048, 908 kern pairs, 1,076 composite glyphs). No FreeType, no GDI, no DirectWrite.
22// license_tier: ORIGINAL
23import "nx_syscalls.nx"
24import "nx_paint_solid_rect.nx"
25const TF_MAGIC_32768: i64 = 32768
26const TF_MAGIC_65536: i64 = 65536
27const TF_MAGIC_16777216: i64 = 16777216
28const TF_MAGIC_2048: i64 = 2048
29const TF_F2DOT14_ONE: i64 = 16384 // 1.0 in the F2Dot14 fixed-point the glyf composite scale fields use
30
31// handle slots (one i64 each)
32const TF_H_BUF: i64 = 0
33const TF_H_GLYF: i64 = 1
34const TF_H_LOCA: i64 = 2
35const TF_H_LOCFMT: i64 = 3
36const TF_H_CMAPSUB: i64 = 4
37const TF_H_HMTX: i64 = 5
38const TF_H_NUMHM: i64 = 6
39const TF_H_CACHE: i64 = 7
40const TF_H_NSIZES: i64 = 8
41const TF_H_SIZE0: i64 = 9 // 9..9+TF_MAXSZ-1 = the pixel heights with a cache slot
42const TF_MAXSZ: i64 = 16
43const TF_H_UPEM: i64 = 25 // head.unitsPerEm -- READ, never assumed
44const TF_H_ASC: i64 = 26 // hhea.ascender (font units, positive up)
45const TF_H_DESC: i64 = 27 // hhea.descender (font units, negative)
46const TF_H_LINEGAP: i64 = 28 // hhea.lineGap
47const TF_H_KPAIRS: i64 = 29 // byte offset of the kern format-0 pair array (0 = no horizontal kern table)
48const TF_H_KN: i64 = 30 // number of kern pairs
49const TF_H_LEN: i64 = 31 // byte length of the font buffer (every table offset is checked against it)
50const TF_H_NGLYPHS: i64 = 32 // maxp.numGlyphs (bounds every gid before loca is indexed)
51const TF_HSLOTS: i64 = 40
52
53// coverage-record slots (per cached (size, char))
54const TF_R_COV: i64 = 0 // coverage bytes, cw x chh, row-major; row 0 = the ASCENDER line
55const TF_R_CW: i64 = 1 // cell width px
56const TF_R_CHH: i64 = 2 // cell height px = ((asc - desc) * ph) / upem + 1
57const TF_R_ADV: i64 = 3 // advance px at this size (hmtx scaled)
58const TF_R_LM: i64 = 4 // left margin px: the cell's x0 sits this far LEFT of the pen (negative lsb)
59const TF_R_PH: i64 = 5
60const TF_REC: i64 = 8 // i64 slots per record
61
62const TF_ASCII_LO: i64 = 32
63const TF_ASCII_HI: i64 = 126
64const TF_ASCII_N: i64 = TF_ASCII_HI - TF_ASCII_LO + 1
65const TF_EM1000: i64 = 1000 // the browser's advance unit (font_adv_em speaks 1000-em)
66const TF_SS: i64 = 4 // supersample factor per axis (SS*SS samples per pixel)
67const TF_MAXE: i64 = 500 // flattened-edge capacity per glyph
68const TF_BEZ_STEPS: i64 = 6 // quadratic Bezier flattening steps
69const TF_COMPOSITE_DEPTH: i64 = 4 // bounded recursion for nested composites
70const TF_CONTRAST_DIV: i64 = 170 // stem-darkening curve: a += a*(255-a)/170
71const TF_KERN_SUBHDR: i64 = 14 // kern format-0 subtable header bytes before the pair array
72const TF_KERN_PAIR: i64 = 6 // left(2) right(2) value(2)
73const TF_CMAP_REC: i64 = 8 // encoding record: platform(2) encoding(2) offset(4)
74const TF_MIN_ADV_PX: i64 = 2
75
76func tf_rd16(b: *u8, o: i64) -> i64 { return (b[o] as i64)*256 + (b[o+1] as i64) }
77func tf_rd16s(b: *u8, o: i64) -> i64 { let v: i64=tf_rd16(b,o); if v>=TF_MAGIC_32768 { return v-TF_MAGIC_65536 } return v }
78func tf_rd32(b: *u8, o: i64) -> i64 { return (b[o] as i64)*TF_MAGIC_16777216 + (b[o+1] as i64)*TF_MAGIC_65536 + (b[o+2] as i64)*256 + (b[o+3] as i64) }
79func tf_bit(f: i64, m: i64) -> i64 { if (f/m)%2 == 1 { return 1 } return 0 }
80// signed 8-bit
81func tf_rd8s(b: *u8, o: i64) -> i64 { let v: i64 = b[o] as i64; if v >= 128 { return v - 256 } return v }
82
83func tf_find_table(b: *u8, t0: i64, t1: i64, t2: i64, t3: i64) -> i64 {
84 let nt: i64=tf_rd16(b,4)
85 var i: i64=0
86 while i<nt { let d: i64=12+i*16
87 if (b[d] as i64)==t0 { if (b[d+1] as i64)==t1 { if (b[d+2] as i64)==t2 { if (b[d+3] as i64)==t3 { return tf_rd32(b,d+8) } } } }
88 i=i+1 }
89 return 0 - 1
90}
91// cmap format-4 lookup: BOTH paths. idRangeOffset==0 -> gid = (c + idDelta) mod 65536;
92// idRangeOffset!=0 -> gid comes from glyphIdArray at &idRangeOffset[i] + idRangeOffset[i] + 2*(c-start),
93// and a 0 there means .notdef (idDelta is NOT applied to 0). The first cut returned 0 for every
94// glyphIdArray segment, which is where fonts put their scattered ranges.
95func tf_cmap_lookup(b: *u8, sub: i64, c: i64) -> i64 {
96 let segX2: i64=tf_rd16(b,sub+6)
97 let segCount: i64=segX2/2
98 var i: i64=0
99 while i<segCount {
100 let endc: i64=tf_rd16(b,sub+14+i*2)
101 if endc>=c {
102 let startc: i64=tf_rd16(b,sub+16+segX2+i*2)
103 if startc<=c {
104 let iro_at: i64 = sub+16+segX2*3+i*2
105 let iro: i64=tf_rd16(b,iro_at)
106 let delta: i64=tf_rd16s(b,sub+16+segX2*2+i*2)
107 if iro==0 { return (c+delta)%TF_MAGIC_65536 }
108 let g: i64 = tf_rd16(b, iro_at + iro + (c-startc)*2)
109 if g == 0 { return 0 }
110 return (g+delta)%TF_MAGIC_65536
111 }
112 return 0
113 }
114 i=i+1
115 }
116 return 0
117}
118// pick the format-4 subtable: scan the encoding records rather than trusting record 0 (a font may list a
119// format-6 or format-12 table first). Returns the absolute subtable offset, or -1 when no format-4 exists.
120func tf_cmap_pick4(b: *u8, cmap: i64, len: i64) -> i64 {
121 let n: i64 = tf_rd16(b, cmap+2)
122 var i: i64 = 0
123 while i < n {
124 let rec: i64 = cmap + 4 + i*TF_CMAP_REC
125 let off: i64 = tf_rd32(b, rec+4)
126 let sub: i64 = cmap + off
127 if sub + 8 < len { if tf_rd16(b, sub) == 4 { return sub } }
128 i = i + 1
129 }
130 return 0 - 1
131}
132
133// append one straight edge (scaled/offset by the composite transform when nested)
134func tf_edge_push(ex0: *i64, ey0: *i64, ex1: *i64, ey1: *i64, ne: i64, x0: i64, y0: i64, x1: i64, y1: i64) -> i64 {
135 if ne >= TF_MAXE { return ne }
136 ex0[ne]=x0; ey0[ne]=y0; ex1[ne]=x1; ey1[ne]=y1
137 return ne+1
138}
139// decode glyph gid -> flattened line edges. returns edge count (max TF_MAXE). Coordinates are transformed
140// by (a,b,c,d as F2Dot14; dx,dy font units): x' = (a*x + c*y)/16384 + dx, y' = (b*x + d*y)/16384 + dy --
141// identity for a simple glyph called directly. Composite glyphs recurse per component with depth bounded.
142func tf_glyph_edges_x(b: *u8, glyf: i64, loca: i64, locfmt: i64, nglyphs: i64, gid: i64,
143 ex0: *i64, ey0: *i64, ex1: *i64, ey1: *i64, ne0: i64,
144 ta: i64, tb: i64, tc: i64, td: i64, tdx: i64, tdy: i64, depth: i64) -> i64 {
145 var ne: i64 = ne0
146 if gid < 0 { return ne }
147 if gid >= nglyphs { return ne }
148 var g0: i64=0
149 var g1: i64=0
150 if locfmt==0 { g0=tf_rd16(b,loca+gid*2)*2; g1=tf_rd16(b,loca+gid*2+2)*2 } else { g0=tf_rd32(b,loca+gid*4); g1=tf_rd32(b,loca+gid*4+4) }
151 if g1<=g0 { return ne }
152 let gp: i64=glyf+g0
153 let nc: i64=tf_rd16s(b,gp)
154 if nc < 0 {
155 // COMPOSITE: components until MORE_COMPONENTS (bit 5) clears
156 if depth >= TF_COMPOSITE_DEPTH { return ne }
157 var cp: i64 = gp + 10
158 var more: i64 = 1
159 while more == 1 {
160 let flags: i64 = tf_rd16(b, cp)
161 let cgid: i64 = tf_rd16(b, cp+2)
162 cp = cp + 4
163 var cdx: i64 = 0
164 var cdy: i64 = 0
165 if tf_bit(flags, 1) == 1 { cdx = tf_rd16s(b, cp); cdy = tf_rd16s(b, cp+2); cp = cp + 4 } else { cdx = tf_rd8s(b, cp); cdy = tf_rd8s(b, cp+1); cp = cp + 2 } // ARG_1_AND_2_ARE_WORDS
166 if tf_bit(flags, 2) == 0 { cdx = 0; cdy = 0 } // ARGS_ARE_XY_VALUES clear = point matching: unsupported, treated as no offset
167 var ca: i64 = TF_F2DOT14_ONE
168 var cb: i64 = 0
169 var cc: i64 = 0
170 var cd: i64 = TF_F2DOT14_ONE
171 if tf_bit(flags, 8) == 1 { ca = tf_rd16s(b, cp); cd = ca; cp = cp + 2 } else { if tf_bit(flags, 64) == 1 { ca = tf_rd16s(b, cp); cd = tf_rd16s(b, cp+2); cp = cp + 4 } else { if tf_bit(flags, 128) == 1 { ca = tf_rd16s(b, cp); cb = tf_rd16s(b, cp+2); cc = tf_rd16s(b, cp+4); cd = tf_rd16s(b, cp+6); cp = cp + 8 } } } // WE_HAVE_A_SCALE / X_AND_Y_SCALE / TWO_BY_TWO
172 // compose the component transform with the caller's: first component, then caller
173 let na: i64 = (ta*ca + tc*cb) / TF_F2DOT14_ONE
174 let nb: i64 = (tb*ca + td*cb) / TF_F2DOT14_ONE
175 let ncc: i64 = (ta*cc + tc*cd) / TF_F2DOT14_ONE
176 let nd: i64 = (tb*cc + td*cd) / TF_F2DOT14_ONE
177 let ndx: i64 = (ta*cdx + tc*cdy) / TF_F2DOT14_ONE + tdx
178 let ndy: i64 = (tb*cdx + td*cdy) / TF_F2DOT14_ONE + tdy
179 ne = tf_glyph_edges_x(b, glyf, loca, locfmt, nglyphs, cgid, ex0, ey0, ex1, ey1, ne, na, nb, ncc, nd, ndx, ndy, depth+1)
180 if tf_bit(flags, 32) == 0 { more = 0 }
181 }
182 return ne
183 }
184 if nc == 0 { return ne }
185 let ep: *i64=sys_mmap(8*(nc+1)) as *i64
186 var i: i64=0
187 while i<nc { ep[i]=tf_rd16(b,gp+10+i*2); i=i+1 }
188 let np: i64=ep[nc-1]+1
189 let instr: i64=tf_rd16(b,gp+10+nc*2)
190 var p: i64=gp+10+nc*2+2+instr
191 let FL: *i64=sys_mmap(8*(np+1)) as *i64
192 let PX: *i64=sys_mmap(8*(np+1)) as *i64
193 let PY: *i64=sys_mmap(8*(np+1)) as *i64
194 let ON: *i64=sys_mmap(8*(np+1)) as *i64
195 var fi: i64=0
196 while fi<np { let f: i64=b[p] as i64; p=p+1; FL[fi]=f; fi=fi+1
197 if tf_bit(f,8)==1 { var rep: i64=b[p] as i64; p=p+1; while rep>0 { if fi<np { FL[fi]=f; fi=fi+1 } rep=rep-1 } } }
198 var x: i64=0
199 i=0
200 while i<np { let f: i64=FL[i]
201 if tf_bit(f,2)==1 { var dx: i64=b[p] as i64; p=p+1; if tf_bit(f,16)==0 { dx=0-dx } x=x+dx }
202 else { if tf_bit(f,16)==0 { x=x+tf_rd16s(b,p); p=p+2 } }
203 PX[i]=x; i=i+1 }
204 var y: i64=0
205 i=0
206 while i<np { let f: i64=FL[i]
207 if tf_bit(f,4)==1 { var dy: i64=b[p] as i64; p=p+1; if tf_bit(f,32)==0 { dy=0-dy } y=y+dy }
208 else { if tf_bit(f,32)==0 { y=y+tf_rd16s(b,p); p=p+2 } }
209 PY[i]=y; ON[i]=tf_bit(f,1); i=i+1 }
210 // apply the transform in place (identity when called directly)
211 i=0
212 while i<np {
213 let ox: i64 = PX[i]
214 let oy: i64 = PY[i]
215 PX[i] = (ta*ox + tc*oy) / TF_F2DOT14_ONE + tdx
216 PY[i] = (tb*ox + td*oy) / TF_F2DOT14_ONE + tdy
217 i=i+1
218 }
219 var cs: i64=0
220 var ci: i64=0
221 while ci<nc {
222 let ce: i64=ep[ci]
223 let cnt: i64=ce-cs+1
224 if cnt >= 1 {
225 // a contour that starts OFF-curve: start from the implied on-curve midpoint (or the last on point)
226 var sidx: i64 = cs
227 var curx: i64=PX[cs]
228 var cury: i64=PY[cs]
229 if ON[cs] == 0 {
230 let lidx: i64 = ce
231 if ON[lidx] == 1 { curx = PX[lidx]; cury = PY[lidx] }
232 else { curx = (PX[cs] + PX[lidx]) / 2; cury = (PY[cs] + PY[lidx]) / 2 }
233 sidx = cs - 1
234 }
235 var j: i64=1
236 while j<=cnt {
237 let idx: i64=cs + (((sidx - cs) + j) % cnt)
238 if ON[idx]==1 {
239 ne = tf_edge_push(ex0, ey0, ex1, ey1, ne, curx, cury, PX[idx], PY[idx])
240 curx=PX[idx]; cury=PY[idx]; j=j+1
241 } else {
242 let eidx: i64=cs + (((sidx - cs) + j + 1)%cnt)
243 let cxp: i64=PX[idx]
244 let cyp: i64=PY[idx]
245 var exp: i64=PX[eidx]
246 var eyp: i64=PY[eidx]
247 var consumed: i64 = 2
248 if ON[eidx]==0 { exp = (cxp + PX[eidx]) / 2; eyp = (cyp + PY[eidx]) / 2; consumed = 1 } // two off-curve points imply an on-curve midpoint
249 var t: i64=1
250 var pxp: i64=curx
251 var pyp: i64=cury
252 let SQ: i64 = TF_BEZ_STEPS*TF_BEZ_STEPS
253 while t<=TF_BEZ_STEPS {
254 let u: i64=TF_BEZ_STEPS-t
255 let qx: i64=(u*u*curx + 2*u*t*cxp + t*t*exp)/SQ
256 let qy: i64=(u*u*cury + 2*u*t*cyp + t*t*eyp)/SQ
257 ne = tf_edge_push(ex0, ey0, ex1, ey1, ne, pxp, pyp, qx, qy)
258 pxp=qx; pyp=qy; t=t+1
259 }
260 curx=exp; cury=eyp; j=j+consumed
261 }
262 }
263 }
264 cs=ce+1; ci=ci+1
265 }
266 return ne
267}
268// the simple entry every caller uses: identity transform, depth 0
269func tf_glyph_edges(b: *u8, glyf: i64, loca: i64, locfmt: i64, nglyphs: i64, gid: i64, ex0: *i64, ey0: *i64, ex1: *i64, ey1: *i64) -> i64 {
270 return tf_glyph_edges_x(b, glyf, loca, locfmt, nglyphs, gid, ex0, ey0, ex1, ey1, 0, TF_F2DOT14_ONE, 0, 0, TF_F2DOT14_ONE, 0, 0, 0)
271}
272func tf_inside(gx: i64, gy: i64, ex0: *i64, ey0: *i64, ex1: *i64, ey1: *i64, ne: i64) -> i64 {
273 var w: i64=0
274 var i: i64=0
275 while i<ne {
276 let y0: i64=ey0[i]
277 let y1: i64=ey1[i]
278 var dir: i64=0
279 if y0<=gy { if gy<y1 { dir=1 } }
280 if y1<=gy { if gy<y0 { dir=0-1 } }
281 if dir!=0 {
282 let xc: i64=ex0[i] + (gy-y0)*(ex1[i]-ex0[i])/(y1-y0)
283 if xc>gx { w=w+dir }
284 }
285 i=i+1
286 }
287 if w!=0 { return 1 }
288 return 0
289}
290
291// parse a .ttf already in memory (b, len). returns handle (as *i64), or 0 on failure. This is the ONE
292// parse core; tf_load reads the file then calls this. Surfaces whose keystone lacks sys_read_file (the
293// native-Windows GUI PE) read the bytes their own way (win32 CreateFileA/ReadFile) and call this directly.
294// Every table offset is checked against len so a truncated or foreign file refuses instead of reading
295// past its buffer.
296func tf_load_buf(b: *u8, len: i64) -> *i64 {
297 if (b as i64) == 0 { return 0 as *i64 }
298 if len <= 12 { return 0 as *i64 }
299 let nt: i64 = tf_rd16(b, 4)
300 if 12 + nt*16 > len { return 0 as *i64 }
301 let glyf: i64 = tf_find_table(b, 103,108,121,102)
302 let loca: i64 = tf_find_table(b, 108,111,99,97)
303 let head: i64 = tf_find_table(b, 104,101,97,100)
304 let cmap: i64 = tf_find_table(b, 99,109,97,112)
305 let hhea: i64 = tf_find_table(b, 104,104,101,97)
306 let hmtx: i64 = tf_find_table(b, 104,109,116,120)
307 let maxp: i64 = tf_find_table(b, 109,97,120,112)
308 if glyf<0 { return 0 as *i64 }
309 if loca<0 { return 0 as *i64 }
310 if head<0 { return 0 as *i64 }
311 if cmap<0 { return 0 as *i64 }
312 if hhea<0 { return 0 as *i64 }
313 if hmtx<0 { return 0 as *i64 }
314 if maxp<0 { return 0 as *i64 }
315 if glyf >= len { return 0 as *i64 }
316 if loca >= len { return 0 as *i64 }
317 if head + 54 > len { return 0 as *i64 }
318 if cmap + 4 > len { return 0 as *i64 }
319 if hhea + 36 > len { return 0 as *i64 }
320 if hmtx >= len { return 0 as *i64 }
321 if maxp + 6 > len { return 0 as *i64 }
322 let upem: i64 = tf_rd16(b, head+18)
323 if upem <= 0 { return 0 as *i64 }
324 let sub: i64 = tf_cmap_pick4(b, cmap, len)
325 if sub < 0 { return 0 as *i64 }
326 let fh: *i64 = sys_mmap(8*TF_HSLOTS) as *i64
327 fh[TF_H_BUF] = b as i64
328 fh[TF_H_LEN] = len
329 fh[TF_H_GLYF] = glyf
330 fh[TF_H_LOCA] = loca
331 fh[TF_H_LOCFMT] = tf_rd16s(b, head+50)
332 fh[TF_H_CMAPSUB] = sub
333 fh[TF_H_HMTX] = hmtx
334 fh[TF_H_NUMHM] = tf_rd16(b, hhea+34)
335 fh[TF_H_NGLYPHS] = tf_rd16(b, maxp+4)
336 fh[TF_H_CACHE] = (sys_mmap(TF_MAXSZ*TF_ASCII_N*TF_REC*8) as *i64) as i64
337 fh[TF_H_NSIZES] = 0
338 fh[TF_H_UPEM] = upem
339 fh[TF_H_ASC] = tf_rd16s(b, hhea+4)
340 fh[TF_H_DESC] = tf_rd16s(b, hhea+6)
341 fh[TF_H_LINEGAP] = tf_rd16s(b, hhea+8)
342 fh[TF_H_KPAIRS] = 0
343 fh[TF_H_KN] = 0
344 if fh[TF_H_NUMHM] <= 0 { return 0 as *i64 }
345 if fh[TF_H_ASC] <= fh[TF_H_DESC] { return 0 as *i64 }
346 // 'kern' table, subtable 0, format 0, horizontal: the pair array is sorted by (left<<16 | right)
347 let kern: i64 = tf_find_table(b, 107,101,114,110)
348 if kern >= 0 { if kern + 4 + TF_KERN_SUBHDR <= len {
349 let ntab: i64 = tf_rd16(b, kern+2)
350 if ntab >= 1 {
351 let st: i64 = kern + 4
352 let cov: i64 = tf_rd16(b, st+4)
353 if cov / 256 == 0 { if cov % 2 == 1 {
354 let npairs: i64 = tf_rd16(b, st+6)
355 if st + TF_KERN_SUBHDR + npairs*TF_KERN_PAIR <= len { fh[TF_H_KPAIRS] = st + TF_KERN_SUBHDR; fh[TF_H_KN] = npairs }
356 } }
357 }
358 } }
359 return fh
360}
361
362// load + parse a .ttf from a file path (needs sys_read_file -- WSL/Linux/NishiOS). returns 0 on failure.
363func tf_load(path: *u8) -> *i64 {
364 let lp: *i64 = sys_mmap(16) as *i64
365 lp[0] = 0 - 1
366 let b: *u8 = sys_read_file(path, lp)
367 if lp[0] <= 12 { return 0 as *i64 }
368 return tf_load_buf(b, lp[0])
369}
370
371// ---- metrics API (the numbers a layout engine needs; all READ from the loaded face) ----
372func tf_upem(fh: *i64) -> i64 { return fh[TF_H_UPEM] }
373func tf_asc(fh: *i64) -> i64 { return fh[TF_H_ASC] }
374func tf_desc(fh: *i64) -> i64 { return fh[TF_H_DESC] }
375func tf_linegap(fh: *i64) -> i64 { return fh[TF_H_LINEGAP] }
376func tf_kern_count(fh: *i64) -> i64 { return fh[TF_H_KN] }
377func tf_nglyphs(fh: *i64) -> i64 { return fh[TF_H_NGLYPHS] }
378// ascender / descender / content height in px at pixel-em ph (descender returned as a POSITIVE depth)
379func tf_asc_px(fh: *i64, ph: i64) -> i64 { return (fh[TF_H_ASC]*ph) / fh[TF_H_UPEM] }
380func tf_desc_px(fh: *i64, ph: i64) -> i64 { return ((0 - fh[TF_H_DESC])*ph) / fh[TF_H_UPEM] }
381func tf_content_px(fh: *i64, ph: i64) -> i64 { return ((fh[TF_H_ASC] - fh[TF_H_DESC])*ph) / fh[TF_H_UPEM] }
382// glyph id for a codepoint (0 = .notdef)
383func tf_gid(fh: *i64, ch: i64) -> i64 { return tf_cmap_lookup(fh[TF_H_BUF] as *u8, fh[TF_H_CMAPSUB], ch) }
384// advance width in FONT UNITS for a gid (hmtx; gids past numHM share the last advance)
385func tf_adv_units_gid(fh: *i64, gid: i64) -> i64 {
386 var hmi: i64 = gid
387 if hmi >= fh[TF_H_NUMHM] { hmi = fh[TF_H_NUMHM]-1 }
388 if hmi < 0 { hmi = 0 }
389 let at: i64 = fh[TF_H_HMTX] + hmi*4
390 if at + 2 > fh[TF_H_LEN] { return 0 }
391 return tf_rd16(fh[TF_H_BUF] as *u8, at)
392}
393func tf_adv_units(fh: *i64, ch: i64) -> i64 { return tf_adv_units_gid(fh, tf_gid(fh, ch)) }
394// advance in the browser's 1000-em unit (font_adv_em's unit): floor(aw * 1000 / upem). For Liberation Sans
395// this reproduces the estate's hand-baked oracle table exactly (space 277, i 222, m 833, W 943).
396func tf_adv_em1000(fh: *i64, ch: i64) -> i64 { return (tf_adv_units(fh, ch) * TF_EM1000) / fh[TF_H_UPEM] }
397// pair kerning in 1000-em units (negative pulls the pair closer). 0 when the face has no horizontal kern
398// table, either glyph is .notdef, or the pair is absent. Binary search over the sorted pair array.
399func tf_kern_em1000(fh: *i64, a: i64, c: i64) -> i64 {
400 let n: i64 = fh[TF_H_KN]
401 if n <= 0 { return 0 }
402 let ga: i64 = tf_gid(fh, a)
403 let gc: i64 = tf_gid(fh, c)
404 if ga == 0 { return 0 }
405 if gc == 0 { return 0 }
406 let key: i64 = ga*TF_MAGIC_65536 + gc
407 let b: *u8 = fh[TF_H_BUF] as *u8
408 let base: i64 = fh[TF_H_KPAIRS]
409 var lo: i64 = 0
410 var hi: i64 = n - 1
411 while lo <= hi {
412 let mid: i64 = (lo + hi) / 2
413 let p: i64 = base + mid*TF_KERN_PAIR
414 let k: i64 = tf_rd16(b, p)*TF_MAGIC_65536 + tf_rd16(b, p+2)
415 if k == key { return (tf_rd16s(b, p+4) * TF_EM1000) / fh[TF_H_UPEM] }
416 if k < key { lo = mid + 1 } else { hi = mid - 1 }
417 }
418 return 0
419}
420
421// get (rasterizing + caching on miss) the coverage record for char ch at pixel height ph (the em size).
422// rec: [TF_R_COV]=cov ptr (bytes, cw x chh; row 0 = ASCENDER line) [TF_R_CW]=cw [TF_R_CHH]=chh
423// [TF_R_ADV]=advance px [TF_R_LM]=left margin px. returns 0 as *i64 if no glyph.
424func tf_glyph(fh: *i64, ch: i64, ph: i64) -> *i64 {
425 if ph <= 2 { return 0 as *i64 }
426 if ch < TF_ASCII_LO { return 0 as *i64 }
427 if ch > TF_ASCII_HI { return 0 as *i64 }
428 // size slot
429 var slot: i64 = 0 - 1
430 var i: i64 = 0
431 while i < fh[TF_H_NSIZES] { if fh[TF_H_SIZE0+i]==ph { slot=i; i=fh[TF_H_NSIZES] } else { i=i+1 } }
432 if slot < 0 {
433 if fh[TF_H_NSIZES] < TF_MAXSZ { slot=fh[TF_H_NSIZES]; fh[TF_H_SIZE0+slot]=ph; fh[TF_H_NSIZES]=fh[TF_H_NSIZES]+1 }
434 else {
435 // evict the LAST slot for this size: clear its records so a stale size cannot be served
436 slot=TF_MAXSZ-1; fh[TF_H_SIZE0+slot]=ph
437 let cache0: *i64 = fh[TF_H_CACHE] as *i64
438 var z: i64 = 0
439 while z < TF_ASCII_N*TF_REC { cache0[slot*TF_ASCII_N*TF_REC + z] = 0; z = z + 1 }
440 }
441 }
442 let cache: *i64 = fh[TF_H_CACHE] as *i64
443 let rec: *i64 = ((cache as i64) + ((slot*TF_ASCII_N + (ch-TF_ASCII_LO))*TF_REC)*8) as *i64
444 if rec[TF_R_COV] != 0 { return rec }
445 let gid0: i64 = tf_cmap_lookup(fh[TF_H_BUF] as *u8, fh[TF_H_CMAPSUB], ch)
446 tf_raster(fh, gid0, ph, rec)
447 return rec
448}
449// BR30 (2026-09-02): rasterize ANY codepoint the cmap maps (Latin-1, Extended, Greek, Cyrillic ...),
450// UNCACHED here -- the caller's atlas caches by (face slot, codepoint, size), so the engine keeps its
451// small fixed ASCII cache and never grows with the page's alphabet. 0 = uncovered (gid 0) or ph too small.
452func tf_glyph_cp(fh: *i64, cp: i64, ph: i64) -> *i64 {
453 if ph <= 2 { return 0 as *i64 }
454 if cp < TF_ASCII_LO { return 0 as *i64 }
455 let gid: i64 = tf_cmap_lookup(fh[TF_H_BUF] as *u8, fh[TF_H_CMAPSUB], cp)
456 if gid <= 0 { return 0 as *i64 }
457 let rec: *i64 = sys_mmap(8*TF_REC) as *i64
458 tf_raster(fh, gid, ph, rec)
459 return rec
460}
461// BR39 (2026-09-02): the face NOTDEF cell (gid 0), for a codepoint the face cannot map -- the box every
462// browser shows without a fallback font. Uncached here like tf_glyph_cp; the atlas caches it once per size.
463func tf_glyph_notdef(fh: *i64, ph: i64) -> *i64 {
464 if ph <= 2 { return 0 as *i64 }
465 let rec: *i64 = sys_mmap(8*TF_REC) as *i64
466 tf_raster(fh, 0, ph, rec)
467 return rec
468}
469// the ONE raster body: ASCII cache path and any-codepoint path both fill their record through it
470func tf_raster(fh: *i64, gid: i64, ph: i64, rec: *i64) -> i64 {
471 let b: *u8 = fh[TF_H_BUF] as *u8
472 let upem: i64 = fh[TF_H_UPEM]
473 let asc: i64 = fh[TF_H_ASC]
474 let aw_units: i64 = tf_adv_units_gid(fh, gid)
475 var advpx: i64 = (aw_units*ph)/upem
476 if advpx < TF_MIN_ADV_PX { advpx = TF_MIN_ADV_PX }
477 let ex0: *i64 = sys_mmap(8*TF_MAXE) as *i64
478 let ey0: *i64 = sys_mmap(8*TF_MAXE) as *i64
479 let ex1: *i64 = sys_mmap(8*TF_MAXE) as *i64
480 let ey1: *i64 = sys_mmap(8*TF_MAXE) as *i64
481 let ne: i64 = tf_glyph_edges(b, fh[TF_H_GLYF], fh[TF_H_LOCA], fh[TF_H_LOCFMT], fh[TF_H_NGLYPHS], gid, ex0, ey0, ex1, ey1)
482 // ink extent in font units: a negative xmin needs a left margin, an xmax past the advance widens the cell
483 var xmin: i64 = 0
484 var xmax: i64 = aw_units
485 var k: i64 = 0
486 while k < ne {
487 if ex0[k] < xmin { xmin = ex0[k] }
488 if ex1[k] < xmin { xmin = ex1[k] }
489 if ex0[k] > xmax { xmax = ex0[k] }
490 if ex1[k] > xmax { xmax = ex1[k] }
491 k = k + 1
492 }
493 var lm: i64 = 0
494 if xmin < 0 { lm = ((0 - xmin)*ph + upem - 1)/upem }
495 var cw: i64 = (xmax*ph + upem - 1)/upem + lm + 2
496 if cw < advpx + 2 { cw = advpx + 2 }
497 let chh: i64 = ((asc - fh[TF_H_DESC])*ph)/upem + 1
498 let cov: *u8 = sys_mmap(cw*chh + 16)
499 let SS: i64 = TF_SS
500 if ne > 0 {
501 var py: i64 = 0
502 while py < chh {
503 var px: i64 = 0
504 while px < cw {
505 var cnt: i64 = 0
506 var sy: i64 = 0
507 while sy < SS {
508 var sx: i64 = 0
509 while sx < SS {
510 let gx: i64 = (((px - lm)*SS+sx)*upem)/(ph*SS)
511 let gy: i64 = asc - ((py*SS+sy)*upem)/(ph*SS)
512 cnt = cnt + tf_inside(gx, gy, ex0, ey0, ex1, ey1, ne)
513 sx = sx + 1
514 }
515 sy = sy + 1
516 }
517 var a: i64 = (cnt*255)/(SS*SS)
518 if a > 0 { a = a + (a*(255-a))/TF_CONTRAST_DIV; if a > 255 { a = 255 } } // contrast boost (stem darkening)
519 cov[py*cw+px] = a as u8
520 px = px + 1
521 }
522 py = py + 1
523 }
524 }
525 rec[TF_R_COV] = cov as i64
526 rec[TF_R_CW] = cw
527 rec[TF_R_CHH] = chh
528 rec[TF_R_ADV] = advpx
529 rec[TF_R_LM] = lm
530 rec[TF_R_PH] = ph
531 return 1
532}
533
534// draw a text run. fh = font handle; (x,y) = TOP-LEFT of the line's glyph box (its ASCENDER line); ph =
535// pixel height of the em; cellw>0 = fixed advance per char (a monospace cell); cellw==0 = proportional
536// advances from hmtx (spaces included -- no guessed space width). returns total width advanced.
537func tf_draw_text(fh: *i64, fb: *Framebuffer, x: i64, y: i64, s: *u8, n: i64, ph: i64, cr: i64, cg: i64, cb: i64, cellw: i64) -> i64 {
538 let fw: i64 = fb.width
539 let fhh: i64 = fb.height
540 let pxb: *u8 = fb.pixels
541 var pen: i64 = x
542 var i: i64 = 0
543 while i < n {
544 let ch: i64 = s[i] & 0xff
545 var adv: i64 = cellw
546 if ch >= TF_ASCII_LO { if ch <= TF_ASCII_HI {
547 let rec: *i64 = tf_glyph(fh, ch, ph)
548 if rec != (0 as *i64) {
549 let cov: *u8 = rec[TF_R_COV] as *u8
550 let cw: i64 = rec[TF_R_CW]
551 let chh: i64 = rec[TF_R_CHH]
552 let natural: i64 = rec[TF_R_ADV]
553 let lm: i64 = rec[TF_R_LM]
554 if cellw == 0 { adv = natural }
555 // draw the FULL coverage width cw (the glyph ink can extend past its own advance -- e.g. an
556 // 'i'/'l' stem near the right edge). Squeeze only when the ink is wider than the cell.
557 var dw: i64 = cw
558 var squeeze: i64 = 0
559 if cellw > 0 { if cw > cellw { dw = cellw; squeeze = 1 } }
560 var xoff: i64 = 0 - lm
561 if cellw > 0 { if cw < cellw { xoff = (cellw-cw)/2 } }
562 var gy: i64 = 0
563 while gy < chh {
564 let fy: i64 = y + gy
565 if fy >= 0 { if fy < fhh {
566 var gx: i64 = 0
567 while gx < dw {
568 var sxc: i64 = gx
569 if squeeze == 1 { sxc = (gx*cw)/dw }
570 if sxc >= cw { sxc = cw-1 }
571 let a: i64 = cov[gy*cw+sxc] as i64
572 if a > 0 {
573 let fx: i64 = pen + xoff + gx
574 if fx >= 0 { if fx < fw {
575 let o: i64 = (fy*fw+fx)*4
576 let ia: i64 = 255-a
577 pxb[o] = (((pxb[o] as i64)*ia + cr*a)/255) as u8
578 pxb[o+1] = (((pxb[o+1] as i64)*ia + cg*a)/255) as u8
579 pxb[o+2] = (((pxb[o+2] as i64)*ia + cb*a)/255) as u8
580 pxb[o+3] = 255 as u8
581 } }
582 }
583 gx = gx + 1
584 }
585 } }
586 gy = gy + 1
587 }
588 }
589 } }
590 pen = pen + adv
591 i = i + 1
592 }
593 return pen - x
594}
595
596// proportional advance of one char at ph (for measurement) -- hmtx, spaces included
597func tf_char_adv(fh: *i64, ch: i64, ph: i64) -> i64 {
598 let rec: *i64 = tf_glyph(fh, ch, ph)
599 if rec == (0 as *i64) { return (tf_adv_units_gid(fh, 0)*ph) / fh[TF_H_UPEM] }
600 return rec[TF_R_ADV]
601}