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