t1Lib.py revision 05a16f2310e26193557a3dc223ac0efeb166789f
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, 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		fss = macfs.FSSpec(path)
246		fss.SetCreatorType('mdos', 'BINA')
247
248def writeOther(path, data, dohex = 0):
249	chunks = findEncryptedChunks(data)
250	f = open(path, "wb")
251	try:
252		hexlinelen = HEXLINELENGTH / 2
253		for isEncrypted, chunk in chunks:
254			if isEncrypted:
255				code = 2
256			else:
257				code = 1
258			if code == 2 and dohex:
259				while chunk:
260					f.write(eexec.hexString(chunk[:hexlinelen]))
261					f.write('\r')
262					chunk = chunk[hexlinelen:]
263			else:
264				f.write(chunk)
265	finally:
266		f.close()
267	if haveMacSupport:
268		fss = macfs.FSSpec(path)
269		fss.SetCreatorType('R*ch', 'TEXT') # BBEdit text file
270
271
272# decryption tools
273
274EEXECBEGIN = "currentfile eexec"
275EEXECEND = '0' * 64
276EEXECINTERNALEND = "currentfile closefile"
277EEXECBEGINMARKER = "%-- eexec start\r"
278EEXECENDMARKER = "%-- eexec end\r"
279
280_ishexRE = re.compile('[0-9A-Fa-f]*$')
281
282def isHex(text):
283	return _ishexRE.match(text) is not None
284
285
286def decryptType1(data):
287	chunks = findEncryptedChunks(data)
288	data = []
289	for isEncrypted, chunk in chunks:
290		if isEncrypted:
291			if isHex(chunk[:4]):
292				chunk = deHexString(chunk)
293			decrypted, R = eexec.decrypt(chunk, 55665)
294			decrypted = decrypted[4:]
295			if decrypted[-len(EEXECINTERNALEND)-1:-1] <> EEXECINTERNALEND \
296					and decrypted[-len(EEXECINTERNALEND)-2:-2] <> EEXECINTERNALEND:
297				raise T1Error, "invalid end of eexec part"
298			decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r'
299			data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
300		else:
301			if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
302				data.append(chunk[:-len(EEXECBEGIN)-1])
303			else:
304				data.append(chunk)
305	return string.join(data, '')
306
307def findEncryptedChunks(data):
308	chunks = []
309	while 1:
310		eBegin = string.find(data, EEXECBEGIN)
311		if eBegin < 0:
312			break
313		eBegin = eBegin + len(EEXECBEGIN) + 1
314		eEnd = string.find(data, EEXECEND, eBegin)
315		if eEnd < 0:
316			raise T1Error, "can't find end of eexec part"
317		cypherText = data[eBegin:eEnd + 2]
318		plainText, R = eexec.decrypt(cypherText, 55665)
319		eEndLocal = string.find(plainText, EEXECINTERNALEND)
320		if eEndLocal < 0:
321			raise T1Error, "can't find end of eexec part"
322		eEnd = eBegin + eEndLocal + len(EEXECINTERNALEND) + 1
323		chunks.append((0, data[:eBegin]))
324		chunks.append((1, data[eBegin:eEnd]))
325		data = data[eEnd:]
326	chunks.append((0, data))
327	return chunks
328
329def deHexString(hexstring):
330	return eexec.deHexString(string.join(string.split(hexstring), ""))
331
332
333# Type 1 assertion
334
335_fontType1RE = re.compile(r"/FontType\s+1\s+def")
336
337def assertType1(data):
338	for head in ['%!PS-AdobeFont', '%!FontType1-1.0']:
339		if data[:len(head)] == head:
340			break
341	else:
342		raise T1Error, "not a PostScript font"
343	if not _fontType1RE.search(data):
344		raise T1Error, "not a Type 1 font"
345	if string.find(data, "currentfile eexec") < 0:
346		raise T1Error, "not an encrypted Type 1 font"
347	# XXX what else?
348	return data
349
350
351# pfb helpers
352
353def longToString(long):
354	str = ""
355	for i in range(4):
356		str = str + chr((long & (0xff << (i * 8))) >> i * 8)
357	return str
358
359def stringToLong(str):
360	if len(str) <> 4:
361		raise ValueError, 'string must be 4 bytes long'
362	long = 0
363	for i in range(4):
364		long = long + (ord(str[i]) << (i * 8))
365	return long
366
367