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