Skip to content

Commit f2fc48b

Browse files
matmenclaude
andcommitted
Test the untested paths, fix what the references disagree with
Coverage from 91.7% to 96.5% of the lines (85% to 88% of branches), tested against independent references where there are some: - zlib: the JavaScript deflate and inflate against zlib, and whole images through the paths of runtimes without node:zlib (streams as in browsers, and neither), on Node, Deno and Bun - WebP: filtered alpha (libwebp-test-data) and frames blended over the ones before, against libwebp - TIFF: BigTIFF (rgb-bigtiff.tif was none), YCbCr against libtiff, old-style LZW, fill order 2, 12 pixel wide tiles, associated alpha, 2- and 32-bit samples, float ranges (scripts/tiff-fixtures.py) - SVG: 180 cases of resvg's test suite against its renderings (XML entities, style sheets, switch, nested svg, aspect ratios, gradients, clip paths, masks, colors, arcs, caps, symbols) - COLR: the other blend modes against the W3C formulas, a touching radial gradient, a solid paint, a glyph without a clip box - shaping against HarfBuzz: every lookup type and format, lookup flags, extension lookups, feature variations, rand (scripts/shaping-features.py), the Thai, Hangul and Hebrew fallbacks and fallback spaces, hyphens and mark positions (scripts/shaping-fallbacks.py) - hinting against FreeType (v35): every instruction the test fonts didn't use and point matching (scripts/hinting-fixtures.py) They found: - SPVTL, SFVTL and SDPVTL set their vectors from the first point to the second, FreeType from the second to the first (which perpendicular vectors show); S45ROUND rounds to FreeType's 26.6 grid, ISECT and ALIGNPTS round like its integer arithmetic. Of 9558 hinted glyphs of 26 fonts, 12 fewer differ from FreeType, none more - rand picked other alternates than HarfBuzz, whose uint32_t random state wraps around before the modulo - presentation attributes with spaces around them (fill=" url(#a) ") didn't apply Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
1 parent bed235c commit f2fc48b

405 files changed

Lines changed: 3380 additions & 29 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## 3.3.1
4+
5+
### Fixes
6+
7+
- TrueType hinting matches FreeType's in more fonts: SPVTL, SFVTL and SDPVTL set their vectors from the second point
8+
to the first (they pointed the other way), S45ROUND rounds to FreeType's 45° grid, ISECT and ALIGNPTS compute like
9+
FreeType's integer arithmetic. (Found by a test font using every instruction, compared with FreeType point for point.)
10+
- `rand`: the alternates picked at random are the ones HarfBuzz picks (its random state wraps around at 2^32).
11+
- `Image.renderSVG`: spaces around presentation attribute values (`fill=" url(#gradient) "`) are ignored, as in CSS.
12+
313
## 3.3.0
414

515
### Features

‎scripts/hinting-fixtures.py‎

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Builds "tests/fonts/hinting instructions.ttf", a TrueType font whose glyph programs use the instructions other test
2+
# fonts don't (vectors, ISECT, ALIGNPTS, UTP, FLIPPT, the round states, MSIRP, SHZ, SCFS, GC, DELTAP2/3, DELTAC,
3+
# IDEF, JROT/JROF, GETINFO, INSTCTRL, ...) and point matching in composite glyphs, and writes FreeType's hinted
4+
# outlines of it (v35 interpreter, as ImageScript's) to tests/fonts/hinting-instructions.json, which tests/font.js
5+
# compares with. Needs fontTools and freetype-py: python3 scripts/hinting-fixtures.py
6+
import array, ctypes, json, os
7+
import freetype
8+
from fontTools.fontBuilder import FontBuilder
9+
from fontTools.pens.ttGlyphPen import TTGlyphPen
10+
from fontTools.ttLib import newTable
11+
from fontTools.ttLib.tables import ttProgram
12+
from fontTools.ttLib.tables._g_l_y_f import Glyph, GlyphComponent
13+
14+
fonts = os.path.join(os.path.dirname(__file__), '..', 'tests', 'fonts')
15+
FILE = os.path.join(fonts, 'hinting instructions.ttf')
16+
SIZES = [11, 13, 16, 24]
17+
18+
OPS = {
19+
'SVTCA0': 0x00, 'SVTCA1': 0x01, 'SPVTCA0': 0x02, 'SPVTCA1': 0x03, 'SFVTCA0': 0x04, 'SFVTCA1': 0x05,
20+
'SPVTL0': 0x06, 'SPVTL1': 0x07, 'SFVTL0': 0x08, 'SFVTL1': 0x09, 'SPVFS': 0x0a, 'SFVFS': 0x0b, 'GPV': 0x0c,
21+
'GFV': 0x0d, 'SFVTPV': 0x0e, 'ISECT': 0x0f, 'SRP0': 0x10, 'SRP1': 0x11, 'SRP2': 0x12, 'SZP0': 0x13, 'SZP1': 0x14,
22+
'SZP2': 0x15, 'SZPS': 0x16, 'SLOOP': 0x17, 'RTG': 0x18, 'RTHG': 0x19, 'SMD': 0x1a, 'ELSE': 0x1b, 'JMPR': 0x1c,
23+
'SCVTCI': 0x1d, 'SSWCI': 0x1e, 'SSW': 0x1f, 'DUP': 0x20, 'POP': 0x21, 'CLEAR': 0x22, 'SWAP': 0x23, 'DEPTH': 0x24,
24+
'CINDEX': 0x25, 'MINDEX': 0x26, 'ALIGNPTS': 0x27, 'UTP': 0x29, 'LOOPCALL': 0x2a, 'CALL': 0x2b, 'FDEF': 0x2c,
25+
'ENDF': 0x2d, 'MDAP0': 0x2e, 'MDAP1': 0x2f, 'IUP0': 0x30, 'IUP1': 0x31, 'SHP0': 0x32, 'SHP1': 0x33, 'SHC0': 0x34,
26+
'SHC1': 0x35, 'SHZ0': 0x36, 'SHZ1': 0x37, 'SHPIX': 0x38, 'IP': 0x39, 'MSIRP0': 0x3a, 'MSIRP1': 0x3b,
27+
'ALIGNRP': 0x3c, 'RTDG': 0x3d, 'MIAP0': 0x3e, 'MIAP1': 0x3f, 'WS': 0x42, 'RS': 0x43, 'WCVTP': 0x44, 'RCVT': 0x45,
28+
'GC0': 0x46, 'GC1': 0x47, 'SCFS': 0x48, 'MD0': 0x49, 'MD1': 0x4a, 'MPPEM': 0x4b, 'MPS': 0x4c, 'FLIPON': 0x4d,
29+
'FLIPOFF': 0x4e, 'LT': 0x50, 'LTEQ': 0x51, 'GT': 0x52, 'GTEQ': 0x53, 'EQ': 0x54, 'NEQ': 0x55, 'ODD': 0x56,
30+
'EVEN': 0x57, 'IF': 0x58, 'EIF': 0x59, 'AND': 0x5a, 'OR': 0x5b, 'NOT': 0x5c, 'DELTAP1': 0x5d, 'SDB': 0x5e,
31+
'SDS': 0x5f, 'ADD': 0x60, 'SUB': 0x61, 'DIV': 0x62, 'MUL': 0x63, 'ABS': 0x64, 'NEG': 0x65, 'FLOOR': 0x66,
32+
'CEILING': 0x67, 'ROUND0': 0x68, 'NROUND0': 0x6c, 'WCVTF': 0x70, 'DELTAP2': 0x71, 'DELTAP3': 0x72,
33+
'DELTAC1': 0x73, 'DELTAC2': 0x74, 'DELTAC3': 0x75, 'SROUND': 0x76, 'S45ROUND': 0x77, 'JROT': 0x78, 'JROF': 0x79,
34+
'ROFF': 0x7a, 'RUTG': 0x7c, 'RDTG': 0x7d, 'SANGW': 0x7e, 'FLIPPT': 0x80, 'FLIPRGON': 0x81, 'FLIPRGOFF': 0x82,
35+
'SCANCTRL': 0x85, 'SDPVTL0': 0x86, 'SDPVTL1': 0x87, 'GETINFO': 0x88, 'IDEF': 0x89, 'ROLL': 0x8a, 'MAX': 0x8b,
36+
'MIN': 0x8c, 'SCANTYPE': 0x8d, 'INSTCTRL': 0x8e,
37+
}
38+
MDRP = lambda flags: 0xc0 | flags # 5 bits: set rp0, keep minimum distance, round, distance type
39+
MIRP = lambda flags: 0xe0 | flags
40+
41+
42+
def push(*values):
43+
if any(v < 0 or v > 255 for v in values):
44+
out = [0x41, len(values)]
45+
for v in values: out += [v >> 8 & 255, v & 255]
46+
return out
47+
return [0x40, len(values)] + list(values)
48+
49+
50+
def asm(*items):
51+
"""items: opcode names, raw opcodes (ints), or lists of bytes (pushes)"""
52+
out = []
53+
for item in items:
54+
if isinstance(item, str): out.append(OPS[item])
55+
elif isinstance(item, int): out.append(item)
56+
else: out += item
57+
return out
58+
59+
60+
def program(code):
61+
p = ttProgram.Program()
62+
p.fromBytecode(bytes(code))
63+
return p
64+
65+
66+
# every glyph: an octagon (points 0-7) and a square inside it (8-11)
67+
def outline():
68+
pen = TTGlyphPen(None)
69+
for contour in [[(100, 0), (500, 0), (600, 100), (600, 600), (500, 700), (100, 700), (0, 600), (0, 100)],
70+
[(200, 200), (400, 200), (400, 400), (200, 400)]]:
71+
pen.moveTo(contour[0])
72+
for point in contour[1:]: pen.lineTo(point)
73+
pen.closePath()
74+
return pen.glyph()
75+
76+
77+
skipped = asm(push(3, 200), 'SHPIX') # moves point 3 a lot: must not run
78+
items = {
79+
# projection and freedom vectors from lines, the stack and each other, dual vectors, MD on both outlines
80+
'vectors': (push(0, 2), 'SPVTL0', push(1, 3), 'SFVTL1', push(4, 40), 'SHPIX', 'GPV', 'SFVFS', 'GFV', 'SPVFS',
81+
push(6, 24), 'SHPIX', 'SFVTPV', push(2, 9), 'SDPVTL1', push(5, 0, 7), 'MD1', 'SHPIX',
82+
push(8, 0, 7), 'MD0', push(4), 'DIV', 'SHPIX', push(9, 3), 'SDPVTL0', push(10, 30), 'SHPIX',
83+
'SPVTCA1', 'SFVTCA0', push(3, 32), 'SHPIX', 'SVTCA1', 'IUP1', 'IUP0'),
84+
# intersections (and parallel lines: the middle), aligning points, untouching
85+
'isect': (push(9, 0, 3, 1, 6), 'ISECT', push(8, 0, 1, 4, 5), 'ISECT', 'SVTCA1', push(10, 11), 'ALIGNPTS',
86+
push(1, 48), 'SHPIX', push(1), 'UTP', push(2, 40), 'SHPIX', 'IUP1', 'IUP0'),
87+
# on and off curve flips
88+
'flip': (push(2), 'FLIPPT', push(4, 6), 'FLIPRGOFF', push(9, 10), 'FLIPRGON', push(10), 'FLIPPT'),
89+
# round states with MDRP, MSIRP, minimum distance, cut-ins, single width, auto flip
90+
'round': ('SVTCA1', push(0), 'MDAP1', push(0x4a), 'SROUND', push(3), MDRP(0b00100), push(0x8e), 'S45ROUND',
91+
push(4), MDRP(0b00100), 'RTDG', push(5), MDRP(0b00100), 'RDTG', push(6), MDRP(0b00100), 'RUTG',
92+
push(7), MDRP(0b00100), 'ROFF', push(1), MDRP(0b00100), 'RTHG', push(96), 'SMD', push(2), MDRP(0b01100),
93+
'RTG', push(17), 'SCVTCI', push(40), 'SSWCI', push(100), 'SSW', 'FLIPOFF', push(9, 0), MIRP(0b10100),
94+
'FLIPON', push(10, 1), MIRP(0b00100), push(11, 300), 'MSIRP1', push(8, 150), 'MSIRP0', 'SVTCA0',
95+
push(0), 'MDAP1', push(2, 100), 'MSIRP0', 'IUP0'),
96+
# coordinates: SCFS, GC on both outlines, SHZ, SHC, MIAP into the twilight zone
97+
'coords': ('SVTCA1', push(3, 500), 'SCFS', push(4), 'GC0', push(5), 'SWAP', 'SCFS', push(6), 'GC1', push(64),
98+
'ADD', push(7), 'SWAP', 'SCFS', push(0), 'SZP0', push(0, 1), 'MIAP0', push(1), 'SZP0', push(1), 'SZP2',
99+
push(0), 'SRP1', push(2), 'SRP2', push(1), 'SHZ1', push(0), 'SHC0', push(1), 'SZPS', 'IUP1'),
100+
# deltas: DELTAP1-3 at 11, 13 (base 0, shift 3), 16 and 24 ppem, DELTAC on a control value used by MIRP
101+
'deltas': ('SVTCA1', push(3), 'SDS', push(0), 'SDB', push(0xbf, 1, 0xd0, 2, 2), 'DELTAP1',
102+
push(0x0f, 3, 1), 'DELTAP2', push(0x80, 4, 1), 'DELTAP3', push(8), 'SDB',
103+
push(0x5f, 5, 0x3c, 6, 2), 'DELTAP1', push(0x8f, 7, 1), 'DELTAP2', push(0x0d, 0, 0x50, 1, 2),
104+
'DELTAC1', push(0x86, 1, 1), 'DELTAC2', push(0x0a, 0, 1), 'DELTAC3', push(0), 'MDAP1', push(8, 0),
105+
MIRP(0b00100), push(9, 1), MIRP(0b00100), 'IUP1'),
106+
# arithmetic, logic and stack instructions computing shifts, a user defined instruction, jumps, GETINFO
107+
'logic': ('SVTCA1', push(1, 7), 'ODD', push(64), 'MUL', push(2, 128), 'EVEN', push(32), 'MUL', 'ADD', 'SHPIX',
108+
push(2, 30, 90), 'MIN', push(20), 'MAX', push(5), 'NEG', 'ABS', 'ADD', 'SHPIX',
109+
push(4, 1, 2, 3), 'ROLL', 'SUB', 'SUB', push(16), 'MUL', 'SHPIX',
110+
push(5, 75), 'CEILING', push(6, 75), 'FLOOR', 'SHPIX', 'SHPIX',
111+
push(1, 0), 'OR', push(1), 'AND', 'NOT', 'NOT', 'IF', push(6, 16), 'SHPIX', 'EIF',
112+
push(len(skipped) + 1, 1), 'JROT', skipped, push(len(skipped) + 1, 0), 'JROF', skipped,
113+
push(0, 1), 'JROF', push(7, 8), 'SHPIX',
114+
0x93, push(8), push(0x3f), 'GETINFO', push(255), 'AND', 'SHPIX', 'MPS', 'POP',
115+
push(0, 0), 'SCANCTRL', 'SCANTYPE', 'IUP1'),
116+
}
117+
glyphs = {name: asm(*code) for name, code in items.items()}
118+
119+
def build(glyphs, file, sizes, json_file=None):
120+
glyph_order = ['.notdef'] + list(glyphs) + ['point-matched']
121+
outlines = {name: outline() for name in glyph_order}
122+
for name, code in glyphs.items(): outlines[name].program = program(code)
123+
outlines['.notdef'].program = program([])
124+
# a composite glyph: a component placed by matching its point 8 to point 2 of the first one, and its own program
125+
composite = Glyph()
126+
composite.numberOfContours = -1
127+
first, second = GlyphComponent(), GlyphComponent()
128+
first.glyphName, first.x, first.y, first.flags = 'vectors', 0, 0, 0x0001 | 0x0002 | 0x0004
129+
second.glyphName, second.firstPt, second.secondPt, second.flags = 'isect', 2, 8, 0x0001 | 0x0004
130+
composite.components = [first, second]
131+
composite.program = program(asm('SVTCA1', push(14, 32), 'SHPIX'))
132+
outlines['point-matched'] = composite
133+
134+
fb = FontBuilder(1000, isTTF=True)
135+
fb.setupGlyphOrder(glyph_order)
136+
fb.setupCharacterMap({0xe000 + i: name for i, name in enumerate(glyph_order[1:])})
137+
fb.setupGlyf(outlines)
138+
fb.setupHorizontalMetrics({name: (700, 0) for name in glyph_order})
139+
fb.setupHorizontalHeader(ascent=900, descent=-200)
140+
fb.setupNameTable({'familyName': 'Hinting Instructions', 'styleName': 'Regular'})
141+
fb.setupOS2(); fb.setupPost()
142+
font = fb.font
143+
# reproducible: no timestamps
144+
font.recalcTimestamp = False
145+
font['head'].created = font['head'].modified = 0
146+
cvt = newTable('cvt ')
147+
cvt.values = array.array('h', [80, 120, 300])
148+
font['cvt '] = cvt
149+
# the user defined instruction 0x93 shifts point 11; the control value program sets INSTCTRL (no effect) and a value
150+
fpgm = newTable('fpgm')
151+
fpgm.program = program(asm(push(0x93), 'IDEF', push(11, 20), 'SHPIX', 'ENDF'))
152+
font['fpgm'] = fpgm
153+
prep = newTable('prep')
154+
prep.program = program(asm(push(0, 2), 'INSTCTRL', push(2, 280), 'WCVTF', push(2), 'RCVT', 'POP'))
155+
font['prep'] = prep
156+
maxp = font['maxp']
157+
maxp.maxZones, maxp.maxTwilightPoints, maxp.maxStorage = 2, 4, 8
158+
maxp.maxFunctionDefs, maxp.maxInstructionDefs, maxp.maxStackElements = 4, 4, 64
159+
maxp.maxSizeOfInstructions = max(len(c) for c in glyphs.values())
160+
font.save(file)
161+
162+
# FreeType's hinted outlines (v35 interpreter): 26.6 points with their on curve flags, and advances
163+
lib = freetype.get_handle()
164+
version = ctypes.c_uint(35)
165+
assert freetype.raw.FT_Property_Set(lib, b'truetype', b'interpreter-version', ctypes.byref(version)) == 0
166+
face = freetype.Face(file)
167+
expected = {}
168+
for size in sizes:
169+
face.set_char_size(size * 64, 0, 72, 72)
170+
for i, name in enumerate(glyph_order[1:]):
171+
face.load_char(chr(0xe000 + i), freetype.FT_LOAD_NO_AUTOHINT | freetype.FT_LOAD_NO_BITMAP)
172+
o = face.glyph.outline
173+
expected[f'{size}:{name}'] = {'advance': face.glyph.advance.x,
174+
'points': [[x, y, t & 1] for (x, y), t in zip(o.points, o.tags)]}
175+
if json_file:
176+
json.dump(expected, open(json_file, 'w'), separators=(',', ':'))
177+
print(len(expected), 'outlines')
178+
179+
return expected
180+
181+
182+
if __name__ == '__main__':
183+
build(glyphs, FILE, SIZES, os.path.join(fonts, 'hinting-instructions.json'))

‎scripts/shaping-fallbacks.py‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Adds the fallbacks of the script shapers to tests/shaping: fonts without GSUB and GPOS (made here, rectangles as
2+
# outlines) and HarfBuzz's shaping of texts with them. Thai positions marks with private use glyphs (Windows and Mac
3+
# ones), Hangul composes and decomposes syllables by the glyphs the font has, Hebrew uses presentation forms; marks
4+
# are placed by the fallback positioning. Needs fontTools and uharfbuzz; run after scripts/shaping-reference.py:
5+
# python scripts/shaping-fallbacks.py
6+
import json, os, random
7+
from fontTools.fontBuilder import FontBuilder
8+
from fontTools.pens.ttGlyphPen import TTGlyphPen
9+
import uharfbuzz as hb
10+
11+
out_dir = os.path.join(os.path.dirname(__file__), '..', 'tests', 'shaping')
12+
random.seed(18)
13+
14+
15+
def rect(x0, y0, x1, y1):
16+
pen = TTGlyphPen(None)
17+
pen.moveTo((x0, y0)); pen.lineTo((x0, y1)); pen.lineTo((x1, y1)); pen.lineTo((x1, y0)); pen.closePath()
18+
return pen.glyph()
19+
20+
21+
def build(file, chars, mark_above, mark_below, zero_width=()):
22+
"""chars: code points; marks get no advance and an outline above or below the base, the others a box"""
23+
names = ['.notdef'] + [f'u{u:04X}' for u in chars]
24+
outlines, metrics = {'.notdef': rect(50, 0, 450, 700)}, {'.notdef': (500, 50)}
25+
for u in chars:
26+
name = f'u{u:04X}'
27+
if u in mark_above: outlines[name], metrics[name] = rect(-350, 750, -150, 950), (0, -350)
28+
elif u in mark_below: outlines[name], metrics[name] = rect(-350, -250, -150, -50), (0, -350)
29+
elif u in zero_width: outlines[name], metrics[name] = rect(-300, 300, -100, 500), (0, -300)
30+
else: outlines[name], metrics[name] = rect(50, 0, 550, 700), (600, 50)
31+
fb = FontBuilder(1000, isTTF=True)
32+
fb.setupGlyphOrder(names)
33+
fb.setupCharacterMap({u: f'u{u:04X}' for u in chars})
34+
fb.setupGlyf(outlines)
35+
fb.setupHorizontalMetrics(metrics)
36+
fb.setupHorizontalHeader(ascent=1000, descent=-300)
37+
fb.setupNameTable({'familyName': 'Fallback ' + file, 'styleName': 'Regular'})
38+
fb.setupOS2(); fb.setupPost()
39+
fb.save(os.path.join(out_dir, file))
40+
41+
42+
cases = []
43+
def shape(file, script, direction, texts):
44+
hbfont = hb.Font(hb.Face(hb.Blob.from_file_path(os.path.join(out_dir, file))))
45+
for text in texts:
46+
buf = hb.Buffer(); buf.add_str(text)
47+
buf.script = script; buf.direction = direction
48+
buf.flags = hb.BufferFlags.BOT | hb.BufferFlags.EOT
49+
hb.shape(hbfont, buf, {})
50+
cases.append({'font': file, 'script': script, 'text': text,
51+
'glyphs': [[i.codepoint, i.cluster, p.x_advance, p.x_offset, p.y_offset] for i, p in zip(buf.glyph_infos, buf.glyph_positions)]})
52+
53+
54+
# Thai: consonants, vowels and marks, and the private use glyphs of one vendor
55+
thai = list(range(0x0e01, 0x0e2f)) + list(range(0x0e30, 0x0e3b)) + list(range(0x0e40, 0x0e4f))
56+
above = {0x0e31, 0x0e34, 0x0e35, 0x0e36, 0x0e37, 0x0e47, 0x0e48, 0x0e49, 0x0e4a, 0x0e4b, 0x0e4c, 0x0e4d, 0x0e4e}
57+
below = {0x0e38, 0x0e39, 0x0e3a}
58+
windows = list(range(0xf700, 0xf71b))
59+
mac = list(range(0xf884, 0xf89f))
60+
build('thai-pua-windows.ttf', thai + windows, above | {u for u in windows if u not in (0xf700, 0xf70f) and not 0xf718 <= u <= 0xf71a}, below | {0xf718, 0xf719, 0xf71a})
61+
build('thai-pua-mac.ttf', thai + mac, above | {u for u in mac if u not in (0xf89a, 0xf89e) and not 0xf89b <= u <= 0xf89d}, below | {0xf89b, 0xf89c, 0xf89d})
62+
thai_texts = [
63+
'ปิ่น', 'ฝั่ง', 'ฟื้น', 'ป่า', 'ป้ำ', 'ฎุ', 'ฏู', 'ญุ', 'ฐู', 'กั่ก', 'ก่ำ', 'ปั๊ม', 'ปํ่า', 'ฟุ้ง', 'ป็่', 'ฬ้',
64+
'กิ่ ปี้ ฝึ๊ ฟื๋', 'ญู่ ฐุ้', 'ปุ่ ฎิ่',
65+
]
66+
# and random sequences of the Thai characters
67+
thai_texts += [''.join(chr(random.choice(thai)) for _ in range(random.randint(2, 7))) for _ in range(10)]
68+
for file in ['thai-pua-windows.ttf', 'thai-pua-mac.ttf']:
69+
shape(file, 'Thai', 'ltr', thai_texts)
70+
71+
# Hangul: jamo, some syllables (the others decompose, jamo sequences compose where the syllable is there), tone marks
72+
# (one with an advance), the dotted circle
73+
syllables = [0xac00, 0xac01, 0xb098, 0xb2e4, 0xd55c, 0xae00, 0xd0dc] # 가 각 나 다 한 글 태
74+
jamo = list(range(0x1100, 0x1113)) + list(range(0x1161, 0x1176)) + list(range(0x11a8, 0x11c3)) + [0xd7cb]
75+
build('hang-jamo.ttf', syllables + jamo + [0x302e, 0x302f, 0x25cc], set(), set(), zero_width={0x302e})
76+
hangul_texts = [
77+
'가', '각', '나', '한', '개', '갤',
78+
'한국어', '각', '간', '각', '갂', '가ퟋ', '가ퟋ', '한〮', '한〯', '〮한', '〯',
79+
'나ᅡ', '가각', '태극기', '글〯자', '가〮',
80+
]
81+
shape('hang-jamo.ttf', 'Hang', 'ltr', hangul_texts)
82+
83+
# Hebrew: letters, points and the presentation forms (no GPOS mark positioning, so they're used)
84+
letters = list(range(0x05d0, 0x05eb))
85+
points = list(range(0x05b0, 0x05bd)) + [0x05bf, 0x05c1, 0x05c2, 0x05c4, 0x05c5, 0x05c7]
86+
forms = [u for u in range(0xfb1d, 0xfb50) if u not in (0xfb37, 0xfb3d, 0xfb3f, 0xfb42, 0xfb45)]
87+
build('hebr-forms.ttf', letters + points + [0x05bd] + forms, {0x05b9, 0x05bf, 0x05c1, 0x05c2, 0x05c4}, set(points) - {0x05b9, 0x05bf, 0x05c1, 0x05c2, 0x05c4} | {0x05bd})
88+
hebrew_texts = [
89+
'בּ', 'שׁ', 'שׂ', 'שּׁ', 'שּׂ', 'אַ', 'אָ', 'וֹ', 'יִ', 'ײַ', 'בֿ', 'כֿ', 'פֿ', 'תּוֹרָה', 'שָׁלוֹם',
90+
'בָּֽ', 'בָֽ', 'אִָֽ', 'בַּֽ', 'רְ', 'ךּ', 'ןּ', 'שִׁ',
91+
]
92+
shape('hebr-forms.ttf', 'Hebr', 'rtl', hebrew_texts)
93+
94+
reference = os.path.join(out_dir, 'reference.json')
95+
data = json.load(open(reference))
96+
fallback_fonts = {c['font'] for c in cases}
97+
data['cases'] = [c for c in data['cases'] if c['font'] not in fallback_fonts] + cases
98+
json.dump(data, open(reference, 'w'), ensure_ascii=False, separators=(',', ':'))
99+
print(len(cases), 'fallback cases,', sum(os.path.getsize(os.path.join(out_dir, f)) for f in fallback_fonts), 'bytes of fonts')

0 commit comments

Comments
 (0)