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" 276# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to 277# follow eexec 278EEXECEND = re.compile(b'(0[ \t\r\n]*){512}', flags=re.M) 279EEXECINTERNALEND = b"currentfile closefile" 280EEXECBEGINMARKER = b"%-- eexec start\r" 281EEXECENDMARKER = b"%-- eexec end\r" 282 283_ishexRE = re.compile(b'[0-9A-Fa-f]*$') 284 285def isHex(text): 286 return _ishexRE.match(text) is not None 287 288 289def decryptType1(data): 290 chunks = findEncryptedChunks(data) 291 data = [] 292 for isEncrypted, chunk in chunks: 293 if isEncrypted: 294 if isHex(chunk[:4]): 295 chunk = deHexString(chunk) 296 decrypted, R = eexec.decrypt(chunk, 55665) 297 decrypted = decrypted[4:] 298 if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \ 299 and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND: 300 raise T1Error("invalid end of eexec part") 301 decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r' 302 data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER) 303 else: 304 if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN: 305 data.append(chunk[:-len(EEXECBEGIN)-1]) 306 else: 307 data.append(chunk) 308 return bytesjoin(data) 309 310def findEncryptedChunks(data): 311 chunks = [] 312 while True: 313 eBegin = data.find(EEXECBEGIN) 314 if eBegin < 0: 315 break 316 eBegin = eBegin + len(EEXECBEGIN) + 1 317 endMatch = EEXECEND.search(data, eBegin) 318 if endMatch is None: 319 raise T1Error("can't find end of eexec part") 320 eEnd = endMatch.start() 321 cypherText = data[eBegin:eEnd + 2] 322 if isHex(cypherText[:4]): 323 cypherText = deHexString(cypherText) 324 plainText, R = eexec.decrypt(cypherText, 55665) 325 eEndLocal = plainText.find(EEXECINTERNALEND) 326 if eEndLocal < 0: 327 raise T1Error("can't find end of eexec part") 328 chunks.append((0, data[:eBegin])) 329 chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1])) 330 data = data[eEnd:] 331 chunks.append((0, data)) 332 return chunks 333 334def deHexString(hexstring): 335 return eexec.deHexString(bytesjoin(hexstring.split())) 336 337 338# Type 1 assertion 339 340_fontType1RE = re.compile(br"/FontType\s+1\s+def") 341 342def assertType1(data): 343 for head in [b'%!PS-AdobeFont', b'%!FontType1']: 344 if data[:len(head)] == head: 345 break 346 else: 347 raise T1Error("not a PostScript font") 348 if not _fontType1RE.search(data): 349 raise T1Error("not a Type 1 font") 350 if data.find(b"currentfile eexec") < 0: 351 raise T1Error("not an encrypted Type 1 font") 352 # XXX what else? 353 return data 354 355 356# pfb helpers 357 358def longToString(long): 359 s = b"" 360 for i in range(4): 361 s += bytechr((long & (0xff << (i * 8))) >> i * 8) 362 return s 363 364def stringToLong(s): 365 if len(s) != 4: 366 raise ValueError('string must be 4 bytes long') 367 l = 0 368 for i in range(4): 369 l += byteord(s[i]) << (i * 8) 370 return l 371