1from fontTools import ttLib 2from fontTools.ttLib.standardGlyphOrder import standardGlyphOrder 3from fontTools.misc import sstruct 4from fontTools.misc.textTools import bytechr, byteord, tobytes, tostr, safeEval, readHex 5from . import DefaultTable 6import sys 7import struct 8import array 9import logging 10 11log = logging.getLogger(__name__) 12 13postFormat = """ 14 > 15 formatType: 16.16F 16 italicAngle: 16.16F # italic angle in degrees 17 underlinePosition: h 18 underlineThickness: h 19 isFixedPitch: L 20 minMemType42: L # minimum memory if TrueType font is downloaded 21 maxMemType42: L # maximum memory if TrueType font is downloaded 22 minMemType1: L # minimum memory if Type1 font is downloaded 23 maxMemType1: L # maximum memory if Type1 font is downloaded 24""" 25 26postFormatSize = sstruct.calcsize(postFormat) 27 28 29class table__p_o_s_t(DefaultTable.DefaultTable): 30 def decompile(self, data, ttFont): 31 sstruct.unpack(postFormat, data[:postFormatSize], self) 32 data = data[postFormatSize:] 33 if self.formatType == 1.0: 34 self.decode_format_1_0(data, ttFont) 35 elif self.formatType == 2.0: 36 self.decode_format_2_0(data, ttFont) 37 elif self.formatType == 3.0: 38 self.decode_format_3_0(data, ttFont) 39 elif self.formatType == 4.0: 40 self.decode_format_4_0(data, ttFont) 41 else: 42 # supported format 43 raise ttLib.TTLibError( 44 "'post' table format %f not supported" % self.formatType 45 ) 46 47 def compile(self, ttFont): 48 data = sstruct.pack(postFormat, self) 49 if self.formatType == 1.0: 50 pass # we're done 51 elif self.formatType == 2.0: 52 data = data + self.encode_format_2_0(ttFont) 53 elif self.formatType == 3.0: 54 pass # we're done 55 elif self.formatType == 4.0: 56 data = data + self.encode_format_4_0(ttFont) 57 else: 58 # supported format 59 raise ttLib.TTLibError( 60 "'post' table format %f not supported" % self.formatType 61 ) 62 return data 63 64 def getGlyphOrder(self): 65 """This function will get called by a ttLib.TTFont instance. 66 Do not call this function yourself, use TTFont().getGlyphOrder() 67 or its relatives instead! 68 """ 69 if not hasattr(self, "glyphOrder"): 70 raise ttLib.TTLibError("illegal use of getGlyphOrder()") 71 glyphOrder = self.glyphOrder 72 del self.glyphOrder 73 return glyphOrder 74 75 def decode_format_1_0(self, data, ttFont): 76 self.glyphOrder = standardGlyphOrder[: ttFont["maxp"].numGlyphs] 77 78 def decode_format_2_0(self, data, ttFont): 79 (numGlyphs,) = struct.unpack(">H", data[:2]) 80 numGlyphs = int(numGlyphs) 81 if numGlyphs > ttFont["maxp"].numGlyphs: 82 # Assume the numGlyphs field is bogus, so sync with maxp. 83 # I've seen this in one font, and if the assumption is 84 # wrong elsewhere, well, so be it: it's hard enough to 85 # work around _one_ non-conforming post format... 86 numGlyphs = ttFont["maxp"].numGlyphs 87 data = data[2:] 88 indices = array.array("H") 89 indices.frombytes(data[: 2 * numGlyphs]) 90 if sys.byteorder != "big": 91 indices.byteswap() 92 data = data[2 * numGlyphs :] 93 maxIndex = max(indices) 94 self.extraNames = extraNames = unpackPStrings(data, maxIndex - 257) 95 self.glyphOrder = glyphOrder = [""] * int(ttFont["maxp"].numGlyphs) 96 for glyphID in range(numGlyphs): 97 index = indices[glyphID] 98 if index > 257: 99 try: 100 name = extraNames[index - 258] 101 except IndexError: 102 name = "" 103 else: 104 # fetch names from standard list 105 name = standardGlyphOrder[index] 106 glyphOrder[glyphID] = name 107 self.build_psNameMapping(ttFont) 108 109 def build_psNameMapping(self, ttFont): 110 mapping = {} 111 allNames = {} 112 for i in range(ttFont["maxp"].numGlyphs): 113 glyphName = psName = self.glyphOrder[i] 114 if glyphName == "": 115 glyphName = "glyph%.5d" % i 116 if glyphName in allNames: 117 # make up a new glyphName that's unique 118 n = allNames[glyphName] 119 while (glyphName + "#" + str(n)) in allNames: 120 n += 1 121 allNames[glyphName] = n + 1 122 glyphName = glyphName + "#" + str(n) 123 124 self.glyphOrder[i] = glyphName 125 allNames[glyphName] = 1 126 if glyphName != psName: 127 mapping[glyphName] = psName 128 129 self.mapping = mapping 130 131 def decode_format_3_0(self, data, ttFont): 132 # Setting self.glyphOrder to None will cause the TTFont object 133 # try and construct glyph names from a Unicode cmap table. 134 self.glyphOrder = None 135 136 def decode_format_4_0(self, data, ttFont): 137 from fontTools import agl 138 139 numGlyphs = ttFont["maxp"].numGlyphs 140 indices = array.array("H") 141 indices.frombytes(data) 142 if sys.byteorder != "big": 143 indices.byteswap() 144 # In some older fonts, the size of the post table doesn't match 145 # the number of glyphs. Sometimes it's bigger, sometimes smaller. 146 self.glyphOrder = glyphOrder = [""] * int(numGlyphs) 147 for i in range(min(len(indices), numGlyphs)): 148 if indices[i] == 0xFFFF: 149 self.glyphOrder[i] = "" 150 elif indices[i] in agl.UV2AGL: 151 self.glyphOrder[i] = agl.UV2AGL[indices[i]] 152 else: 153 self.glyphOrder[i] = "uni%04X" % indices[i] 154 self.build_psNameMapping(ttFont) 155 156 def encode_format_2_0(self, ttFont): 157 numGlyphs = ttFont["maxp"].numGlyphs 158 glyphOrder = ttFont.getGlyphOrder() 159 assert len(glyphOrder) == numGlyphs 160 indices = array.array("H") 161 extraDict = {} 162 extraNames = self.extraNames = [ 163 n for n in self.extraNames if n not in standardGlyphOrder 164 ] 165 for i in range(len(extraNames)): 166 extraDict[extraNames[i]] = i 167 for glyphID in range(numGlyphs): 168 glyphName = glyphOrder[glyphID] 169 if glyphName in self.mapping: 170 psName = self.mapping[glyphName] 171 else: 172 psName = glyphName 173 if psName in extraDict: 174 index = 258 + extraDict[psName] 175 elif psName in standardGlyphOrder: 176 index = standardGlyphOrder.index(psName) 177 else: 178 index = 258 + len(extraNames) 179 extraDict[psName] = len(extraNames) 180 extraNames.append(psName) 181 indices.append(index) 182 if sys.byteorder != "big": 183 indices.byteswap() 184 return ( 185 struct.pack(">H", numGlyphs) + indices.tobytes() + packPStrings(extraNames) 186 ) 187 188 def encode_format_4_0(self, ttFont): 189 from fontTools import agl 190 191 numGlyphs = ttFont["maxp"].numGlyphs 192 glyphOrder = ttFont.getGlyphOrder() 193 assert len(glyphOrder) == numGlyphs 194 indices = array.array("H") 195 for glyphID in glyphOrder: 196 glyphID = glyphID.split("#")[0] 197 if glyphID in agl.AGL2UV: 198 indices.append(agl.AGL2UV[glyphID]) 199 elif len(glyphID) == 7 and glyphID[:3] == "uni": 200 indices.append(int(glyphID[3:], 16)) 201 else: 202 indices.append(0xFFFF) 203 if sys.byteorder != "big": 204 indices.byteswap() 205 return indices.tobytes() 206 207 def toXML(self, writer, ttFont): 208 formatstring, names, fixes = sstruct.getformat(postFormat) 209 for name in names: 210 value = getattr(self, name) 211 writer.simpletag(name, value=value) 212 writer.newline() 213 if hasattr(self, "mapping"): 214 writer.begintag("psNames") 215 writer.newline() 216 writer.comment( 217 "This file uses unique glyph names based on the information\n" 218 "found in the 'post' table. Since these names might not be unique,\n" 219 "we have to invent artificial names in case of clashes. In order to\n" 220 "be able to retain the original information, we need a name to\n" 221 "ps name mapping for those cases where they differ. That's what\n" 222 "you see below.\n" 223 ) 224 writer.newline() 225 items = sorted(self.mapping.items()) 226 for name, psName in items: 227 writer.simpletag("psName", name=name, psName=psName) 228 writer.newline() 229 writer.endtag("psNames") 230 writer.newline() 231 if hasattr(self, "extraNames"): 232 writer.begintag("extraNames") 233 writer.newline() 234 writer.comment( 235 "following are the name that are not taken from the standard Mac glyph order" 236 ) 237 writer.newline() 238 for name in self.extraNames: 239 writer.simpletag("psName", name=name) 240 writer.newline() 241 writer.endtag("extraNames") 242 writer.newline() 243 if hasattr(self, "data"): 244 writer.begintag("hexdata") 245 writer.newline() 246 writer.dumphex(self.data) 247 writer.endtag("hexdata") 248 writer.newline() 249 250 def fromXML(self, name, attrs, content, ttFont): 251 if name not in ("psNames", "extraNames", "hexdata"): 252 setattr(self, name, safeEval(attrs["value"])) 253 elif name == "psNames": 254 self.mapping = {} 255 for element in content: 256 if not isinstance(element, tuple): 257 continue 258 name, attrs, content = element 259 if name == "psName": 260 self.mapping[attrs["name"]] = attrs["psName"] 261 elif name == "extraNames": 262 self.extraNames = [] 263 for element in content: 264 if not isinstance(element, tuple): 265 continue 266 name, attrs, content = element 267 if name == "psName": 268 self.extraNames.append(attrs["name"]) 269 else: 270 self.data = readHex(content) 271 272 273def unpackPStrings(data, n): 274 # extract n Pascal strings from data. 275 # if there is not enough data, use "" 276 277 strings = [] 278 index = 0 279 dataLen = len(data) 280 281 for _ in range(n): 282 if dataLen <= index: 283 length = 0 284 else: 285 length = byteord(data[index]) 286 index += 1 287 288 if dataLen <= index + length - 1: 289 name = "" 290 else: 291 name = tostr(data[index : index + length], encoding="latin1") 292 strings.append(name) 293 index += length 294 295 if index < dataLen: 296 log.warning("%d extra bytes in post.stringData array", dataLen - index) 297 298 elif dataLen < index: 299 log.warning("not enough data in post.stringData array") 300 301 return strings 302 303 304def packPStrings(strings): 305 data = b"" 306 for s in strings: 307 data = data + bytechr(len(s)) + tobytes(s, encoding="latin1") 308 return data 309