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