blob: 40e67f6f65d1ec64f2fa49da7eeba76ec8fec675 [file] [log] [blame]
Just7842e561999-12-16 21:34:53 +00001"""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
Just5810aa92001-06-24 15:12:48 +000011write(path, data, kind='OTHER', dohex=0)
Just7842e561999-12-16 21:34:53 +000012 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
Justc2be3d92000-01-12 19:15:57 +000023from fontTools.misc import eexec
Just7842e561999-12-16 21:34:53 +000024import string
25import re
26import os
27
28if os.name == 'mac':
jvr25ccb9c2002-05-03 19:38:07 +000029 try:
30 from Carbon import Res
31 except ImportError:
32 import Res # MacPython < 2.2
Just7842e561999-12-16 21:34:53 +000033 import macfs
34
35error = 't1Lib.error'
36
Just7842e561999-12-16 21:34:53 +000037
38class T1Font:
39
Just36183002000-03-28 10:38:43 +000040 """Type 1 font class.
41
42 Uses a minimal interpeter that supports just about enough PS to parse
43 Type 1 fonts.
Just7842e561999-12-16 21:34:53 +000044 """
45
46 def __init__(self, path=None):
47 if path is not None:
48 self.data, type = read(path)
49 else:
50 pass # XXX
51
52 def saveAs(self, path, type):
jvr8c74f462001-08-16 10:34:22 +000053 write(path, self.getData(), type)
Just7842e561999-12-16 21:34:53 +000054
55 def getData(self):
Just36183002000-03-28 10:38:43 +000056 # XXX Todo: if the data has been converted to Python object,
57 # recreate the PS stream
Just7842e561999-12-16 21:34:53 +000058 return self.data
59
60 def __getitem__(self, key):
61 if not hasattr(self, "font"):
62 self.parse()
Just36183002000-03-28 10:38:43 +000063 return self.font[key]
Just7842e561999-12-16 21:34:53 +000064
65 def parse(self):
Just528614e2000-01-16 22:14:02 +000066 from fontTools.misc import psLib
67 from fontTools.misc import psCharStrings
Just7842e561999-12-16 21:34:53 +000068 self.font = psLib.suckfont(self.data)
69 charStrings = self.font["CharStrings"]
70 lenIV = self.font["Private"].get("lenIV", 4)
71 assert lenIV >= 0
72 for glyphName, charString in charStrings.items():
Justc2be3d92000-01-12 19:15:57 +000073 charString, R = eexec.decrypt(charString, 4330)
Just7842e561999-12-16 21:34:53 +000074 charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:])
75 subrs = self.font["Private"]["Subrs"]
76 for i in range(len(subrs)):
Justc2be3d92000-01-12 19:15:57 +000077 charString, R = eexec.decrypt(subrs[i], 4330)
Just7842e561999-12-16 21:34:53 +000078 subrs[i] = psCharStrings.T1CharString(charString[lenIV:])
79 del self.data
80
81
82
Just36183002000-03-28 10:38:43 +000083# low level T1 data read and write functions
Just7842e561999-12-16 21:34:53 +000084
85def read(path):
86 """reads any Type 1 font file, returns raw data"""
87 normpath = string.lower(path)
88 if os.name == 'mac':
89 fss = macfs.FSSpec(path)
90 creator, type = fss.GetCreatorType()
91 if type == 'LWFN':
Just5810aa92001-06-24 15:12:48 +000092 return readLWFN(path), 'LWFN'
Just7842e561999-12-16 21:34:53 +000093 if normpath[-4:] == '.pfb':
Just5810aa92001-06-24 15:12:48 +000094 return readPFB(path), 'PFB'
Just7842e561999-12-16 21:34:53 +000095 else:
Just5810aa92001-06-24 15:12:48 +000096 return readOther(path), 'OTHER'
Just7842e561999-12-16 21:34:53 +000097
98def write(path, data, kind='OTHER', dohex=0):
Just5810aa92001-06-24 15:12:48 +000099 assertType1(data)
Just7842e561999-12-16 21:34:53 +0000100 kind = string.upper(kind)
101 try:
102 os.remove(path)
103 except os.error:
104 pass
105 err = 1
106 try:
107 if kind == 'LWFN':
Just5810aa92001-06-24 15:12:48 +0000108 writeLWFN(path, data)
Just7842e561999-12-16 21:34:53 +0000109 elif kind == 'PFB':
Just5810aa92001-06-24 15:12:48 +0000110 writePFB(path, data)
Just7842e561999-12-16 21:34:53 +0000111 else:
Just5810aa92001-06-24 15:12:48 +0000112 writeOther(path, data, dohex)
Just7842e561999-12-16 21:34:53 +0000113 err = 0
114 finally:
115 if err and not DEBUG:
116 try:
117 os.remove(path)
118 except os.error:
119 pass
120
121
122# -- internal --
123
124LWFNCHUNKSIZE = 2000
125HEXLINELENGTH = 80
126
127
Just5810aa92001-06-24 15:12:48 +0000128def readLWFN(path):
Just7842e561999-12-16 21:34:53 +0000129 """reads an LWFN font file, returns raw data"""
Juste9601bf2001-07-30 19:04:40 +0000130 resRef = Res.FSpOpenResFile(path, 1) # read-only
Just7842e561999-12-16 21:34:53 +0000131 try:
Juste9601bf2001-07-30 19:04:40 +0000132 Res.UseResFile(resRef)
Just7842e561999-12-16 21:34:53 +0000133 n = Res.Count1Resources('POST')
134 data = []
135 for i in range(501, 501 + n):
136 res = Res.Get1Resource('POST', i)
137 code = ord(res.data[0])
138 if ord(res.data[1]) <> 0:
139 raise error, 'corrupt LWFN file'
140 if code in [1, 2]:
141 data.append(res.data[2:])
142 elif code in [3, 5]:
143 break
144 elif code == 4:
145 f = open(path, "rb")
146 data.append(f.read())
147 f.close()
148 elif code == 0:
149 pass # comment, ignore
150 else:
151 raise error, 'bad chunk code: ' + `code`
152 finally:
Juste9601bf2001-07-30 19:04:40 +0000153 Res.CloseResFile(resRef)
Just7842e561999-12-16 21:34:53 +0000154 data = string.join(data, '')
Just5810aa92001-06-24 15:12:48 +0000155 assertType1(data)
Just7842e561999-12-16 21:34:53 +0000156 return data
157
Just5810aa92001-06-24 15:12:48 +0000158def readPFB(path, onlyHeader=0):
Just7842e561999-12-16 21:34:53 +0000159 """reads a PFB font file, returns raw data"""
160 f = open(path, "rb")
161 data = []
162 while 1:
163 if f.read(1) <> chr(128):
164 raise error, 'corrupt PFB file'
165 code = ord(f.read(1))
166 if code in [1, 2]:
Just5810aa92001-06-24 15:12:48 +0000167 chunklen = stringToLong(f.read(4))
Just36183002000-03-28 10:38:43 +0000168 chunk = f.read(chunklen)
169 assert len(chunk) == chunklen
170 data.append(chunk)
Just7842e561999-12-16 21:34:53 +0000171 elif code == 3:
172 break
173 else:
174 raise error, 'bad chunk code: ' + `code`
Just5810aa92001-06-24 15:12:48 +0000175 if onlyHeader:
176 break
Just7842e561999-12-16 21:34:53 +0000177 f.close()
178 data = string.join(data, '')
Just5810aa92001-06-24 15:12:48 +0000179 assertType1(data)
Just7842e561999-12-16 21:34:53 +0000180 return data
181
Just5810aa92001-06-24 15:12:48 +0000182def readOther(path):
Just7842e561999-12-16 21:34:53 +0000183 """reads any (font) file, returns raw data"""
184 f = open(path, "rb")
185 data = f.read()
186 f.close()
Just5810aa92001-06-24 15:12:48 +0000187 assertType1(data)
Just7842e561999-12-16 21:34:53 +0000188
Just5810aa92001-06-24 15:12:48 +0000189 chunks = findEncryptedChunks(data)
Just7842e561999-12-16 21:34:53 +0000190 data = []
Just5810aa92001-06-24 15:12:48 +0000191 for isEncrypted, chunk in chunks:
192 if isEncrypted and isHex(chunk[:4]):
193 data.append(deHexString(chunk))
Just7842e561999-12-16 21:34:53 +0000194 else:
195 data.append(chunk)
196 return string.join(data, '')
197
198# file writing tools
199
Just5810aa92001-06-24 15:12:48 +0000200def writeLWFN(path, data):
Juste9601bf2001-07-30 19:04:40 +0000201 Res.FSpCreateResFile(path, "just", "LWFN", 0)
202 resRef = Res.FSpOpenResFile(path, 2) # write-only
Just7842e561999-12-16 21:34:53 +0000203 try:
Juste9601bf2001-07-30 19:04:40 +0000204 Res.UseResFile(resRef)
Just7842e561999-12-16 21:34:53 +0000205 resID = 501
Just5810aa92001-06-24 15:12:48 +0000206 chunks = findEncryptedChunks(data)
207 for isEncrypted, chunk in chunks:
208 if isEncrypted:
Just7842e561999-12-16 21:34:53 +0000209 code = 2
210 else:
211 code = 1
212 while chunk:
213 res = Res.Resource(chr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
214 res.AddResource('POST', resID, '')
215 chunk = chunk[LWFNCHUNKSIZE - 2:]
216 resID = resID + 1
217 res = Res.Resource(chr(5) + '\0')
218 res.AddResource('POST', resID, '')
219 finally:
Juste9601bf2001-07-30 19:04:40 +0000220 Res.CloseResFile(resRef)
Just7842e561999-12-16 21:34:53 +0000221
Just5810aa92001-06-24 15:12:48 +0000222def writePFB(path, data):
223 chunks = findEncryptedChunks(data)
Just36183002000-03-28 10:38:43 +0000224 f = open(path, "wb")
Just7842e561999-12-16 21:34:53 +0000225 try:
Just5810aa92001-06-24 15:12:48 +0000226 for isEncrypted, chunk in chunks:
227 if isEncrypted:
Just7842e561999-12-16 21:34:53 +0000228 code = 2
229 else:
230 code = 1
231 f.write(chr(128) + chr(code))
Just5810aa92001-06-24 15:12:48 +0000232 f.write(longToString(len(chunk)))
Just7842e561999-12-16 21:34:53 +0000233 f.write(chunk)
234 f.write(chr(128) + chr(3))
235 finally:
236 f.close()
237 if os.name == 'mac':
Just36183002000-03-28 10:38:43 +0000238 fss = macfs.FSSpec(path)
Just7842e561999-12-16 21:34:53 +0000239 fss.SetCreatorType('mdos', 'BINA')
240
Just5810aa92001-06-24 15:12:48 +0000241def writeOther(path, data, dohex = 0):
242 chunks = findEncryptedChunks(data)
Just7842e561999-12-16 21:34:53 +0000243 f = open(path, "wb")
244 try:
245 hexlinelen = HEXLINELENGTH / 2
Just5810aa92001-06-24 15:12:48 +0000246 for isEncrypted, chunk in chunks:
247 if isEncrypted:
Just7842e561999-12-16 21:34:53 +0000248 code = 2
249 else:
250 code = 1
251 if code == 2 and dohex:
252 while chunk:
Justc2be3d92000-01-12 19:15:57 +0000253 f.write(eexec.hexString(chunk[:hexlinelen]))
Just7842e561999-12-16 21:34:53 +0000254 f.write('\r')
255 chunk = chunk[hexlinelen:]
256 else:
257 f.write(chunk)
258 finally:
259 f.close()
260 if os.name == 'mac':
261 fss = macfs.FSSpec(path)
262 fss.SetCreatorType('R*ch', 'TEXT') # BBEdit text file
263
264
265# decryption tools
266
267EEXECBEGIN = "currentfile eexec"
268EEXECEND = '0' * 64
269EEXECINTERNALEND = "currentfile closefile"
270EEXECBEGINMARKER = "%-- eexec start\r"
271EEXECENDMARKER = "%-- eexec end\r"
272
273_ishexRE = re.compile('[0-9A-Fa-f]*$')
274
Just5810aa92001-06-24 15:12:48 +0000275def isHex(text):
Just7842e561999-12-16 21:34:53 +0000276 return _ishexRE.match(text) is not None
277
278
Just5810aa92001-06-24 15:12:48 +0000279def decryptType1(data):
280 chunks = findEncryptedChunks(data)
Just7842e561999-12-16 21:34:53 +0000281 data = []
Just5810aa92001-06-24 15:12:48 +0000282 for isEncrypted, chunk in chunks:
283 if isEncrypted:
284 if isHex(chunk[:4]):
285 chunk = deHexString(chunk)
Justc2be3d92000-01-12 19:15:57 +0000286 decrypted, R = eexec.decrypt(chunk, 55665)
Just7842e561999-12-16 21:34:53 +0000287 decrypted = decrypted[4:]
288 if decrypted[-len(EEXECINTERNALEND)-1:-1] <> EEXECINTERNALEND \
289 and decrypted[-len(EEXECINTERNALEND)-2:-2] <> EEXECINTERNALEND:
290 raise error, "invalid end of eexec part"
291 decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r'
292 data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
293 else:
294 if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
295 data.append(chunk[:-len(EEXECBEGIN)-1])
296 else:
297 data.append(chunk)
298 return string.join(data, '')
299
Just5810aa92001-06-24 15:12:48 +0000300def findEncryptedChunks(data):
Just7842e561999-12-16 21:34:53 +0000301 chunks = []
302 while 1:
Just5810aa92001-06-24 15:12:48 +0000303 eBegin = string.find(data, EEXECBEGIN)
304 if eBegin < 0:
Just7842e561999-12-16 21:34:53 +0000305 break
jvr90290b72001-11-28 12:22:15 +0000306 eBegin = eBegin + len(EEXECBEGIN) + 1
Just5810aa92001-06-24 15:12:48 +0000307 eEnd = string.find(data, EEXECEND, eBegin)
308 if eEnd < 0:
Just7842e561999-12-16 21:34:53 +0000309 raise error, "can't find end of eexec part"
jvr90290b72001-11-28 12:22:15 +0000310 cypherText = data[eBegin:eEnd + 2]
311 plainText, R = eexec.decrypt(cypherText, 55665)
312 eEndLocal = string.find(plainText, EEXECINTERNALEND)
313 if eEndLocal < 0:
314 raise error, "can't find end of eexec part"
315 eEnd = eBegin + eEndLocal + len(EEXECINTERNALEND) + 1
316 chunks.append((0, data[:eBegin]))
317 chunks.append((1, data[eBegin:eEnd]))
Just5810aa92001-06-24 15:12:48 +0000318 data = data[eEnd:]
Just7842e561999-12-16 21:34:53 +0000319 chunks.append((0, data))
320 return chunks
321
Just5810aa92001-06-24 15:12:48 +0000322def deHexString(hexstring):
Justc2be3d92000-01-12 19:15:57 +0000323 return eexec.deHexString(string.join(string.split(hexstring), ""))
Just7842e561999-12-16 21:34:53 +0000324
325
326# Type 1 assertion
327
328_fontType1RE = re.compile(r"/FontType\s+1\s+def")
329
Just5810aa92001-06-24 15:12:48 +0000330def assertType1(data):
Just7842e561999-12-16 21:34:53 +0000331 for head in ['%!PS-AdobeFont', '%!FontType1-1.0']:
332 if data[:len(head)] == head:
333 break
334 else:
335 raise error, "not a PostScript font"
336 if not _fontType1RE.search(data):
337 raise error, "not a Type 1 font"
338 if string.find(data, "currentfile eexec") < 0:
339 raise error, "not an encrypted Type 1 font"
340 # XXX what else?
341 return data
342
343
344# pfb helpers
345
Just5810aa92001-06-24 15:12:48 +0000346def longToString(long):
Just7842e561999-12-16 21:34:53 +0000347 str = ""
348 for i in range(4):
349 str = str + chr((long & (0xff << (i * 8))) >> i * 8)
350 return str
351
Just5810aa92001-06-24 15:12:48 +0000352def stringToLong(str):
Just7842e561999-12-16 21:34:53 +0000353 if len(str) <> 4:
354 raise ValueError, 'string must be 4 bytes long'
355 long = 0
356 for i in range(4):
357 long = long + (ord(str[i]) << (i * 8))
358 return long
Just5810aa92001-06-24 15:12:48 +0000359