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