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