Coverage for /usr/lib/python3/dist-packages/fontTools/ttLib/tables/_g_v_a_r.py: 19%
185 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
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
14log = logging.getLogger(__name__)
15TupleVariation = tv.TupleVariation
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
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"""
39GVAR_HEADER_SIZE = sstruct.calcsize(GVAR_HEADER_FORMAT)
42class _LazyDict(UserDict):
43 def __init__(self, data):
44 super().__init__()
45 self.data = data
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
55class table__g_v_a_r(DefaultTable.DefaultTable):
56 dependencies = ["fvar", "glyf"]
58 def __init__(self, tag=None):
59 DefaultTable.DefaultTable.__init__(self, tag)
60 self.version, self.reserved = 1, 0
61 self.variations = {}
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)
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)
92 result = [compiledHeader, compiledOffsets]
93 result.extend(sharedTuples)
94 result.extend(compiledGlyphs)
95 return b"".join(result)
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
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"]
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)
141 for gid in range(self.glyphCount):
142 glyphName = glyphs[gid]
143 variations[glyphName] = partial(decompileVarGlyph, glyphName, gid)
144 self.variations = _LazyDict(variations)
146 if ttFont.lazy is False: # Be lazy for None and True
147 self.ensureDecompiled()
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)
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()
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]
176 return offsets
178 @staticmethod
179 def compileOffsets_(offsets):
180 """Packs a list of offsets into a 'gvar' offset table.
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)
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()
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
242 @staticmethod
243 def getNumPoints_(glyph):
244 NUM_PHANTOM_POINTS = 4
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
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)
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 )