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