aboutsummaryrefslogtreecommitdiffstats
path: root/text/test-kern.py
blob: 2d423f2da2f0f0df1fbdc8e63b2f43f78a79dd58 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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())