blob: cd3a464b85afe59c2070a527851fa7c7688bae37 [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':
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':
Just5810aa92001-06-24 15:12:48 +000089 return readLWFN(path), 'LWFN'
Just7842e561999-12-16 21:34:53 +000090 if normpath[-4:] == '.pfb':
Just5810aa92001-06-24 15:12:48 +000091 return readPFB(path), 'PFB'
Just7842e561999-12-16 21:34:53 +000092 else:
Just5810aa92001-06-24 15:12:48 +000093 return readOther(path), 'OTHER'
Just7842e561999-12-16 21:34:53 +000094
95def write(path, data, kind='OTHER', dohex=0):
Just5810aa92001-06-24 15:12:48 +000096 assertType1(data)
Just7842e561999-12-16 21:34:53 +000097 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':
Just5810aa92001-06-24 15:12:48 +0000105 writeLWFN(path, data)
Just7842e561999-12-16 21:34:53 +0000106 elif kind == 'PFB':
Just5810aa92001-06-24 15:12:48 +0000107 writePFB(path, data)
Just7842e561999-12-16 21:34:53 +0000108 else:
Just5810aa92001-06-24 15:12:48 +0000109 writeOther(path, data, dohex)
Just7842e561999-12-16 21:34:53 +0000110 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
Just5810aa92001-06-24 15:12:48 +0000125def readLWFN(path):
Just7842e561999-12-16 21:34:53 +0000126 """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, '')
Just5810aa92001-06-24 15:12:48 +0000152 assertType1(data)
Just7842e561999-12-16 21:34:53 +0000153 return data
154
Just5810aa92001-06-24 15:12:48 +0000155def readPFB(path, onlyHeader=0):
Just7842e561999-12-16 21:34:53 +0000156 """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]:
Just5810aa92001-06-24 15:12:48 +0000164 chunklen = stringToLong(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`
Just5810aa92001-06-24 15:12:48 +0000172 if onlyHeader:
173 break
Just7842e561999-12-16 21:34:53 +0000174 f.close()
175 data = string.join(data, '')
Just5810aa92001-06-24 15:12:48 +0000176 assertType1(data)
Just7842e561999-12-16 21:34:53 +0000177 return data
178
Just5810aa92001-06-24 15:12:48 +0000179def readOther(path):
Just7842e561999-12-16 21:34:53 +0000180 """reads any (font) file, returns raw data"""
181 f = open(path, "rb")
182 data = f.read()
183 f.close()
Just5810aa92001-06-24 15:12:48 +0000184 assertType1(data)
Just7842e561999-12-16 21:34:53 +0000185
Just5810aa92001-06-24 15:12:48 +0000186 chunks = findEncryptedChunks(data)
Just7842e561999-12-16 21:34:53 +0000187 data = []
Just5810aa92001-06-24 15:12:48 +0000188 for isEncrypted, chunk in chunks:
189 if isEncrypted and isHex(chunk[:4]):
190 data.append(deHexString(chunk))
Just7842e561999-12-16 21:34:53 +0000191 else:
192 data.append(chunk)
193 return string.join(data, '')
194
195# file writing tools
196
Just5810aa92001-06-24 15:12:48 +0000197def writeLWFN(path, data):
Just7842e561999-12-16 21:34:53 +0000198 Res.CreateResFile(path)
199 fss = macfs.FSSpec(path)
200 fss.SetCreatorType('just', 'LWFN')
201 resref = Res.OpenResFile(path)
202 try:
203 Res.UseResFile(resref)
204 resID = 501
Just5810aa92001-06-24 15:12:48 +0000205 chunks = findEncryptedChunks(data)
206 for isEncrypted, chunk in chunks:
207 if isEncrypted:
Just7842e561999-12-16 21:34:53 +0000208 code = 2
209 else:
210 code = 1
211 while chunk:
212 res = Res.Resource(chr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
213 res.AddResource('POST', resID, '')
214 chunk = chunk[LWFNCHUNKSIZE - 2:]
215 resID = resID + 1
216 res = Res.Resource(chr(5) + '\0')
217 res.AddResource('POST', resID, '')
218 finally:
219 Res.CloseResFile(resref)
220
Just5810aa92001-06-24 15:12:48 +0000221def writePFB(path, data):
222 chunks = findEncryptedChunks(data)
Just36183002000-03-28 10:38:43 +0000223 f = open(path, "wb")
Just7842e561999-12-16 21:34:53 +0000224 try:
Just5810aa92001-06-24 15:12:48 +0000225 for isEncrypted, chunk in chunks:
226 if isEncrypted:
Just7842e561999-12-16 21:34:53 +0000227 code = 2
228 else:
229 code = 1
230 f.write(chr(128) + chr(code))
Just5810aa92001-06-24 15:12:48 +0000231 f.write(longToString(len(chunk)))
Just7842e561999-12-16 21:34:53 +0000232 f.write(chunk)
233 f.write(chr(128) + chr(3))
234 finally:
235 f.close()
236 if os.name == 'mac':
Just36183002000-03-28 10:38:43 +0000237 fss = macfs.FSSpec(path)
Just7842e561999-12-16 21:34:53 +0000238 fss.SetCreatorType('mdos', 'BINA')
239
Just5810aa92001-06-24 15:12:48 +0000240def writeOther(path, data, dohex = 0):
241 chunks = findEncryptedChunks(data)
Just7842e561999-12-16 21:34:53 +0000242 f = open(path, "wb")
243 try:
244 hexlinelen = HEXLINELENGTH / 2
Just5810aa92001-06-24 15:12:48 +0000245 for isEncrypted, chunk in chunks:
246 if isEncrypted:
Just7842e561999-12-16 21:34:53 +0000247 code = 2
248 else:
249 code = 1
250 if code == 2 and dohex:
251 while chunk:
Justc2be3d92000-01-12 19:15:57 +0000252 f.write(eexec.hexString(chunk[:hexlinelen]))
Just7842e561999-12-16 21:34:53 +0000253 f.write('\r')
254 chunk = chunk[hexlinelen:]
255 else:
256 f.write(chunk)
257 finally:
258 f.close()
259 if os.name == 'mac':
260 fss = macfs.FSSpec(path)
261 fss.SetCreatorType('R*ch', 'TEXT') # BBEdit text file
262
263
264# decryption tools
265
266EEXECBEGIN = "currentfile eexec"
267EEXECEND = '0' * 64
268EEXECINTERNALEND = "currentfile closefile"
269EEXECBEGINMARKER = "%-- eexec start\r"
270EEXECENDMARKER = "%-- eexec end\r"
271
272_ishexRE = re.compile('[0-9A-Fa-f]*$')
273
Just5810aa92001-06-24 15:12:48 +0000274def isHex(text):
Just7842e561999-12-16 21:34:53 +0000275 return _ishexRE.match(text) is not None
276
277
Just5810aa92001-06-24 15:12:48 +0000278def decryptType1(data):
279 chunks = findEncryptedChunks(data)
Just7842e561999-12-16 21:34:53 +0000280 data = []
Just5810aa92001-06-24 15:12:48 +0000281 for isEncrypted, chunk in chunks:
282 if isEncrypted:
283 if isHex(chunk[:4]):
284 chunk = deHexString(chunk)
Justc2be3d92000-01-12 19:15:57 +0000285 decrypted, R = eexec.decrypt(chunk, 55665)
Just7842e561999-12-16 21:34:53 +0000286 decrypted = decrypted[4:]
287 if decrypted[-len(EEXECINTERNALEND)-1:-1] <> EEXECINTERNALEND \
288 and decrypted[-len(EEXECINTERNALEND)-2:-2] <> EEXECINTERNALEND:
289 raise error, "invalid end of eexec part"
290 decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r'
291 data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
292 else:
293 if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
294 data.append(chunk[:-len(EEXECBEGIN)-1])
295 else:
296 data.append(chunk)
297 return string.join(data, '')
298
Just5810aa92001-06-24 15:12:48 +0000299def findEncryptedChunks(data):
Just7842e561999-12-16 21:34:53 +0000300 chunks = []
301 while 1:
Just5810aa92001-06-24 15:12:48 +0000302 eBegin = string.find(data, EEXECBEGIN)
303 if eBegin < 0:
Just7842e561999-12-16 21:34:53 +0000304 break
Just5810aa92001-06-24 15:12:48 +0000305 eEnd = string.find(data, EEXECEND, eBegin)
306 if eEnd < 0:
Just7842e561999-12-16 21:34:53 +0000307 raise error, "can't find end of eexec part"
Just5810aa92001-06-24 15:12:48 +0000308 chunks.append((0, data[:eBegin + len(EEXECBEGIN) + 1]))
309 chunks.append((1, data[eBegin + len(EEXECBEGIN) + 1:eEnd]))
310 data = data[eEnd:]
Just7842e561999-12-16 21:34:53 +0000311 chunks.append((0, data))
312 return chunks
313
Just5810aa92001-06-24 15:12:48 +0000314def deHexString(hexstring):
Justc2be3d92000-01-12 19:15:57 +0000315 return eexec.deHexString(string.join(string.split(hexstring), ""))
Just7842e561999-12-16 21:34:53 +0000316
317
318# Type 1 assertion
319
320_fontType1RE = re.compile(r"/FontType\s+1\s+def")
321
Just5810aa92001-06-24 15:12:48 +0000322def assertType1(data):
Just7842e561999-12-16 21:34:53 +0000323 for head in ['%!PS-AdobeFont', '%!FontType1-1.0']:
324 if data[:len(head)] == head:
325 break
326 else:
327 raise error, "not a PostScript font"
328 if not _fontType1RE.search(data):
329 raise error, "not a Type 1 font"
330 if string.find(data, "currentfile eexec") < 0:
331 raise error, "not an encrypted Type 1 font"
332 # XXX what else?
333 return data
334
335
336# pfb helpers
337
Just5810aa92001-06-24 15:12:48 +0000338def longToString(long):
Just7842e561999-12-16 21:34:53 +0000339 str = ""
340 for i in range(4):
341 str = str + chr((long & (0xff << (i * 8))) >> i * 8)
342 return str
343
Just5810aa92001-06-24 15:12:48 +0000344def stringToLong(str):
Just7842e561999-12-16 21:34:53 +0000345 if len(str) <> 4:
346 raise ValueError, 'string must be 4 bytes long'
347 long = 0
348 for i in range(4):
349 long = long + (ord(str[i]) << (i * 8))
350 return long
Just5810aa92001-06-24 15:12:48 +0000351