1from . import DefaultTable 2from fontTools.misc import sstruct 3from fontTools.misc.textTools import safeEval 4import struct 5 6VDMX_HeaderFmt = """ 7 > # big endian 8 version: H # Version number (0 or 1) 9 numRecs: H # Number of VDMX groups present 10 numRatios: H # Number of aspect ratio groupings 11""" 12# the VMDX header is followed by an array of RatRange[numRatios] (i.e. aspect 13# ratio ranges); 14VDMX_RatRangeFmt = """ 15 > # big endian 16 bCharSet: B # Character set 17 xRatio: B # Value to use for x-Ratio 18 yStartRatio: B # Starting y-Ratio value 19 yEndRatio: B # Ending y-Ratio value 20""" 21# followed by an array of offset[numRatios] from start of VDMX table to the 22# VDMX Group for this ratio range (offsets will be re-calculated on compile); 23# followed by an array of Group[numRecs] records; 24VDMX_GroupFmt = """ 25 > # big endian 26 recs: H # Number of height records in this group 27 startsz: B # Starting yPelHeight 28 endsz: B # Ending yPelHeight 29""" 30# followed by an array of vTable[recs] records. 31VDMX_vTableFmt = """ 32 > # big endian 33 yPelHeight: H # yPelHeight to which values apply 34 yMax: h # Maximum value (in pels) for this yPelHeight 35 yMin: h # Minimum value (in pels) for this yPelHeight 36""" 37 38 39class table_V_D_M_X_(DefaultTable.DefaultTable): 40 41 def decompile(self, data, ttFont): 42 pos = 0 # track current position from to start of VDMX table 43 dummy, data = sstruct.unpack2(VDMX_HeaderFmt, data, self) 44 pos += sstruct.calcsize(VDMX_HeaderFmt) 45 self.ratRanges = [] 46 for i in range(self.numRatios): 47 ratio, data = sstruct.unpack2(VDMX_RatRangeFmt, data) 48 pos += sstruct.calcsize(VDMX_RatRangeFmt) 49 # the mapping between a ratio and a group is defined further below 50 ratio['groupIndex'] = None 51 self.ratRanges.append(ratio) 52 lenOffset = struct.calcsize('>H') 53 _offsets = [] # temporarily store offsets to groups 54 for i in range(self.numRatios): 55 offset = struct.unpack('>H', data[0:lenOffset])[0] 56 data = data[lenOffset:] 57 pos += lenOffset 58 _offsets.append(offset) 59 self.groups = [] 60 for groupIndex in range(self.numRecs): 61 # the offset to this group from beginning of the VDMX table 62 currOffset = pos 63 group, data = sstruct.unpack2(VDMX_GroupFmt, data) 64 # the group lenght and bounding sizes are re-calculated on compile 65 recs = group.pop('recs') 66 startsz = group.pop('startsz') 67 endsz = group.pop('endsz') 68 pos += sstruct.calcsize(VDMX_GroupFmt) 69 for j in range(recs): 70 vTable, data = sstruct.unpack2(VDMX_vTableFmt, data) 71 vTableLength = sstruct.calcsize(VDMX_vTableFmt) 72 pos += vTableLength 73 # group is a dict of (yMax, yMin) tuples keyed by yPelHeight 74 group[vTable['yPelHeight']] = (vTable['yMax'], vTable['yMin']) 75 # make sure startsz and endsz match the calculated values 76 minSize = min(group.keys()) 77 maxSize = max(group.keys()) 78 assert startsz == minSize, \ 79 "startsz (%s) must equal min yPelHeight (%s): group %d" % \ 80 (group.startsz, minSize, groupIndex) 81 assert endsz == maxSize, \ 82 "endsz (%s) must equal max yPelHeight (%s): group %d" % \ 83 (group.endsz, maxSize, groupIndex) 84 self.groups.append(group) 85 # match the defined offsets with the current group's offset 86 for offsetIndex, offsetValue in enumerate(_offsets): 87 # when numRecs < numRatios there can more than one ratio range 88 # sharing the same VDMX group 89 if currOffset == offsetValue: 90 # map the group with the ratio range thas has the same 91 # index as the offset to that group (it took me a while..) 92 self.ratRanges[offsetIndex]['groupIndex'] = groupIndex 93 # check that all ratio ranges have a group 94 for i in range(self.numRatios): 95 ratio = self.ratRanges[i] 96 if ratio['groupIndex'] is None: 97 from fontTools import ttLib 98 raise ttLib.TTLibError( 99 "no group defined for ratRange %d" % i) 100 101 def _getOffsets(self): 102 """ 103 Calculate offsets to VDMX_Group records. 104 For each ratRange return a list of offset values from the beginning of 105 the VDMX table to a VDMX_Group. 106 """ 107 lenHeader = sstruct.calcsize(VDMX_HeaderFmt) 108 lenRatRange = sstruct.calcsize(VDMX_RatRangeFmt) 109 lenOffset = struct.calcsize('>H') 110 lenGroupHeader = sstruct.calcsize(VDMX_GroupFmt) 111 lenVTable = sstruct.calcsize(VDMX_vTableFmt) 112 # offset to the first group 113 pos = lenHeader + self.numRatios*lenRatRange + self.numRatios*lenOffset 114 groupOffsets = [] 115 for group in self.groups: 116 groupOffsets.append(pos) 117 lenGroup = lenGroupHeader + len(group) * lenVTable 118 pos += lenGroup # offset to next group 119 offsets = [] 120 for ratio in self.ratRanges: 121 groupIndex = ratio['groupIndex'] 122 offsets.append(groupOffsets[groupIndex]) 123 return offsets 124 125 def compile(self, ttFont): 126 if not(self.version == 0 or self.version == 1): 127 from fontTools import ttLib 128 raise ttLib.TTLibError( 129 "unknown format for VDMX table: version %s" % self.version) 130 data = sstruct.pack(VDMX_HeaderFmt, self) 131 for ratio in self.ratRanges: 132 data += sstruct.pack(VDMX_RatRangeFmt, ratio) 133 # recalculate offsets to VDMX groups 134 for offset in self._getOffsets(): 135 data += struct.pack('>H', offset) 136 for group in self.groups: 137 recs = len(group) 138 startsz = min(group.keys()) 139 endsz = max(group.keys()) 140 gHeader = {'recs': recs, 'startsz': startsz, 'endsz': endsz} 141 data += sstruct.pack(VDMX_GroupFmt, gHeader) 142 for yPelHeight, (yMax, yMin) in sorted(group.items()): 143 vTable = {'yPelHeight': yPelHeight, 'yMax': yMax, 'yMin': yMin} 144 data += sstruct.pack(VDMX_vTableFmt, vTable) 145 return data 146 147 def toXML(self, writer, ttFont): 148 writer.simpletag("version", value=self.version) 149 writer.newline() 150 writer.begintag("ratRanges") 151 writer.newline() 152 for ratio in self.ratRanges: 153 groupIndex = ratio['groupIndex'] 154 writer.simpletag( 155 "ratRange", 156 bCharSet=ratio['bCharSet'], 157 xRatio=ratio['xRatio'], 158 yStartRatio=ratio['yStartRatio'], 159 yEndRatio=ratio['yEndRatio'], 160 groupIndex=groupIndex 161 ) 162 writer.newline() 163 writer.endtag("ratRanges") 164 writer.newline() 165 writer.begintag("groups") 166 writer.newline() 167 for groupIndex in range(self.numRecs): 168 group = self.groups[groupIndex] 169 recs = len(group) 170 startsz = min(group.keys()) 171 endsz = max(group.keys()) 172 writer.begintag("group", index=groupIndex) 173 writer.newline() 174 writer.comment("recs=%d, startsz=%d, endsz=%d" % 175 (recs, startsz, endsz)) 176 writer.newline() 177 for yPelHeight, (yMax, yMin) in sorted(group.items()): 178 writer.simpletag( 179 "record", 180 [('yPelHeight', yPelHeight), ('yMax', yMax), ('yMin', yMin)]) 181 writer.newline() 182 writer.endtag("group") 183 writer.newline() 184 writer.endtag("groups") 185 writer.newline() 186 187 def fromXML(self, name, attrs, content, ttFont): 188 if name == "version": 189 self.version = safeEval(attrs["value"]) 190 elif name == "ratRanges": 191 if not hasattr(self, "ratRanges"): 192 self.ratRanges = [] 193 for element in content: 194 if not isinstance(element, tuple): 195 continue 196 name, attrs, content = element 197 if name == "ratRange": 198 if not hasattr(self, "numRatios"): 199 self.numRatios = 1 200 else: 201 self.numRatios += 1 202 ratio = { 203 "bCharSet": safeEval(attrs["bCharSet"]), 204 "xRatio": safeEval(attrs["xRatio"]), 205 "yStartRatio": safeEval(attrs["yStartRatio"]), 206 "yEndRatio": safeEval(attrs["yEndRatio"]), 207 "groupIndex": safeEval(attrs["groupIndex"]) 208 } 209 self.ratRanges.append(ratio) 210 elif name == "groups": 211 if not hasattr(self, "groups"): 212 self.groups = [] 213 for element in content: 214 if not isinstance(element, tuple): 215 continue 216 name, attrs, content = element 217 if name == "group": 218 if not hasattr(self, "numRecs"): 219 self.numRecs = 1 220 else: 221 self.numRecs += 1 222 group = {} 223 for element in content: 224 if not isinstance(element, tuple): 225 continue 226 name, attrs, content = element 227 if name == "record": 228 yPelHeight = safeEval(attrs["yPelHeight"]) 229 yMax = safeEval(attrs["yMax"]) 230 yMin = safeEval(attrs["yMin"]) 231 group[yPelHeight] = (yMax, yMin) 232 self.groups.append(group) 233