aboutsummaryrefslogtreecommitdiffstats
path: root/text/test-kern.py
diff options
context:
space:
mode:
authoreunhhu <[email protected]>2026-09-28 16:13:58 +0100
committereunhhu <[email protected]>2026-09-28 16:13:58 +0100
commitea6dbcf627e9a440590d0d35ff16ca7ae60d5719 (patch)
tree7eecf9bfcea53b5b79b4d2f360446f54b0d5e924 /text/test-kern.py
parentcf6bcf8697589d2a6fc3c6d98bc52bf7eb1ea4ae (diff)
downloadvega-ea6dbcf627e9a440590d0d35ff16ca7ae60d5719.tar.gz
vega-ea6dbcf627e9a440590d0d35ff16ca7ae60d5719.zip
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.
Diffstat (limited to 'text/test-kern.py')
-rw-r--r--text/test-kern.py99
1 files changed, 99 insertions, 0 deletions
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())