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