|
| 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')) |
0 commit comments