t1Lib.py revision 7d4b693627c128bea3ad0c6dd49e923fe5ca4947
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 getGlyphSet(self):
67		"""Return a generic GlyphSet, which is a dict-like object
68		mapping glyph names to glyph objects. The returned glyph objects
69		have a .draw() method that supports the Pen protocol, and will
70		have an attribute named 'width', but only *after* the .draw() method
71		has been called.
72
73		In the case of Type 1, the GlyphSet is simply the CharStrings dict.
74		"""
75		return self["CharStrings"]
76
77	def __getitem__(self, key):
78		if not hasattr(self, "font"):
79			self.parse()
80		return self.font[key]
81
82	def parse(self):
83		from fontTools.misc import psLib
84		from fontTools.misc import psCharStrings
85		self.font = psLib.suckfont(self.data)
86		charStrings = self.font["CharStrings"]
87		lenIV = self.font["Private"].get("lenIV", 4)
88		assert lenIV >= 0
89		subrs = self.font["Private"]["Subrs"]
90		for glyphName, charString in charStrings.items():
91			charString, R = eexec.decrypt(charString, 4330)
92			charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:],
93					subrs=subrs)
94		for i in range(len(subrs)):
95			charString, R = eexec.decrypt(subrs[i], 4330)
96			subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
97		del self.data
98
99
100# low level T1 data read and write functions
101
102def read(path):
103	"""reads any Type 1 font file, returns raw data"""
104	normpath = string.lower(path)
105	if haveMacSupport:
106		creator, type = MacOS.GetCreatorAndType(path)
107		if type == 'LWFN':
108			return readLWFN(path), 'LWFN'
109	if normpath[-4:] == '.pfb':
110		return readPFB(path), '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	if haveMacSupport:
256		MacOS.SetCreatorAndType(path, 'mdos', 'BINA')
257
258def writeOther(path, data, dohex = 0):
259	chunks = findEncryptedChunks(data)
260	f = open(path, "wb")
261	try:
262		hexlinelen = HEXLINELENGTH / 2
263		for isEncrypted, chunk in chunks:
264			if isEncrypted:
265				code = 2
266			else:
267				code = 1
268			if code == 2 and dohex:
269				while chunk:
270					f.write(eexec.hexString(chunk[:hexlinelen]))
271					f.write('\r')
272					chunk = chunk[hexlinelen:]
273			else:
274				f.write(chunk)
275	finally:
276		f.close()
277	if haveMacSupport:
278		MacOS.SetCreatorAndType(path, 'R*ch', 'TEXT') # BBEdit text file
279
280
281# decryption tools
282
283EEXECBEGIN = "currentfile eexec"
284EEXECEND = '0' * 64
285EEXECINTERNALEND = "currentfile closefile"
286EEXECBEGINMARKER = "%-- eexec start\r"
287EEXECENDMARKER = "%-- eexec end\r"
288
289_ishexRE = re.compile('[0-9A-Fa-f]*$')
290
291def isHex(text):
292	return _ishexRE.match(text) is not None
293
294
295def decryptType1(data):
296	chunks = findEncryptedChunks(data)
297	data = []
298	for isEncrypted, chunk in chunks:
299		if isEncrypted:
300			if isHex(chunk[:4]):
301				chunk = deHexString(chunk)
302			decrypted, R = eexec.decrypt(chunk, 55665)
303			decrypted = decrypted[4:]
304			if decrypted[-len(EEXECINTERNALEND)-1:-1] <> EEXECINTERNALEND \
305					and decrypted[-len(EEXECINTERNALEND)-2:-2] <> EEXECINTERNALEND:
306				raise T1Error, "invalid end of eexec part"
307			decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r'
308			data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
309		else:
310			if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
311				data.append(chunk[:-len(EEXECBEGIN)-1])
312			else:
313				data.append(chunk)
314	return string.join(data, '')
315
316def findEncryptedChunks(data):
317	chunks = []
318	while 1:
319		eBegin = string.find(data, EEXECBEGIN)
320		if eBegin < 0:
321			break
322		eBegin = eBegin + len(EEXECBEGIN) + 1
323		eEnd = string.find(data, EEXECEND, eBegin)
324		if eEnd < 0:
325			raise T1Error, "can't find end of eexec part"
326		cypherText = data[eBegin:eEnd + 2]
327		plainText, R = eexec.decrypt(cypherText, 55665)
328		eEndLocal = string.find(plainText, EEXECINTERNALEND)
329		if eEndLocal < 0:
330			raise T1Error, "can't find end of eexec part"
331		eEnd = eBegin + eEndLocal + len(EEXECINTERNALEND) + 1
332		chunks.append((0, data[:eBegin]))
333		chunks.append((1, data[eBegin:eEnd]))
334		data = data[eEnd:]
335	chunks.append((0, data))
336	return chunks
337
338def deHexString(hexstring):
339	return eexec.deHexString(string.join(string.split(hexstring), ""))
340
341
342# Type 1 assertion
343
344_fontType1RE = re.compile(r"/FontType\s+1\s+def")
345
346def assertType1(data):
347	for head in ['%!PS-AdobeFont', '%!FontType1-1.0']:
348		if data[:len(head)] == head:
349			break
350	else:
351		raise T1Error, "not a PostScript font"
352	if not _fontType1RE.search(data):
353		raise T1Error, "not a Type 1 font"
354	if string.find(data, "currentfile eexec") < 0:
355		raise T1Error, "not an encrypted Type 1 font"
356	# XXX what else?
357	return data
358
359
360# pfb helpers
361
362def longToString(long):
363	str = ""
364	for i in range(4):
365		str = str + chr((long & (0xff << (i * 8))) >> i * 8)
366	return str
367
368def stringToLong(str):
369	if len(str) <> 4:
370		raise ValueError, 'string must be 4 bytes long'
371	long = 0
372	for i in range(4):
373		long = long + (ord(str[i]) << (i * 8))
374	return long
375
376