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