t1Lib.py revision af1c9968b2edc827ee9115355a72b3c56c4fbe61
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"""
18from __future__ import print_function, division
19from fontTools.misc.py23 import *
20from fontTools.misc import eexec
21from fontTools.misc.macCreatorType import getMacCreatorAndType
22import os
23import re
24
25__author__ = "jvr"
26__version__ = "1.0b2"
27DEBUG = 0
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(object):
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 = path.lower()
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 = kind.upper()
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.FSOpenResFile(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 = byteord(res.data[0])
154			if byteord(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: ' + repr(code))
170	finally:
171		Res.CloseResFile(resRef)
172	data = bytesjoin(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 True:
181		if f.read(1) != bytechr(128):
182			raise T1Error('corrupt PFB file')
183		code = byteord(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: ' + repr(code))
193		if onlyHeader:
194			break
195	f.close()
196	data = bytesjoin(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 bytesjoin(data)
215
216# file writing tools
217
218def writeLWFN(path, data):
219	Res.FSpCreateResFile(path, "just", "LWFN", 0)
220	resRef = Res.FSOpenResFile(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(bytechr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
232				res.AddResource('POST', resID, '')
233				chunk = chunk[LWFNCHUNKSIZE - 2:]
234				resID = resID + 1
235		res = Res.Resource(bytechr(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(bytechr(128) + bytechr(code))
250			f.write(longToString(len(chunk)))
251			f.write(chunk)
252		f.write(bytechr(128) + bytechr(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 bytesjoin(data)
311
312def findEncryptedChunks(data):
313	chunks = []
314	while True:
315		eBegin = data.find(EEXECBEGIN)
316		if eBegin < 0:
317			break
318		eBegin = eBegin + len(EEXECBEGIN) + 1
319		eEnd = data.find(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 = plainText.find(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(strjoin(hexstring.split()))
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 data.find("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 + bytechr((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 + (byteord(str[i]) << (i * 8))
371	return long
372
373