t1Lib.py revision 489d76a340845361def6af9ab7d9152f8e66f417
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 MacOS
39
40
41class T1Error(Exception): pass
42
43
44class T1Font:
45
46	"""Type 1 font class.
47
48	Uses a minimal interpeter that supports just about enough PS to parse
49	Type 1 fonts.
50	"""
51
52	def __init__(self, path=None):
53		if path is not None:
54			self.data, type = read(path)
55		else:
56			pass # XXX
57
58	def saveAs(self, path, type):
59		write(path, self.getData(), type)
60
61	def getData(self):
62		# XXX Todo: if the data has been converted to Python object,
63		# recreate the PS stream
64		return self.data
65
66	def __getitem__(self, key):
67		if not hasattr(self, "font"):
68			self.parse()
69		return self.font[key]
70
71	def parse(self):
72		from fontTools.misc import psLib
73		from fontTools.misc import psCharStrings
74		self.font = psLib.suckfont(self.data)
75		charStrings = self.font["CharStrings"]
76		lenIV = self.font["Private"].get("lenIV", 4)
77		assert lenIV >= 0
78		subrs = self.font["Private"]["Subrs"]
79		for glyphName, charString in charStrings.items():
80			charString, R = eexec.decrypt(charString, 4330)
81			charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:],
82					subrs=subrs)
83		for i in range(len(subrs)):
84			charString, R = eexec.decrypt(subrs[i], 4330)
85			subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
86		del self.data
87
88
89# low level T1 data read and write functions
90
91def read(path):
92	"""reads any Type 1 font file, returns raw data"""
93	normpath = string.lower(path)
94	if haveMacSupport:
95		creator, type = MacOS.GetCreatorAndType(path)
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, onlyHeader=0):
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				if onlyHeader and code == 2:
147					break
148				data.append(res.data[2:])
149			elif code in [3, 5]:
150				break
151			elif code == 4:
152				f = open(path, "rb")
153				data.append(f.read())
154				f.close()
155			elif code == 0:
156				pass # comment, ignore
157			else:
158				raise T1Error, 'bad chunk code: ' + `code`
159	finally:
160		Res.CloseResFile(resRef)
161	data = string.join(data, '')
162	assertType1(data)
163	return data
164
165def readPFB(path, onlyHeader=0):
166	"""reads a PFB font file, returns raw data"""
167	f = open(path, "rb")
168	data = []
169	while 1:
170		if f.read(1) <> chr(128):
171			raise T1Error, 'corrupt PFB file'
172		code = ord(f.read(1))
173		if code in [1, 2]:
174			chunklen = stringToLong(f.read(4))
175			chunk = f.read(chunklen)
176			assert len(chunk) == chunklen
177			data.append(chunk)
178		elif code == 3:
179			break
180		else:
181			raise T1Error, 'bad chunk code: ' + `code`
182		if onlyHeader:
183			break
184	f.close()
185	data = string.join(data, '')
186	assertType1(data)
187	return data
188
189def readOther(path):
190	"""reads any (font) file, returns raw data"""
191	f = open(path, "rb")
192	data = f.read()
193	f.close()
194	assertType1(data)
195
196	chunks = findEncryptedChunks(data)
197	data = []
198	for isEncrypted, chunk in chunks:
199		if isEncrypted and isHex(chunk[:4]):
200			data.append(deHexString(chunk))
201		else:
202			data.append(chunk)
203	return string.join(data, '')
204
205# file writing tools
206
207def writeLWFN(path, data):
208	Res.FSpCreateResFile(path, "just", "LWFN", 0)
209	resRef = Res.FSpOpenResFile(path, 2)  # write-only
210	try:
211		Res.UseResFile(resRef)
212		resID = 501
213		chunks = findEncryptedChunks(data)
214		for isEncrypted, chunk in chunks:
215			if isEncrypted:
216				code = 2
217			else:
218				code = 1
219			while chunk:
220				res = Res.Resource(chr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
221				res.AddResource('POST', resID, '')
222				chunk = chunk[LWFNCHUNKSIZE - 2:]
223				resID = resID + 1
224		res = Res.Resource(chr(5) + '\0')
225		res.AddResource('POST', resID, '')
226	finally:
227		Res.CloseResFile(resRef)
228
229def writePFB(path, data):
230	chunks = findEncryptedChunks(data)
231	f = open(path, "wb")
232	try:
233		for isEncrypted, chunk in chunks:
234			if isEncrypted:
235				code = 2
236			else:
237				code = 1
238			f.write(chr(128) + chr(code))
239			f.write(longToString(len(chunk)))
240			f.write(chunk)
241		f.write(chr(128) + chr(3))
242	finally:
243		f.close()
244	if haveMacSupport:
245		MacOS.SetCreatorAndType(path, 'mdos', 'BINA')
246
247def writeOther(path, data, dohex = 0):
248	chunks = findEncryptedChunks(data)
249	f = open(path, "wb")
250	try:
251		hexlinelen = HEXLINELENGTH / 2
252		for isEncrypted, chunk in chunks:
253			if isEncrypted:
254				code = 2
255			else:
256				code = 1
257			if code == 2 and dohex:
258				while chunk:
259					f.write(eexec.hexString(chunk[:hexlinelen]))
260					f.write('\r')
261					chunk = chunk[hexlinelen:]
262			else:
263				f.write(chunk)
264	finally:
265		f.close()
266	if haveMacSupport:
267		MacOS.SetCreatorAndType(path, '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