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