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