• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import io
2import os
3import re
4import random
5from fontTools.feaLib.builder import addOpenTypeFeaturesFromString
6from fontTools.ttLib import TTFont, newTable, registerCustomTableClass, unregisterCustomTableClass
7from fontTools.ttLib.tables.DefaultTable import DefaultTable
8from fontTools.ttLib.tables._c_m_a_p import CmapSubtable
9import pytest
10
11
12DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
13
14
15class CustomTableClass(DefaultTable):
16
17    def decompile(self, data, ttFont):
18        self.numbers = list(data)
19
20    def compile(self, ttFont):
21        return bytes(self.numbers)
22
23    # not testing XML read/write
24
25
26table_C_U_S_T_ = CustomTableClass  # alias for testing
27
28
29TABLETAG = "CUST"
30
31
32def normalize_TTX(string):
33    string = re.sub(' ttLibVersion=".*"', "", string)
34    string = re.sub('checkSumAdjustment value=".*"', "", string)
35    string = re.sub('modified value=".*"', "", string)
36    return string
37
38
39def test_registerCustomTableClass():
40    font = TTFont()
41    font[TABLETAG] = newTable(TABLETAG)
42    font[TABLETAG].data = b"\x00\x01\xff"
43    f = io.BytesIO()
44    font.save(f)
45    f.seek(0)
46    assert font[TABLETAG].data == b"\x00\x01\xff"
47    registerCustomTableClass(TABLETAG, "ttFont_test", "CustomTableClass")
48    try:
49        font = TTFont(f)
50        assert font[TABLETAG].numbers == [0, 1, 255]
51        assert font[TABLETAG].compile(font) == b"\x00\x01\xff"
52    finally:
53        unregisterCustomTableClass(TABLETAG)
54
55
56def test_registerCustomTableClassStandardName():
57    registerCustomTableClass(TABLETAG, "ttFont_test")
58    try:
59        font = TTFont()
60        font[TABLETAG] = newTable(TABLETAG)
61        font[TABLETAG].numbers = [4, 5, 6]
62        assert font[TABLETAG].compile(font) == b"\x04\x05\x06"
63    finally:
64        unregisterCustomTableClass(TABLETAG)
65
66
67ttxTTF = r"""<?xml version="1.0" encoding="UTF-8"?>
68<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.9.0">
69  <hmtx>
70    <mtx name=".notdef" width="300" lsb="0"/>
71  </hmtx>
72</ttFont>
73"""
74
75
76ttxOTF = """<?xml version="1.0" encoding="UTF-8"?>
77<ttFont sfntVersion="OTTO" ttLibVersion="4.9.0">
78  <hmtx>
79    <mtx name=".notdef" width="300" lsb="0"/>
80  </hmtx>
81</ttFont>
82"""
83
84
85def test_sfntVersionFromTTX():
86    # https://github.com/fonttools/fonttools/issues/2370
87    font = TTFont()
88    assert font.sfntVersion == "\x00\x01\x00\x00"
89    ttx = io.StringIO(ttxOTF)
90    # Font is "empty", TTX file will determine sfntVersion
91    font.importXML(ttx)
92    assert font.sfntVersion == "OTTO"
93    ttx = io.StringIO(ttxTTF)
94    # Font is not "empty", sfntVersion in TTX file will be ignored
95    font.importXML(ttx)
96    assert font.sfntVersion == "OTTO"
97
98
99def test_virtualGlyphId():
100    otfpath = os.path.join(DATA_DIR, "TestVGID-Regular.otf")
101    ttxpath = os.path.join(DATA_DIR, "TestVGID-Regular.ttx")
102
103    otf = TTFont(otfpath)
104
105    ttx = TTFont()
106    ttx.importXML(ttxpath)
107
108    with open(ttxpath, encoding="utf-8") as fp:
109        xml = normalize_TTX(fp.read()).splitlines()
110
111    for font in (otf, ttx):
112        GSUB = font["GSUB"].table
113        assert GSUB.LookupList.LookupCount == 37
114        lookup = GSUB.LookupList.Lookup[32]
115        assert lookup.LookupType == 8
116        subtable = lookup.SubTable[0]
117        assert subtable.LookAheadGlyphCount == 1
118        lookahead = subtable.LookAheadCoverage[0]
119        assert len(lookahead.glyphs) == 46
120        assert "glyph00453" in lookahead.glyphs
121
122        out = io.StringIO()
123        font.saveXML(out)
124        outxml = normalize_TTX(out.getvalue()).splitlines()
125        assert xml == outxml
126
127
128def test_setGlyphOrder_also_updates_glyf_glyphOrder():
129    # https://github.com/fonttools/fonttools/issues/2060#issuecomment-1063932428
130    font = TTFont()
131    font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx"))
132    current_order = font.getGlyphOrder()
133
134    assert current_order == font["glyf"].glyphOrder
135
136    new_order = list(current_order)
137    while new_order == current_order:
138        random.shuffle(new_order)
139
140    font.setGlyphOrder(new_order)
141
142    assert font.getGlyphOrder() == new_order
143    assert font["glyf"].glyphOrder == new_order
144
145
146@pytest.mark.parametrize("lazy", [None, True, False])
147def test_ensureDecompiled(lazy):
148    # test that no matter the lazy value, ensureDecompiled decompiles all tables
149    font = TTFont()
150    font.importXML(os.path.join(DATA_DIR, "TestTTF-Regular.ttx"))
151    # test font has no OTL so we add some, as an example of otData-driven tables
152    addOpenTypeFeaturesFromString(
153        font,
154        """
155        feature calt {
156            sub period' period' period' space by ellipsis;
157        } calt;
158
159        feature dist {
160            pos period period -30;
161        } dist;
162        """
163    )
164    # also add an additional cmap subtable that will be lazily-loaded
165    cm = CmapSubtable.newSubtable(14)
166    cm.platformID = 0
167    cm.platEncID = 5
168    cm.language = 0
169    cm.cmap = {}
170    cm.uvsDict = {0xFE00: [(0x002e, None)]}
171    font["cmap"].tables.append(cm)
172
173    # save and reload, potentially lazily
174    buf = io.BytesIO()
175    font.save(buf)
176    buf.seek(0)
177    font = TTFont(buf, lazy=lazy)
178
179    # check no table is loaded until/unless requested, no matter the laziness
180    for tag in font.keys():
181        assert not font.isLoaded(tag)
182
183    if lazy is not False:
184        # additional cmap doesn't get decompiled automatically unless lazy=False;
185        # can't use hasattr or else cmap's maginc __getattr__ kicks in...
186        cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14)
187        assert cm.data is not None
188        assert "uvsDict" not in cm.__dict__
189        # glyf glyphs are not expanded unless lazy=False
190        assert font["glyf"].glyphs["period"].data is not None
191        assert not hasattr(font["glyf"].glyphs["period"], "coordinates")
192
193    if lazy is True:
194        # OTL tables hold a 'reader' to lazily load when lazy=True
195        assert "reader" in font["GSUB"].table.LookupList.__dict__
196        assert "reader" in font["GPOS"].table.LookupList.__dict__
197
198    font.ensureDecompiled()
199
200    # all tables are decompiled now
201    for tag in font.keys():
202        assert font.isLoaded(tag)
203    # including the additional cmap
204    cm = next(st for st in font["cmap"].tables if st.__dict__["format"] == 14)
205    assert cm.data is None
206    assert "uvsDict" in cm.__dict__
207    # expanded glyf glyphs lost the 'data' attribute
208    assert not hasattr(font["glyf"].glyphs["period"], "data")
209    assert hasattr(font["glyf"].glyphs["period"], "coordinates")
210    # and OTL tables have read their 'reader'
211    assert "reader" not in font["GSUB"].table.LookupList.__dict__
212    assert "Lookup" in font["GSUB"].table.LookupList.__dict__
213    assert "reader" not in font["GPOS"].table.LookupList.__dict__
214    assert "Lookup" in font["GPOS"].table.LookupList.__dict__
215