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