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