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