• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc.testTools import FakeFont, getXML, parseXML
2from fontTools.misc.textTools import deHexStr, hexStr
3from fontTools.ttLib import TTLibError, getTableClass, getTableModule, newTable
4import unittest
5from fontTools.ttLib.tables.TupleVariation import TupleVariation
6
7
8gvarClass = getTableClass("gvar")
9
10
11GVAR_DATA = deHexStr(
12    "0001 0000 "  #   0: majorVersion=1 minorVersion=0
13    "0002 0000 "  #   4: axisCount=2 sharedTupleCount=0
14    "0000001C "  #   8: offsetToSharedTuples=28
15    "0003 0000 "  #  12: glyphCount=3 flags=0
16    "0000001C "  #  16: offsetToGlyphVariationData=28
17    "0000 0000 000C 002F "  #  20: offsets=[0,0,12,47], times 2: [0,0,24,94],
18    #                 #           +offsetToGlyphVariationData: [28,28,52,122]
19    #
20    # 28: Glyph variation data for glyph #0, ".notdef"
21    # ------------------------------------------------
22    # (no variation data for this glyph)
23    #
24    # 28: Glyph variation data for glyph #1, "space"
25    # ----------------------------------------------
26    "8001 000C "  #  28: tupleVariationCount=1|TUPLES_SHARE_POINT_NUMBERS, offsetToData=12(+28=40)
27    "000A "  #  32: tvHeader[0].variationDataSize=10
28    "8000 "  #  34: tvHeader[0].tupleIndex=EMBEDDED_PEAK
29    "0000 2CCD "  #  36: tvHeader[0].peakTuple={wght:0.0, wdth:0.7}
30    "00 "  #  40: all points
31    "03 01 02 03 04 "  #  41: deltaX=[1, 2, 3, 4]
32    "03 0b 16 21 2C "  #  46: deltaY=[11, 22, 33, 44]
33    "00 "  #  51: padding
34    #
35    # 52: Glyph variation data for glyph #2, "I"
36    # ------------------------------------------
37    "8002 001c "  #  52: tupleVariationCount=2|TUPLES_SHARE_POINT_NUMBERS, offsetToData=28(+52=80)
38    "0012 "  #  56: tvHeader[0].variationDataSize=18
39    "C000 "  #  58: tvHeader[0].tupleIndex=EMBEDDED_PEAK|INTERMEDIATE_REGION
40    "2000 0000 "  #  60: tvHeader[0].peakTuple={wght:0.5, wdth:0.0}
41    "0000 0000 "  #  64: tvHeader[0].intermediateStart={wght:0.0, wdth:0.0}
42    "4000 0000 "  #  68: tvHeader[0].intermediateEnd={wght:1.0, wdth:0.0}
43    "0016 "  #  72: tvHeader[1].variationDataSize=22
44    "A000 "  #  74: tvHeader[1].tupleIndex=EMBEDDED_PEAK|PRIVATE_POINTS
45    "C000 3333 "  #  76: tvHeader[1].peakTuple={wght:-1.0, wdth:0.8}
46    "00 "  #  80: all points
47    "07 03 01 04 01 "  #  81: deltaX.len=7, deltaX=[3, 1, 4, 1,
48    "05 09 02 06 "  #  86:                       5, 9, 2, 6]
49    "07 03 01 04 01 "  #  90: deltaY.len=7, deltaY=[3, 1, 4, 1,
50    "05 09 02 06 "  #  95:                       5, 9, 2, 6]
51    "06 "  #  99: 6 points
52    "05 00 01 03 01 "  # 100: runLen=5(+1=6); delta-encoded run=[0, 1, 4, 5,
53    "01 01 "  # 105:                                    6, 7]
54    "05 f8 07 fc 03 fe 01 "  # 107: deltaX.len=5, deltaX=[-8,7,-4,3,-2,1]
55    "05 a8 4d 2c 21 ea 0b "  # 114: deltaY.len=5, deltaY=[-88,77,44,33,-22,11]
56    "00"  # 121: padding
57)  # 122: <end>
58assert len(GVAR_DATA) == 122
59
60
61GVAR_VARIATIONS = {
62    ".notdef": [],
63    "space": [
64        TupleVariation(
65            {"wdth": (0.0, 0.7000122, 0.7000122)}, [(1, 11), (2, 22), (3, 33), (4, 44)]
66        ),
67    ],
68    "I": [
69        TupleVariation(
70            {"wght": (0.0, 0.5, 1.0)},
71            [(3, 3), (1, 1), (4, 4), (1, 1), (5, 5), (9, 9), (2, 2), (6, 6)],
72        ),
73        TupleVariation(
74            {"wght": (-1.0, -1.0, 0.0), "wdth": (0.0, 0.7999878, 0.7999878)},
75            [(-8, -88), (7, 77), None, None, (-4, 44), (3, 33), (-2, -22), (1, 11)],
76        ),
77    ],
78}
79
80
81GVAR_XML = [
82    '<version value="1"/>',
83    '<reserved value="0"/>',
84    '<glyphVariations glyph="I">',
85    "  <tuple>",
86    '    <coord axis="wght" min="0.0" value="0.5" max="1.0"/>',
87    '    <delta pt="0" x="3" y="3"/>',
88    '    <delta pt="1" x="1" y="1"/>',
89    '    <delta pt="2" x="4" y="4"/>',
90    '    <delta pt="3" x="1" y="1"/>',
91    '    <delta pt="4" x="5" y="5"/>',
92    '    <delta pt="5" x="9" y="9"/>',
93    '    <delta pt="6" x="2" y="2"/>',
94    '    <delta pt="7" x="6" y="6"/>',
95    "  </tuple>",
96    "  <tuple>",
97    '    <coord axis="wght" value="-1.0"/>',
98    '    <coord axis="wdth" value="0.8"/>',
99    '    <delta pt="0" x="-8" y="-88"/>',
100    '    <delta pt="1" x="7" y="77"/>',
101    '    <delta pt="4" x="-4" y="44"/>',
102    '    <delta pt="5" x="3" y="33"/>',
103    '    <delta pt="6" x="-2" y="-22"/>',
104    '    <delta pt="7" x="1" y="11"/>',
105    "  </tuple>",
106    "</glyphVariations>",
107    '<glyphVariations glyph="space">',
108    "  <tuple>",
109    '    <coord axis="wdth" value="0.7"/>',
110    '    <delta pt="0" x="1" y="11"/>',
111    '    <delta pt="1" x="2" y="22"/>',
112    '    <delta pt="2" x="3" y="33"/>',
113    '    <delta pt="3" x="4" y="44"/>',
114    "  </tuple>",
115    "</glyphVariations>",
116]
117
118
119GVAR_DATA_EMPTY_VARIATIONS = deHexStr(
120    "0001 0000 "  #  0: majorVersion=1 minorVersion=0
121    "0002 0000 "  #  4: axisCount=2 sharedTupleCount=0
122    "0000001c "  #  8: offsetToSharedTuples=28
123    "0003 0000 "  # 12: glyphCount=3 flags=0
124    "0000001c "  # 16: offsetToGlyphVariationData=28
125    "0000 0000 0000 0000"  # 20: offsets=[0, 0, 0, 0]
126)  # 28: <end>
127
128
129def hexencode(s):
130    h = hexStr(s).upper()
131    return " ".join([h[i : i + 2] for i in range(0, len(h), 2)])
132
133
134class GVARTableTest(unittest.TestCase):
135    def assertVariationsAlmostEqual(self, vars1, vars2):
136        self.assertSetEqual(set(vars1.keys()), set(vars2.keys()))
137        for glyphName, variations1 in vars1.items():
138            variations2 = vars2[glyphName]
139            self.assertEqual(len(variations1), len(variations2))
140            for v1, v2 in zip(variations1, variations2):
141                self.assertSetEqual(set(v1.axes), set(v2.axes))
142                for axisTag, support1 in v1.axes.items():
143                    support2 = v2.axes[axisTag]
144                    self.assertEqual(len(support1), len(support2))
145                    for s1, s2 in zip(support1, support2):
146                        self.assertAlmostEqual(s1, s2)
147                self.assertEqual(v1.coordinates, v2.coordinates)
148
149    def makeFont(self, variations):
150        glyphs = [".notdef", "space", "I"]
151        Axis = getTableModule("fvar").Axis
152        Glyph = getTableModule("glyf").Glyph
153        glyf, fvar, gvar = newTable("glyf"), newTable("fvar"), newTable("gvar")
154        font = FakeFont(glyphs)
155        font.tables = {"glyf": glyf, "gvar": gvar, "fvar": fvar}
156        glyf.glyphs = {glyph: Glyph() for glyph in glyphs}
157        glyf.glyphs["I"].coordinates = [(10, 10), (10, 20), (20, 20), (20, 10)]
158        fvar.axes = [Axis(), Axis()]
159        fvar.axes[0].axisTag, fvar.axes[1].axisTag = "wght", "wdth"
160        gvar.variations = variations
161        return font, gvar
162
163    def test_compile(self):
164        font, gvar = self.makeFont(GVAR_VARIATIONS)
165        self.assertEqual(hexStr(gvar.compile(font)), hexStr(GVAR_DATA))
166
167    def test_compile_noVariations(self):
168        font, gvar = self.makeFont({})
169        self.assertEqual(hexStr(gvar.compile(font)), hexStr(GVAR_DATA_EMPTY_VARIATIONS))
170
171    def test_compile_emptyVariations(self):
172        font, gvar = self.makeFont({".notdef": [], "space": [], "I": []})
173        self.assertEqual(hexStr(gvar.compile(font)), hexStr(GVAR_DATA_EMPTY_VARIATIONS))
174
175    def test_decompile(self):
176        for lazy in (True, False, None):
177            with self.subTest(lazy=lazy):
178                font, gvar = self.makeFont({})
179                font.lazy = lazy
180                gvar.decompile(GVAR_DATA, font)
181
182                self.assertEqual(
183                    all(callable(v) for v in gvar.variations.data.values()),
184                    lazy is not False,
185                )
186
187                self.assertVariationsAlmostEqual(gvar.variations, GVAR_VARIATIONS)
188
189    def test_decompile_noVariations(self):
190        font, gvar = self.makeFont({})
191        gvar.decompile(GVAR_DATA_EMPTY_VARIATIONS, font)
192        self.assertEqual(gvar.variations, {".notdef": [], "space": [], "I": []})
193
194    def test_fromXML(self):
195        font, gvar = self.makeFont({})
196        for name, attrs, content in parseXML(GVAR_XML):
197            gvar.fromXML(name, attrs, content, ttFont=font)
198        self.assertVariationsAlmostEqual(
199            gvar.variations, {g: v for g, v in GVAR_VARIATIONS.items() if v}
200        )
201
202    def test_toXML(self):
203        font, gvar = self.makeFont(GVAR_VARIATIONS)
204        self.assertEqual(getXML(gvar.toXML, font), GVAR_XML)
205
206    def test_compileOffsets_shortFormat(self):
207        self.assertEqual(
208            (deHexStr("00 00 00 02 FF C0"), 0),
209            gvarClass.compileOffsets_([0, 4, 0x1FF80]),
210        )
211
212    def test_compileOffsets_longFormat(self):
213        self.assertEqual(
214            (deHexStr("00 00 00 00 00 00 00 04 CA FE BE EF"), 1),
215            gvarClass.compileOffsets_([0, 4, 0xCAFEBEEF]),
216        )
217
218    def test_decompileOffsets_shortFormat(self):
219        decompileOffsets = gvarClass.decompileOffsets_
220        data = deHexStr("00 11 22 33 44 55 66 77 88 99 aa bb")
221        self.assertEqual(
222            [2 * 0x0011, 2 * 0x2233, 2 * 0x4455, 2 * 0x6677, 2 * 0x8899, 2 * 0xAABB],
223            list(decompileOffsets(data, tableFormat=0, glyphCount=5)),
224        )
225
226    def test_decompileOffsets_longFormat(self):
227        decompileOffsets = gvarClass.decompileOffsets_
228        data = deHexStr("00 11 22 33 44 55 66 77 88 99 aa bb")
229        self.assertEqual(
230            [0x00112233, 0x44556677, 0x8899AABB],
231            list(decompileOffsets(data, tableFormat=1, glyphCount=2)),
232        )
233
234
235if __name__ == "__main__":
236    import sys
237
238    sys.exit(unittest.main())
239