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