blob: a2c63fbb49bb3d3100fb2dc6c86c351ab4111c16 [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
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
23import eexec
24import string
25import re
26import os
27
28if os.name == 'mac':
29 import Res
30 import macfs
31
32error = 't1Lib.error'
33
34# work in progress
35
36
37class T1Font:
38
39 """Type 1 font class.
40 XXX This is work in progress! For now just use the read()
41 and write() functions as described above, they are stable.
42 """
43
44 def __init__(self, path=None):
45 if path is not None:
46 self.data, type = read(path)
47 else:
48 pass # XXX
49
50 def saveAs(self, path, type):
51 self.write(path, self.getData(), type)
52
53 def getData(self):
54 return self.data
55
56 def __getitem__(self, key):
57 if not hasattr(self, "font"):
58 self.parse()
59 return self.font[key]
60 else:
61 return self.font[key]
62
63 def parse(self):
64 import psLib
65 import psCharStrings
66 self.font = psLib.suckfont(self.data)
67 charStrings = self.font["CharStrings"]
68 lenIV = self.font["Private"].get("lenIV", 4)
69 assert lenIV >= 0
70 for glyphName, charString in charStrings.items():
71 charString, R = eexec.Decrypt(charString, 4330)
72 charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:])
73 subrs = self.font["Private"]["Subrs"]
74 for i in range(len(subrs)):
75 charString, R = eexec.Decrypt(subrs[i], 4330)
76 subrs[i] = psCharStrings.T1CharString(charString[lenIV:])
77 del self.data
78
79
80
81# public functions
82
83def read(path):
84 """reads any Type 1 font file, returns raw data"""
85 normpath = string.lower(path)
86 if os.name == 'mac':
87 fss = macfs.FSSpec(path)
88 creator, type = fss.GetCreatorType()
89 if type == 'LWFN':
90 return readlwfn(path), 'LWFN'
91 if normpath[-4:] == '.pfb':
92 return readpfb(path), 'PFB'
93 else:
94 return readother(path), 'OTHER'
95
96def write(path, data, kind='OTHER', dohex=0):
97 asserttype1(data)
98 kind = string.upper(kind)
99 try:
100 os.remove(path)
101 except os.error:
102 pass
103 err = 1
104 try:
105 if kind == 'LWFN':
106 writelwfn(path, data)
107 elif kind == 'PFB':
108 writepfb(path, data)
109 else:
110 writeother(path, data, dohex)
111 err = 0
112 finally:
113 if err and not DEBUG:
114 try:
115 os.remove(path)
116 except os.error:
117 pass
118
119
120# -- internal --
121
122LWFNCHUNKSIZE = 2000
123HEXLINELENGTH = 80
124
125
126def readlwfn(path):
127 """reads an LWFN font file, returns raw data"""
128 resref = Res.OpenResFile(path)
129 try:
130 Res.UseResFile(resref)
131 n = Res.Count1Resources('POST')
132 data = []
133 for i in range(501, 501 + n):
134 res = Res.Get1Resource('POST', i)
135 code = ord(res.data[0])
136 if ord(res.data[1]) <> 0:
137 raise error, 'corrupt LWFN file'
138 if code in [1, 2]:
139 data.append(res.data[2:])
140 elif code in [3, 5]:
141 break
142 elif code == 4:
143 f = open(path, "rb")
144 data.append(f.read())
145 f.close()
146 elif code == 0:
147 pass # comment, ignore
148 else:
149 raise error, 'bad chunk code: ' + `code`
150 finally:
151 Res.CloseResFile(resref)
152 data = string.join(data, '')
153 asserttype1(data)
154 return data
155
156def readpfb(path):
157 """reads a PFB font file, returns raw data"""
158 f = open(path, "rb")
159 data = []
160 while 1:
161 if f.read(1) <> chr(128):
162 raise error, 'corrupt PFB file'
163 code = ord(f.read(1))
164 if code in [1, 2]:
165 chunklen = string2long(f.read(4))
166 data.append(f.read(chunklen))
167 elif code == 3:
168 break
169 else:
170 raise error, 'bad chunk code: ' + `code`
171 f.close()
172 data = string.join(data, '')
173 asserttype1(data)
174 return data
175
176def readother(path):
177 """reads any (font) file, returns raw data"""
178 f = open(path, "rb")
179 data = f.read()
180 f.close()
181 asserttype1(data)
182
183 chunks = findencryptedchunks(data)
184 data = []
185 for isencrypted, chunk in chunks:
186 if isencrypted and ishex(chunk[:4]):
187 data.append(dehexstring(chunk))
188 else:
189 data.append(chunk)
190 return string.join(data, '')
191
192# file writing tools
193
194def writelwfn(path, data):
195 Res.CreateResFile(path)
196 fss = macfs.FSSpec(path)
197 fss.SetCreatorType('just', 'LWFN')
198 resref = Res.OpenResFile(path)
199 try:
200 Res.UseResFile(resref)
201 resID = 501
202 chunks = findencryptedchunks(data)
203 for isencrypted, chunk in chunks:
204 if isencrypted:
205 code = 2
206 else:
207 code = 1
208 while chunk:
209 res = Res.Resource(chr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
210 res.AddResource('POST', resID, '')
211 chunk = chunk[LWFNCHUNKSIZE - 2:]
212 resID = resID + 1
213 res = Res.Resource(chr(5) + '\0')
214 res.AddResource('POST', resID, '')
215 finally:
216 Res.CloseResFile(resref)
217
218def writepfb(path, data):
219 chunks = findencryptedchunks(data)
220 f = open(dstpath, "wb")
221 try:
222 for isencrypted, chunk in chunks:
223 if isencrypted:
224 code = 2
225 else:
226 code = 1
227 f.write(chr(128) + chr(code))
228 f.write(long2string(len(chunk)))
229 f.write(chunk)
230 f.write(chr(128) + chr(3))
231 finally:
232 f.close()
233 if os.name == 'mac':
234 fss = macfs.FSSpec(dstpath)
235 fss.SetCreatorType('mdos', 'BINA')
236
237def writeother(path, data, dohex = 0):
238 chunks = findencryptedchunks(data)
239 f = open(path, "wb")
240 try:
241 hexlinelen = HEXLINELENGTH / 2
242 for isencrypted, chunk in chunks:
243 if isencrypted:
244 code = 2
245 else:
246 code = 1
247 if code == 2 and dohex:
248 while chunk:
249 f.write(eexec.hexstring(chunk[:hexlinelen]))
250 f.write('\r')
251 chunk = chunk[hexlinelen:]
252 else:
253 f.write(chunk)
254 finally:
255 f.close()
256 if os.name == 'mac':
257 fss = macfs.FSSpec(path)
258 fss.SetCreatorType('R*ch', 'TEXT') # BBEdit text file
259
260
261# decryption tools
262
263EEXECBEGIN = "currentfile eexec"
264EEXECEND = '0' * 64
265EEXECINTERNALEND = "currentfile closefile"
266EEXECBEGINMARKER = "%-- eexec start\r"
267EEXECENDMARKER = "%-- eexec end\r"
268
269_ishexRE = re.compile('[0-9A-Fa-f]*$')
270
271def ishex(text):
272 return _ishexRE.match(text) is not None
273
274
275def decrypttype1(data):
276 chunks = findencryptedchunks(data)
277 data = []
278 for isencrypted, chunk in chunks:
279 if isencrypted:
280 if ishex(chunk[:4]):
281 chunk = dehexstring(chunk)
282 decrypted, R = eexec.Decrypt(chunk, 55665)
283 decrypted = decrypted[4:]
284 if decrypted[-len(EEXECINTERNALEND)-1:-1] <> EEXECINTERNALEND \
285 and decrypted[-len(EEXECINTERNALEND)-2:-2] <> EEXECINTERNALEND:
286 raise error, "invalid end of eexec part"
287 decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r'
288 data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
289 else:
290 if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
291 data.append(chunk[:-len(EEXECBEGIN)-1])
292 else:
293 data.append(chunk)
294 return string.join(data, '')
295
296def findencryptedchunks(data):
297 chunks = []
298 while 1:
299 ebegin = string.find(data, EEXECBEGIN)
300 if ebegin < 0:
301 break
302 eend = string.find(data, EEXECEND, ebegin)
303 if eend < 0:
304 raise error, "can't find end of eexec part"
305 chunks.append((0, data[:ebegin + len(EEXECBEGIN) + 1]))
306 chunks.append((1, data[ebegin + len(EEXECBEGIN) + 1:eend]))
307 data = data[eend:]
308 chunks.append((0, data))
309 return chunks
310
311def dehexstring(hexstring):
312 return eexec.dehexstring(string.join(string.split(hexstring), ""))
313
314
315# Type 1 assertion
316
317_fontType1RE = re.compile(r"/FontType\s+1\s+def")
318
319def asserttype1(data):
320 for head in ['%!PS-AdobeFont', '%!FontType1-1.0']:
321 if data[:len(head)] == head:
322 break
323 else:
324 raise error, "not a PostScript font"
325 if not _fontType1RE.search(data):
326 raise error, "not a Type 1 font"
327 if string.find(data, "currentfile eexec") < 0:
328 raise error, "not an encrypted Type 1 font"
329 # XXX what else?
330 return data
331
332
333# pfb helpers
334
335def long2string(long):
336 str = ""
337 for i in range(4):
338 str = str + chr((long & (0xff << (i * 8))) >> i * 8)
339 return str
340
341def string2long(str):
342 if len(str) <> 4:
343 raise ValueError, 'string must be 4 bytes long'
344 long = 0
345 for i in range(4):
346 long = long + (ord(str[i]) << (i * 8))
347 return long