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""" 18from __future__ import print_function, division, absolute_import 19from fontTools.misc.py23 import * 20from fontTools.misc import eexec 21from fontTools.misc.macCreatorType import getMacCreatorAndType 22import os 23import re 24 25__author__ = "jvr" 26__version__ = "1.0b2" 27DEBUG = 0 28 29 30try: 31 try: 32 from Carbon import Res 33 except ImportError: 34 import Res # MacPython < 2.2 35except ImportError: 36 haveMacSupport = 0 37else: 38 haveMacSupport = 1 39 40 41class T1Error(Exception): pass 42 43 44class T1Font(object): 45 46 """Type 1 font class. 47 48 Uses a minimal interpeter that supports just about enough PS to parse 49 Type 1 fonts. 50 """ 51 52 def __init__(self, path, encoding="ascii", kind=None): 53 if kind is None: 54 self.data, _ = read(path) 55 elif kind == "LWFN": 56 self.data = readLWFN(path) 57 elif kind == "PFB": 58 self.data = readPFB(path) 59 elif kind == "OTHER": 60 self.data = readOther(path) 61 else: 62 raise ValueError(kind) 63 self.encoding = encoding 64 65 def saveAs(self, path, type, dohex=False): 66 write(path, self.getData(), type, dohex) 67 68 def getData(self): 69 # XXX Todo: if the data has been converted to Python object, 70 # recreate the PS stream 71 return self.data 72 73 def getGlyphSet(self): 74 """Return a generic GlyphSet, which is a dict-like object 75 mapping glyph names to glyph objects. The returned glyph objects 76 have a .draw() method that supports the Pen protocol, and will 77 have an attribute named 'width', but only *after* the .draw() method 78 has been called. 79 80 In the case of Type 1, the GlyphSet is simply the CharStrings dict. 81 """ 82 return self["CharStrings"] 83 84 def __getitem__(self, key): 85 if not hasattr(self, "font"): 86 self.parse() 87 return self.font[key] 88 89 def parse(self): 90 from fontTools.misc import psLib 91 from fontTools.misc import psCharStrings 92 self.font = psLib.suckfont(self.data, self.encoding) 93 charStrings = self.font["CharStrings"] 94 lenIV = self.font["Private"].get("lenIV", 4) 95 assert lenIV >= 0 96 subrs = self.font["Private"]["Subrs"] 97 for glyphName, charString in charStrings.items(): 98 charString, R = eexec.decrypt(charString, 4330) 99 charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:], 100 subrs=subrs) 101 for i in range(len(subrs)): 102 charString, R = eexec.decrypt(subrs[i], 4330) 103 subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs) 104 del self.data 105 106 107# low level T1 data read and write functions 108 109def read(path, onlyHeader=False): 110 """reads any Type 1 font file, returns raw data""" 111 _, ext = os.path.splitext(path) 112 ext = ext.lower() 113 creator, typ = getMacCreatorAndType(path) 114 if typ == 'LWFN': 115 return readLWFN(path, onlyHeader), 'LWFN' 116 if ext == '.pfb': 117 return readPFB(path, onlyHeader), 'PFB' 118 else: 119 return readOther(path), 'OTHER' 120 121def write(path, data, kind='OTHER', dohex=False): 122 assertType1(data) 123 kind = kind.upper() 124 try: 125 os.remove(path) 126 except os.error: 127 pass 128 err = 1 129 try: 130 if kind == 'LWFN': 131 writeLWFN(path, data) 132 elif kind == 'PFB': 133 writePFB(path, data) 134 else: 135 writeOther(path, data, dohex) 136 err = 0 137 finally: 138 if err and not DEBUG: 139 try: 140 os.remove(path) 141 except os.error: 142 pass 143 144 145# -- internal -- 146 147LWFNCHUNKSIZE = 2000 148HEXLINELENGTH = 80 149 150 151def readLWFN(path, onlyHeader=False): 152 """reads an LWFN font file, returns raw data""" 153 from fontTools.misc.macRes import ResourceReader 154 reader = ResourceReader(path) 155 try: 156 data = [] 157 for res in reader.get('POST', []): 158 code = byteord(res.data[0]) 159 if byteord(res.data[1]) != 0: 160 raise T1Error('corrupt LWFN file') 161 if code in [1, 2]: 162 if onlyHeader and code == 2: 163 break 164 data.append(res.data[2:]) 165 elif code in [3, 5]: 166 break 167 elif code == 4: 168 with open(path, "rb") as f: 169 data.append(f.read()) 170 elif code == 0: 171 pass # comment, ignore 172 else: 173 raise T1Error('bad chunk code: ' + repr(code)) 174 finally: 175 reader.close() 176 data = bytesjoin(data) 177 assertType1(data) 178 return data 179 180def readPFB(path, onlyHeader=False): 181 """reads a PFB font file, returns raw data""" 182 data = [] 183 with open(path, "rb") as f: 184 while True: 185 if f.read(1) != bytechr(128): 186 raise T1Error('corrupt PFB file') 187 code = byteord(f.read(1)) 188 if code in [1, 2]: 189 chunklen = stringToLong(f.read(4)) 190 chunk = f.read(chunklen) 191 assert len(chunk) == chunklen 192 data.append(chunk) 193 elif code == 3: 194 break 195 else: 196 raise T1Error('bad chunk code: ' + repr(code)) 197 if onlyHeader: 198 break 199 data = bytesjoin(data) 200 assertType1(data) 201 return data 202 203def readOther(path): 204 """reads any (font) file, returns raw data""" 205 with open(path, "rb") as f: 206 data = f.read() 207 assertType1(data) 208 chunks = findEncryptedChunks(data) 209 data = [] 210 for isEncrypted, chunk in chunks: 211 if isEncrypted and isHex(chunk[:4]): 212 data.append(deHexString(chunk)) 213 else: 214 data.append(chunk) 215 return bytesjoin(data) 216 217# file writing tools 218 219def writeLWFN(path, data): 220 # Res.FSpCreateResFile was deprecated in OS X 10.5 221 Res.FSpCreateResFile(path, "just", "LWFN", 0) 222 resRef = Res.FSOpenResFile(path, 2) # write-only 223 try: 224 Res.UseResFile(resRef) 225 resID = 501 226 chunks = findEncryptedChunks(data) 227 for isEncrypted, chunk in chunks: 228 if isEncrypted: 229 code = 2 230 else: 231 code = 1 232 while chunk: 233 res = Res.Resource(bytechr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2]) 234 res.AddResource('POST', resID, '') 235 chunk = chunk[LWFNCHUNKSIZE - 2:] 236 resID = resID + 1 237 res = Res.Resource(bytechr(5) + '\0') 238 res.AddResource('POST', resID, '') 239 finally: 240 Res.CloseResFile(resRef) 241 242def writePFB(path, data): 243 chunks = findEncryptedChunks(data) 244 with open(path, "wb") as f: 245 for isEncrypted, chunk in chunks: 246 if isEncrypted: 247 code = 2 248 else: 249 code = 1 250 f.write(bytechr(128) + bytechr(code)) 251 f.write(longToString(len(chunk))) 252 f.write(chunk) 253 f.write(bytechr(128) + bytechr(3)) 254 255def writeOther(path, data, dohex=False): 256 chunks = findEncryptedChunks(data) 257 with open(path, "wb") as f: 258 hexlinelen = HEXLINELENGTH // 2 259 for isEncrypted, chunk in chunks: 260 if isEncrypted: 261 code = 2 262 else: 263 code = 1 264 if code == 2 and dohex: 265 while chunk: 266 f.write(eexec.hexString(chunk[:hexlinelen])) 267 f.write(b'\r') 268 chunk = chunk[hexlinelen:] 269 else: 270 f.write(chunk) 271 272 273# decryption tools 274 275EEXECBEGIN = b"currentfile eexec" 276EEXECEND = b'0' * 64 277EEXECINTERNALEND = b"currentfile closefile" 278EEXECBEGINMARKER = b"%-- eexec start\r" 279EEXECENDMARKER = b"%-- eexec end\r" 280 281_ishexRE = re.compile(b'[0-9A-Fa-f]*$') 282 283def isHex(text): 284 return _ishexRE.match(text) is not None 285 286 287def decryptType1(data): 288 chunks = findEncryptedChunks(data) 289 data = [] 290 for isEncrypted, chunk in chunks: 291 if isEncrypted: 292 if isHex(chunk[:4]): 293 chunk = deHexString(chunk) 294 decrypted, R = eexec.decrypt(chunk, 55665) 295 decrypted = decrypted[4:] 296 if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \ 297 and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND: 298 raise T1Error("invalid end of eexec part") 299 decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r' 300 data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER) 301 else: 302 if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN: 303 data.append(chunk[:-len(EEXECBEGIN)-1]) 304 else: 305 data.append(chunk) 306 return bytesjoin(data) 307 308def findEncryptedChunks(data): 309 chunks = [] 310 while True: 311 eBegin = data.find(EEXECBEGIN) 312 if eBegin < 0: 313 break 314 eBegin = eBegin + len(EEXECBEGIN) + 1 315 eEnd = data.find(EEXECEND, eBegin) 316 if eEnd < 0: 317 raise T1Error("can't find end of eexec part") 318 cypherText = data[eBegin:eEnd + 2] 319 if isHex(cypherText[:4]): 320 cypherText = deHexString(cypherText) 321 plainText, R = eexec.decrypt(cypherText, 55665) 322 eEndLocal = plainText.find(EEXECINTERNALEND) 323 if eEndLocal < 0: 324 raise T1Error("can't find end of eexec part") 325 chunks.append((0, data[:eBegin])) 326 chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1])) 327 data = data[eEnd:] 328 chunks.append((0, data)) 329 return chunks 330 331def deHexString(hexstring): 332 return eexec.deHexString(bytesjoin(hexstring.split())) 333 334 335# Type 1 assertion 336 337_fontType1RE = re.compile(br"/FontType\s+1\s+def") 338 339def assertType1(data): 340 for head in [b'%!PS-AdobeFont', b'%!FontType1']: 341 if data[:len(head)] == head: 342 break 343 else: 344 raise T1Error("not a PostScript font") 345 if not _fontType1RE.search(data): 346 raise T1Error("not a Type 1 font") 347 if data.find(b"currentfile eexec") < 0: 348 raise T1Error("not an encrypted Type 1 font") 349 # XXX what else? 350 return data 351 352 353# pfb helpers 354 355def longToString(long): 356 s = b"" 357 for i in range(4): 358 s += bytechr((long & (0xff << (i * 8))) >> i * 8) 359 return s 360 361def stringToLong(s): 362 if len(s) != 4: 363 raise ValueError('string must be 4 bytes long') 364 l = 0 365 for i in range(4): 366 l += byteord(s[i]) << (i * 8) 367 return l 368