From ea6dbcf627e9a440590d0d35ff16ca7ae60d5719 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 16:13:58 +0100 Subject: Kern with class subtables too (kern formats 2 and 3) The kern table's class subtables, OpenType's and Apple's, as HarfBuzz reads them (hb-aat-layout-kerx-table.hh, hb-ot-kern-table.hh): format 2's class tables give byte offsets into a kerning array, format 3's byte classes index a value list. Both go through the same pass as pairs (marks skipped, the kerning split between the pair). Format 2 filters the glyphs by the class tables' digests, as HarfBuzz's accelerator does, so a digest's false positive reads class 0 there too; what a subtable reads must lie within it, the last one's within the table. The build checks the subtables as HarfBuzz's sanitizer does, and still refuses state machines (format 1) and cross-stream kerning. text/test-kern.py makes fonts with format 2 and 3 subtables in every order, with the edge cases that reading has (value 1, glyphs outside a table, offsets below the array or odd, values past a subtable's or the table's end, classes and indexes past their counts): 24 fonts, 96 plans, the same as HarfBuzz. make check runs it where fontTools is. HarfBuzz's suite: 5017 of 5199 the same. --- text/test-kern.py | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 text/test-kern.py (limited to 'text/test-kern.py') diff --git a/text/test-kern.py b/text/test-kern.py new file mode 100644 index 0000000..2d423f2 --- /dev/null +++ b/text/test-kern.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""The kern table's class subtables against HarfBuzz, where no font here has +them: + + python3 text/test-kern.py VEGA_SYMBOLS [FONT.ttf] + +Fonts made from DejaVu Sans without its GPOS, their kern table a format 3 +subtable and a format 2 one in every order (and two of either), with what +HarfBuzz's reading of them turns on: class value 1 (out of bounds), +glyphs outside a class table (a digest's false positive takes class 0), +offsets below the kerning array, odd ones, values past the subtable's end +(the last subtable may read on to the table's) and past the table's, class +and index values past their counts. Each is built with vega-symbols, which +refuses a font whose shaping differs from HarfBuzz's on random runs. +Needs fontTools; prints one JSON line, exits 1 on a difference. +""" +import json +import random +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +from fontTools.ttLib import TTFont, newTable + + +def subtable(format, body): + return struct.pack('>HHBB', 0, 6 + len(body), format, 1) + body # horizontal + + +def format3(rng, glyphs): + lefts, rights, values = 7, 9, 40 + body = struct.pack('>HBBBB', glyphs, values, lefts, rights, 0) + body += b''.join(struct.pack('>h', rng.randrange(-150, 150)) for _ in range(values)) + body += bytes(rng.randrange(lefts + 1) for _ in range(glyphs)) # some past the count + body += bytes(rng.randrange(rights + 1) for _ in range(glyphs)) + body += bytes(rng.randrange(values + 3) for _ in range(lefts * rights)) + return subtable(3, body) + + +def format2(rng, latin): + rows, columns = 6, 8 + width = 2 * columns + first_left, first_right, left_count, right_count = latin[5], latin[3], 60, 70 + left_at = 6 + 8 + right_at = left_at + 4 + 2 * left_count + array = right_at + 4 + 2 * right_count + + def left(): + k = rng.randrange(20) + return (1 if k == 0 else rng.randrange(array) if k == 1 else array + rows * width + 64 if k == 2 else + array + rng.randrange(rows) * width + (k == 3)) + + def right(): + k = rng.randrange(20) + return 1 if k == 0 else 60000 if k == 1 else 2 * rng.randrange(columns) + + body = struct.pack('>HHHH', width, left_at, right_at, array) + body += struct.pack('>HH', first_left, left_count) + b''.join(struct.pack('>H', left()) for _ in range(left_count)) + body += struct.pack('>HH', first_right, right_count) + b''.join(struct.pack('>H', right()) for _ in range(right_count)) + body += b''.join(struct.pack('>h', rng.randrange(-120, 120)) for _ in range(rows * columns)) + return subtable(2, body) + + +def main(): + tool = sys.argv[1] + source = sys.argv[2] if len(sys.argv) > 2 else '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf' + fonts = plans = differing = 0 + with tempfile.TemporaryDirectory(prefix='vega-kern-') as directory: + work = Path(directory) + for seed in range(1, 5): + for order in ('32', '23', '3', '2', '22', '33'): + rng = random.Random(seed * 100 + len(order)) + font = TTFont(source) + del font['GPOS'] + glyphs = len(font.getGlyphOrder()) + cmap = font.getBestCmap() + latin = sorted({font.getGlyphID(cmap[c]) for c in range(0x20, 0x7F) if c in cmap}) + tables = [format3(rng, glyphs) if c == '3' else format2(rng, latin) for c in order] + font['kern'] = newTable('kern') + font['kern'].decompile(struct.pack('>HH', 0, len(tables)) + b''.join(tables), font) + path = work / f'kern-{seed}-{order}.ttf' + font.save(path) + built = subprocess.run([tool, 'build', '--font', str(path), '--codes', '0x20-0x7e', '--shape', + 'Latn,Zyyy', '--runs', '3000', '-o', str(work / 'kern.vsym')], + capture_output=True, text=True) + results = [json.loads(line) for line in built.stdout.splitlines() if line.startswith('{"script"')] + fonts += 1 + plans += len(results) + if built.returncode or not results or any(r['differences'] for r in results): + differing += 1 + print(f'seed {seed}, subtables {order}:\n{built.stderr[-2000:]}', file=sys.stderr) + print(json.dumps({'fonts': fonts, 'plans': plans, 'differing_fonts': differing})) + return 1 if differing else 0 + + +if __name__ == '__main__': + sys.exit(main()) -- cgit v1.2.3