• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from collections import UserDict, deque
2from functools import partial
3from fontTools.misc import sstruct
4from fontTools.misc.textTools import safeEval
5from . import DefaultTable
6import array
7import itertools
8import logging
9import struct
10import sys
11import fontTools.ttLib.tables.TupleVariation as tv
12
13
14log = logging.getLogger(__name__)
15TupleVariation = tv.TupleVariation
16
17
18# https://www.microsoft.com/typography/otspec/gvar.htm
19# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm
20#
21# Apple's documentation of 'gvar':
22# https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6gvar.html
23#
24# FreeType2 source code for parsing 'gvar':
25# http://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/src/truetype/ttgxvar.c
26
27GVAR_HEADER_FORMAT = """
28	> # big endian
29	version:			H
30	reserved:			H
31	axisCount:			H
32	sharedTupleCount:		H
33	offsetToSharedTuples:		I
34	glyphCount:			H
35	flags:				H
36	offsetToGlyphVariationData:	I
37"""
38
39GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
40
41
42class _LazyDict(UserDict):
43    def __init__(self, data):
44        super().__init__()
45        self.data = data
46
47    def __getitem__(self, k):
48        v = self.data[k]
49        if callable(v):
50            v = v()
51            self.data[k] = v
52        return v
53
54
55class table__g_v_a_r(DefaultTable.DefaultTable):
56    dependencies = ["fvar", "glyf"]
57
58    def __init__(self, tag=None):
59        DefaultTable.DefaultTable.__init__(self, tag)
60        self.version, self.reserved = 1, 0
61        self.variations = {}
62
63    def compile(self, ttFont):
64        axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
65        sharedTuples = tv.compileSharedTuples(
66            axisTags, itertools.chain(*self.variations.values())
67        )
68        sharedTupleIndices = {coord: i for i, coord in enumerate(sharedTuples)}
69        sharedTupleSize = sum([len(c) for c in sharedTuples])
70        compiledGlyphs = self.compileGlyphs_(ttFont, axisTags, sharedTupleIndices)
71        offset = 0
72        offsets = []
73        for glyph in compiledGlyphs:
74            offsets.append(offset)
75            offset += len(glyph)
76        offsets.append(offset)
77        compiledOffsets, tableFormat = self.compileOffsets_(offsets)
78
79        header = {}
80        header["version"] = self.version
81        header["reserved"] = self.reserved
82        header["axisCount"] = len(axisTags)
83        header["sharedTupleCount"] = len(sharedTuples)
84        header["offsetToSharedTuples"] = GVAR_HEADER_SIZE + len(compiledOffsets)
85        header["glyphCount"] = len(compiledGlyphs)
86        header["flags"] = tableFormat
87        header["offsetToGlyphVariationData"] = (
88            header["offsetToSharedTuples"] + sharedTupleSize
89        )
90        compiledHeader = sstruct.pack(GVAR_HEADER_FORMAT, header)
91
92        result = [compiledHeader, compiledOffsets]
93        result.extend(sharedTuples)
94        result.extend(compiledGlyphs)
95        return b"".join(result)
96
97    def compileGlyphs_(self, ttFont, axisTags, sharedCoordIndices):
98        result = []
99        glyf = ttFont["glyf"]
100        for glyphName in ttFont.getGlyphOrder():
101            variations = self.variations.get(glyphName, [])
102            if not variations:
103                result.append(b"")
104                continue
105            pointCountUnused = 0  # pointCount is actually unused by compileGlyph
106            result.append(
107                compileGlyph_(
108                    variations, pointCountUnused, axisTags, sharedCoordIndices
109                )
110            )
111        return result
112
113    def decompile(self, data, ttFont):
114        axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
115        glyphs = ttFont.getGlyphOrder()
116        sstruct.unpack(GVAR_HEADER_FORMAT, data[0:GVAR_HEADER_SIZE], self)
117        assert len(glyphs) == self.glyphCount
118        assert len(axisTags) == self.axisCount
119        offsets = self.decompileOffsets_(
120            data[GVAR_HEADER_SIZE:],
121            tableFormat=(self.flags & 1),
122            glyphCount=self.glyphCount,
123        )
124        sharedCoords = tv.decompileSharedTuples(
125            axisTags, self.sharedTupleCount, data, self.offsetToSharedTuples
126        )
127        variations = {}
128        offsetToData = self.offsetToGlyphVariationData
129        glyf = ttFont["glyf"]
130
131        def decompileVarGlyph(glyphName, gid):
132            gvarData = data[
133                offsetToData + offsets[gid] : offsetToData + offsets[gid + 1]
134            ]
135            if not gvarData:
136                return []
137            glyph = glyf[glyphName]
138            numPointsInGlyph = self.getNumPoints_(glyph)
139            return decompileGlyph_(numPointsInGlyph, sharedCoords, axisTags, gvarData)
140
141        for gid in range(self.glyphCount):
142            glyphName = glyphs[gid]
143            variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
144        self.variations = _LazyDict(variations)
145
146        if ttFont.lazy is False:  # Be lazy for None and True
147            self.ensureDecompiled()
148
149    def ensureDecompiled(self, recurse=False):
150        # The recurse argument is unused, but part of the signature of
151        # ensureDecompiled across the library.
152        # Use a zero-length deque to consume the lazy dict
153        deque(self.variations.values(), maxlen=0)
154
155    @staticmethod
156    def decompileOffsets_(data, tableFormat, glyphCount):
157        if tableFormat == 0:
158            # Short format: array of UInt16
159            offsets = array.array("H")
160            offsetsSize = (glyphCount + 1) * 2
161        else:
162            # Long format: array of UInt32
163            offsets = array.array("I")
164            offsetsSize = (glyphCount + 1) * 4
165        offsets.frombytes(data[0:offsetsSize])
166        if sys.byteorder != "big":
167            offsets.byteswap()
168
169        # In the short format, offsets need to be multiplied by 2.
170        # This is not documented in Apple's TrueType specification,
171        # but can be inferred from the FreeType implementation, and
172        # we could verify it with two sample GX fonts.
173        if tableFormat == 0:
174            offsets = [off * 2 for off in offsets]
175
176        return offsets
177
178    @staticmethod
179    def compileOffsets_(offsets):
180        """Packs a list of offsets into a 'gvar' offset table.
181
182        Returns a pair (bytestring, tableFormat). Bytestring is the
183        packed offset table. Format indicates whether the table
184        uses short (tableFormat=0) or long (tableFormat=1) integers.
185        The returned tableFormat should get packed into the flags field
186        of the 'gvar' header.
187        """
188        assert len(offsets) >= 2
189        for i in range(1, len(offsets)):
190            assert offsets[i - 1] <= offsets[i]
191        if max(offsets) <= 0xFFFF * 2:
192            packed = array.array("H", [n >> 1 for n in offsets])
193            tableFormat = 0
194        else:
195            packed = array.array("I", offsets)
196            tableFormat = 1
197        if sys.byteorder != "big":
198            packed.byteswap()
199        return (packed.tobytes(), tableFormat)
200
201    def toXML(self, writer, ttFont):
202        writer.simpletag("version", value=self.version)
203        writer.newline()
204        writer.simpletag("reserved", value=self.reserved)
205        writer.newline()
206        axisTags = [axis.axisTag for axis in ttFont["fvar"].axes]
207        for glyphName in ttFont.getGlyphNames():
208            variations = self.variations.get(glyphName)
209            if not variations:
210                continue
211            writer.begintag("glyphVariations", glyph=glyphName)
212            writer.newline()
213            for gvar in variations:
214                gvar.toXML(writer, axisTags)
215            writer.endtag("glyphVariations")
216            writer.newline()
217
218    def fromXML(self, name, attrs, content, ttFont):
219        if name == "version":
220            self.version = safeEval(attrs["value"])
221        elif name == "reserved":
222            self.reserved = safeEval(attrs["value"])
223        elif name == "glyphVariations":
224            if not hasattr(self, "variations"):
225                self.variations = {}
226            glyphName = attrs["glyph"]
227            glyph = ttFont["glyf"][glyphName]
228            numPointsInGlyph = self.getNumPoints_(glyph)
229            glyphVariations = []
230            for element in content:
231                if isinstance(element, tuple):
232                    name, attrs, content = element
233                    if name == "tuple":
234                        gvar = TupleVariation({}, [None] * numPointsInGlyph)
235                        glyphVariations.append(gvar)
236                        for tupleElement in content:
237                            if isinstance(tupleElement, tuple):
238                                tupleName, tupleAttrs, tupleContent = tupleElement
239                                gvar.fromXML(tupleName, tupleAttrs, tupleContent)
240            self.variations[glyphName] = glyphVariations
241
242    @staticmethod
243    def getNumPoints_(glyph):
244        NUM_PHANTOM_POINTS = 4
245
246        if glyph.isComposite():
247            return len(glyph.components) + NUM_PHANTOM_POINTS
248        elif glyph.isVarComposite():
249            count = 0
250            for component in glyph.components:
251                count += component.getPointCount()
252            return count + NUM_PHANTOM_POINTS
253        else:
254            # Empty glyphs (eg. space, nonmarkingreturn) have no "coordinates" attribute.
255            return len(getattr(glyph, "coordinates", [])) + NUM_PHANTOM_POINTS
256
257
258def compileGlyph_(variations, pointCount, axisTags, sharedCoordIndices):
259    tupleVariationCount, tuples, data = tv.compileTupleVariationStore(
260        variations, pointCount, axisTags, sharedCoordIndices
261    )
262    if tupleVariationCount == 0:
263        return b""
264    result = [struct.pack(">HH", tupleVariationCount, 4 + len(tuples)), tuples, data]
265    if (len(tuples) + len(data)) % 2 != 0:
266        result.append(b"\0")  # padding
267    return b"".join(result)
268
269
270def decompileGlyph_(pointCount, sharedTuples, axisTags, data):
271    if len(data) < 4:
272        return []
273    tupleVariationCount, offsetToData = struct.unpack(">HH", data[:4])
274    dataPos = offsetToData
275    return tv.decompileTupleVariationStore(
276        "gvar",
277        axisTags,
278        tupleVariationCount,
279        pointCount,
280        sharedTuples,
281        data,
282        4,
283        offsetToData,
284    )
285