1"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts (Python2 only) 2 3Functions for reading and writing raw Type 1 data: 4 5read(path) 6 reads any Type 1 font file, returns the raw data and a type indicator: 7 'LWFN', 'PFB' or 'OTHER', depending on the format of the file pointed 8 to by 'path'. 9 Raises an error when the file does not contain valid Type 1 data. 10 11write(path, data, kind='OTHER', dohex=False) 12 writes raw Type 1 data to the file pointed to by 'path'. 13 'kind' can be one of 'LWFN', 'PFB' or 'OTHER'; it defaults to 'OTHER'. 14 'dohex' is a flag which determines whether the eexec encrypted 15 part should be written as hexadecimal or binary, but only if kind 16 is 'OTHER'. 17""" 18import fontTools 19from fontTools.misc import eexec 20from fontTools.misc.macCreatorType import getMacCreatorAndType 21from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes 22from fontTools.misc.psOperators import _type1_pre_eexec_order, _type1_fontinfo_order, _type1_post_eexec_order 23from fontTools.encodings.StandardEncoding import StandardEncoding 24import os 25import re 26 27__author__ = "jvr" 28__version__ = "1.0b3" 29DEBUG = 0 30 31 32try: 33 try: 34 from Carbon import Res 35 except ImportError: 36 import Res # MacPython < 2.2 37except ImportError: 38 haveMacSupport = 0 39else: 40 haveMacSupport = 1 41 42 43class T1Error(Exception): pass 44 45 46class T1Font(object): 47 48 """Type 1 font class. 49 50 Uses a minimal interpeter that supports just about enough PS to parse 51 Type 1 fonts. 52 """ 53 54 def __init__(self, path, encoding="ascii", kind=None): 55 if kind is None: 56 self.data, _ = read(path) 57 elif kind == "LWFN": 58 self.data = readLWFN(path) 59 elif kind == "PFB": 60 self.data = readPFB(path) 61 elif kind == "OTHER": 62 self.data = readOther(path) 63 else: 64 raise ValueError(kind) 65 self.encoding = encoding 66 67 def saveAs(self, path, type, dohex=False): 68 write(path, self.getData(), type, dohex) 69 70 def getData(self): 71 if not hasattr(self, "data"): 72 self.data = self.createData() 73 return self.data 74 75 def getGlyphSet(self): 76 """Return a generic GlyphSet, which is a dict-like object 77 mapping glyph names to glyph objects. The returned glyph objects 78 have a .draw() method that supports the Pen protocol, and will 79 have an attribute named 'width', but only *after* the .draw() method 80 has been called. 81 82 In the case of Type 1, the GlyphSet is simply the CharStrings dict. 83 """ 84 return self["CharStrings"] 85 86 def __getitem__(self, key): 87 if not hasattr(self, "font"): 88 self.parse() 89 return self.font[key] 90 91 def parse(self): 92 from fontTools.misc import psLib 93 from fontTools.misc import psCharStrings 94 self.font = psLib.suckfont(self.data, self.encoding) 95 charStrings = self.font["CharStrings"] 96 lenIV = self.font["Private"].get("lenIV", 4) 97 assert lenIV >= 0 98 subrs = self.font["Private"]["Subrs"] 99 for glyphName, charString in charStrings.items(): 100 charString, R = eexec.decrypt(charString, 4330) 101 charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:], 102 subrs=subrs) 103 for i in range(len(subrs)): 104 charString, R = eexec.decrypt(subrs[i], 4330) 105 subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs) 106 del self.data 107 108 def createData(self): 109 sf = self.font 110 111 eexec_began = False 112 eexec_dict = {} 113 lines = [] 114 lines.extend([self._tobytes(f"%!FontType1-1.1: {sf['FontName']}"), 115 self._tobytes(f"%t1Font: ({fontTools.version})"), 116 self._tobytes(f"%%BeginResource: font {sf['FontName']}")]) 117 # follow t1write.c:writeRegNameKeyedFont 118 size = 3 # Headroom for new key addition 119 size += 1 # FontMatrix is always counted 120 size += 1 + 1 # Private, CharStings 121 for key in font_dictionary_keys: 122 size += int(key in sf) 123 lines.append(self._tobytes(f"{size} dict dup begin")) 124 125 for key, value in sf.items(): 126 if eexec_began: 127 eexec_dict[key] = value 128 continue 129 130 if key == "FontInfo": 131 fi = sf["FontInfo"] 132 # follow t1write.c:writeFontInfoDict 133 size = 3 # Headroom for new key addition 134 for subkey in FontInfo_dictionary_keys: 135 size += int(subkey in fi) 136 lines.append(self._tobytes(f"/FontInfo {size} dict dup begin")) 137 138 for subkey, subvalue in fi.items(): 139 lines.extend(self._make_lines(subkey, subvalue)) 140 lines.append(b"end def") 141 elif key in _type1_post_eexec_order: # usually 'Private' 142 eexec_dict[key] = value 143 eexec_began = True 144 else: 145 lines.extend(self._make_lines(key, value)) 146 lines.append(b"end") 147 eexec_portion = self.encode_eexec(eexec_dict) 148 lines.append(bytesjoin([b"currentfile eexec ", eexec_portion])) 149 150 for _ in range(8): 151 lines.append(self._tobytes("0"*64)) 152 lines.extend([b"cleartomark", 153 b"%%EndResource", 154 b"%%EOF"]) 155 156 data = bytesjoin(lines, "\n") 157 return data 158 159 def encode_eexec(self, eexec_dict): 160 lines = [] 161 162 # '-|', '|-', '|' 163 RD_key, ND_key, NP_key = None, None, None 164 165 for key, value in eexec_dict.items(): 166 if key == "Private": 167 pr = eexec_dict["Private"] 168 # follow t1write.c:writePrivateDict 169 size = 3 # for RD, ND, NP 170 for subkey in Private_dictionary_keys: 171 size += int(subkey in pr) 172 lines.append(b"dup /Private") 173 lines.append(self._tobytes(f"{size} dict dup begin")) 174 for subkey, subvalue in pr.items(): 175 if not RD_key and subvalue == RD_value: 176 RD_key = subkey 177 elif not ND_key and subvalue == ND_value: 178 ND_key = subkey 179 elif not NP_key and subvalue == PD_value: 180 NP_key = subkey 181 182 if subkey == 'OtherSubrs': 183 # XXX: assert that no flex hint is used 184 lines.append(self._tobytes(hintothers)) 185 elif subkey == "Subrs": 186 # XXX: standard Subrs only 187 lines.append(b"/Subrs 5 array") 188 for i, subr_bin in enumerate(std_subrs): 189 encrypted_subr, R = eexec.encrypt(bytesjoin([char_IV, subr_bin]), 4330) 190 lines.append(bytesjoin([self._tobytes(f"dup {i} {len(encrypted_subr)} {RD_key} "), encrypted_subr, self._tobytes(f" {NP_key}")])) 191 lines.append(b'def') 192 193 lines.append(b"put") 194 else: 195 lines.extend(self._make_lines(subkey, subvalue)) 196 elif key == "CharStrings": 197 lines.append(b"dup /CharStrings") 198 lines.append(self._tobytes(f"{len(eexec_dict['CharStrings'])} dict dup begin")) 199 for glyph_name, char_bin in eexec_dict["CharStrings"].items(): 200 char_bin.compile() 201 encrypted_char, R = eexec.encrypt(bytesjoin([char_IV, char_bin.bytecode]), 4330) 202 lines.append(bytesjoin([self._tobytes(f"/{glyph_name} {len(encrypted_char)} {RD_key} "), encrypted_char, self._tobytes(f" {ND_key}")])) 203 lines.append(b"end put") 204 else: 205 lines.extend(self._make_lines(key, value)) 206 207 lines.extend([b"end", 208 b"dup /FontName get exch definefont pop", 209 b"mark", 210 b"currentfile closefile\n"]) 211 212 eexec_portion = bytesjoin(lines, "\n") 213 encrypted_eexec, R = eexec.encrypt(bytesjoin([eexec_IV, eexec_portion]), 55665) 214 215 return encrypted_eexec 216 217 def _make_lines(self, key, value): 218 if key == "FontName": 219 return [self._tobytes(f"/{key} /{value} def")] 220 if key in ["isFixedPitch", "ForceBold", "RndStemUp"]: 221 return [self._tobytes(f"/{key} {'true' if value else 'false'} def")] 222 elif key == "Encoding": 223 if value == StandardEncoding: 224 return [self._tobytes(f"/{key} StandardEncoding def")] 225 else: 226 # follow fontTools.misc.psOperators._type1_Encoding_repr 227 lines = [] 228 lines.append(b"/Encoding 256 array") 229 lines.append(b"0 1 255 {1 index exch /.notdef put} for") 230 for i in range(256): 231 name = value[i] 232 if name != ".notdef": 233 lines.append(self._tobytes(f"dup {i} /{name} put")) 234 lines.append(b"def") 235 return lines 236 if isinstance(value, str): 237 return [self._tobytes(f"/{key} ({value}) def")] 238 elif isinstance(value, bool): 239 return [self._tobytes(f"/{key} {'true' if value else 'false'} def")] 240 elif isinstance(value, list): 241 return [self._tobytes(f"/{key} [{' '.join(str(v) for v in value)}] def")] 242 elif isinstance(value, tuple): 243 return [self._tobytes(f"/{key} {{{' '.join(str(v) for v in value)}}} def")] 244 else: 245 return [self._tobytes(f"/{key} {value} def")] 246 247 def _tobytes(self, s, errors="strict"): 248 return tobytes(s, self.encoding, errors) 249 250 251# low level T1 data read and write functions 252 253def read(path, onlyHeader=False): 254 """reads any Type 1 font file, returns raw data""" 255 _, ext = os.path.splitext(path) 256 ext = ext.lower() 257 creator, typ = getMacCreatorAndType(path) 258 if typ == 'LWFN': 259 return readLWFN(path, onlyHeader), 'LWFN' 260 if ext == '.pfb': 261 return readPFB(path, onlyHeader), 'PFB' 262 else: 263 return readOther(path), 'OTHER' 264 265def write(path, data, kind='OTHER', dohex=False): 266 assertType1(data) 267 kind = kind.upper() 268 try: 269 os.remove(path) 270 except os.error: 271 pass 272 err = 1 273 try: 274 if kind == 'LWFN': 275 writeLWFN(path, data) 276 elif kind == 'PFB': 277 writePFB(path, data) 278 else: 279 writeOther(path, data, dohex) 280 err = 0 281 finally: 282 if err and not DEBUG: 283 try: 284 os.remove(path) 285 except os.error: 286 pass 287 288 289# -- internal -- 290 291LWFNCHUNKSIZE = 2000 292HEXLINELENGTH = 80 293 294 295def readLWFN(path, onlyHeader=False): 296 """reads an LWFN font file, returns raw data""" 297 from fontTools.misc.macRes import ResourceReader 298 reader = ResourceReader(path) 299 try: 300 data = [] 301 for res in reader.get('POST', []): 302 code = byteord(res.data[0]) 303 if byteord(res.data[1]) != 0: 304 raise T1Error('corrupt LWFN file') 305 if code in [1, 2]: 306 if onlyHeader and code == 2: 307 break 308 data.append(res.data[2:]) 309 elif code in [3, 5]: 310 break 311 elif code == 4: 312 with open(path, "rb") as f: 313 data.append(f.read()) 314 elif code == 0: 315 pass # comment, ignore 316 else: 317 raise T1Error('bad chunk code: ' + repr(code)) 318 finally: 319 reader.close() 320 data = bytesjoin(data) 321 assertType1(data) 322 return data 323 324def readPFB(path, onlyHeader=False): 325 """reads a PFB font file, returns raw data""" 326 data = [] 327 with open(path, "rb") as f: 328 while True: 329 if f.read(1) != bytechr(128): 330 raise T1Error('corrupt PFB file') 331 code = byteord(f.read(1)) 332 if code in [1, 2]: 333 chunklen = stringToLong(f.read(4)) 334 chunk = f.read(chunklen) 335 assert len(chunk) == chunklen 336 data.append(chunk) 337 elif code == 3: 338 break 339 else: 340 raise T1Error('bad chunk code: ' + repr(code)) 341 if onlyHeader: 342 break 343 data = bytesjoin(data) 344 assertType1(data) 345 return data 346 347def readOther(path): 348 """reads any (font) file, returns raw data""" 349 with open(path, "rb") as f: 350 data = f.read() 351 assertType1(data) 352 chunks = findEncryptedChunks(data) 353 data = [] 354 for isEncrypted, chunk in chunks: 355 if isEncrypted and isHex(chunk[:4]): 356 data.append(deHexString(chunk)) 357 else: 358 data.append(chunk) 359 return bytesjoin(data) 360 361# file writing tools 362 363def writeLWFN(path, data): 364 # Res.FSpCreateResFile was deprecated in OS X 10.5 365 Res.FSpCreateResFile(path, "just", "LWFN", 0) 366 resRef = Res.FSOpenResFile(path, 2) # write-only 367 try: 368 Res.UseResFile(resRef) 369 resID = 501 370 chunks = findEncryptedChunks(data) 371 for isEncrypted, chunk in chunks: 372 if isEncrypted: 373 code = 2 374 else: 375 code = 1 376 while chunk: 377 res = Res.Resource(bytechr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2]) 378 res.AddResource('POST', resID, '') 379 chunk = chunk[LWFNCHUNKSIZE - 2:] 380 resID = resID + 1 381 res = Res.Resource(bytechr(5) + '\0') 382 res.AddResource('POST', resID, '') 383 finally: 384 Res.CloseResFile(resRef) 385 386def writePFB(path, data): 387 chunks = findEncryptedChunks(data) 388 with open(path, "wb") as f: 389 for isEncrypted, chunk in chunks: 390 if isEncrypted: 391 code = 2 392 else: 393 code = 1 394 f.write(bytechr(128) + bytechr(code)) 395 f.write(longToString(len(chunk))) 396 f.write(chunk) 397 f.write(bytechr(128) + bytechr(3)) 398 399def writeOther(path, data, dohex=False): 400 chunks = findEncryptedChunks(data) 401 with open(path, "wb") as f: 402 hexlinelen = HEXLINELENGTH // 2 403 for isEncrypted, chunk in chunks: 404 if isEncrypted: 405 code = 2 406 else: 407 code = 1 408 if code == 2 and dohex: 409 while chunk: 410 f.write(eexec.hexString(chunk[:hexlinelen])) 411 f.write(b'\r') 412 chunk = chunk[hexlinelen:] 413 else: 414 f.write(chunk) 415 416 417# decryption tools 418 419EEXECBEGIN = b"currentfile eexec" 420# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to 421# follow eexec 422EEXECEND = re.compile(b'(0[ \t\r\n]*){512}', flags=re.M) 423EEXECINTERNALEND = b"currentfile closefile" 424EEXECBEGINMARKER = b"%-- eexec start\r" 425EEXECENDMARKER = b"%-- eexec end\r" 426 427_ishexRE = re.compile(b'[0-9A-Fa-f]*$') 428 429def isHex(text): 430 return _ishexRE.match(text) is not None 431 432 433def decryptType1(data): 434 chunks = findEncryptedChunks(data) 435 data = [] 436 for isEncrypted, chunk in chunks: 437 if isEncrypted: 438 if isHex(chunk[:4]): 439 chunk = deHexString(chunk) 440 decrypted, R = eexec.decrypt(chunk, 55665) 441 decrypted = decrypted[4:] 442 if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \ 443 and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND: 444 raise T1Error("invalid end of eexec part") 445 decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r' 446 data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER) 447 else: 448 if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN: 449 data.append(chunk[:-len(EEXECBEGIN)-1]) 450 else: 451 data.append(chunk) 452 return bytesjoin(data) 453 454def findEncryptedChunks(data): 455 chunks = [] 456 while True: 457 eBegin = data.find(EEXECBEGIN) 458 if eBegin < 0: 459 break 460 eBegin = eBegin + len(EEXECBEGIN) + 1 461 endMatch = EEXECEND.search(data, eBegin) 462 if endMatch is None: 463 raise T1Error("can't find end of eexec part") 464 eEnd = endMatch.start() 465 cypherText = data[eBegin:eEnd + 2] 466 if isHex(cypherText[:4]): 467 cypherText = deHexString(cypherText) 468 plainText, R = eexec.decrypt(cypherText, 55665) 469 eEndLocal = plainText.find(EEXECINTERNALEND) 470 if eEndLocal < 0: 471 raise T1Error("can't find end of eexec part") 472 chunks.append((0, data[:eBegin])) 473 chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1])) 474 data = data[eEnd:] 475 chunks.append((0, data)) 476 return chunks 477 478def deHexString(hexstring): 479 return eexec.deHexString(bytesjoin(hexstring.split())) 480 481 482# Type 1 assertion 483 484_fontType1RE = re.compile(br"/FontType\s+1\s+def") 485 486def assertType1(data): 487 for head in [b'%!PS-AdobeFont', b'%!FontType1']: 488 if data[:len(head)] == head: 489 break 490 else: 491 raise T1Error("not a PostScript font") 492 if not _fontType1RE.search(data): 493 raise T1Error("not a Type 1 font") 494 if data.find(b"currentfile eexec") < 0: 495 raise T1Error("not an encrypted Type 1 font") 496 # XXX what else? 497 return data 498 499 500# pfb helpers 501 502def longToString(long): 503 s = b"" 504 for i in range(4): 505 s += bytechr((long & (0xff << (i * 8))) >> i * 8) 506 return s 507 508def stringToLong(s): 509 if len(s) != 4: 510 raise ValueError('string must be 4 bytes long') 511 l = 0 512 for i in range(4): 513 l += byteord(s[i]) << (i * 8) 514 return l 515 516 517# PS stream helpers 518 519font_dictionary_keys = list(_type1_pre_eexec_order) 520# t1write.c:writeRegNameKeyedFont 521# always counts following keys 522font_dictionary_keys.remove("FontMatrix") 523 524FontInfo_dictionary_keys = list(_type1_fontinfo_order) 525# extend because AFDKO tx may use following keys 526FontInfo_dictionary_keys.extend([ 527 "FSType", 528 "Copyright", 529]) 530 531Private_dictionary_keys = [ 532 # We don't know what names will be actually used. 533 # "RD", 534 # "ND", 535 # "NP", 536 "Subrs", 537 "OtherSubrs", 538 "UniqueID", 539 "BlueValues", 540 "OtherBlues", 541 "FamilyBlues", 542 "FamilyOtherBlues", 543 "BlueScale", 544 "BlueShift", 545 "BlueFuzz", 546 "StdHW", 547 "StdVW", 548 "StemSnapH", 549 "StemSnapV", 550 "ForceBold", 551 "LanguageGroup", 552 "password", 553 "lenIV", 554 "MinFeature", 555 "RndStemUp", 556] 557 558# t1write_hintothers.h 559hintothers = """/OtherSubrs[{}{}{}{systemdict/internaldict known not{pop 3}{1183615869 560systemdict/internaldict get exec dup/startlock known{/startlock get exec}{dup 561/strtlck known{/strtlck get exec}{pop 3}ifelse}ifelse}ifelse}executeonly]def""" 562# t1write.c:saveStdSubrs 563std_subrs = [ 564 # 3 0 callother pop pop setcurrentpoint return 565 b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b", 566 # 0 1 callother return 567 b"\x8b\x8c\x0c\x10\x0b", 568 # 0 2 callother return 569 b"\x8b\x8d\x0c\x10\x0b", 570 # return 571 b"\x0b", 572 # 3 1 3 callother pop callsubr return 573 b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b" 574] 575# follow t1write.c:writeRegNameKeyedFont 576eexec_IV = b"cccc" 577char_IV = b"\x0c\x0c\x0c\x0c" 578RD_value = ("string", "currentfile", "exch", "readstring", "pop") 579ND_value = ("def",) 580PD_value = ("put",) 581