1"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts (Python2 only)
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=False)
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 'OTHER'.
17"""
18from fontTools.misc.py23 import bytechr, byteord, bytesjoin
19from fontTools.misc import eexec
20from fontTools.misc.macCreatorType import getMacCreatorAndType
21import os
22import re
23
24__author__ = "jvr"
25__version__ = "1.0b2"
26DEBUG = 0
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
39
40class T1Error(Exception): pass
41
42
43class T1Font(object):
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, encoding="ascii", kind=None):
52		if kind is None:
53			self.data, _ = read(path)
54		elif kind == "LWFN":
55			self.data = readLWFN(path)
56		elif kind == "PFB":
57			self.data = readPFB(path)
58		elif kind == "OTHER":
59			self.data = readOther(path)
60		else:
61			raise ValueError(kind)
62		self.encoding = encoding
63
64	def saveAs(self, path, type, dohex=False):
65		write(path, self.getData(), type, dohex)
66
67	def getData(self):
68		# XXX Todo: if the data has been converted to Python object,
69		# recreate the PS stream
70		return self.data
71
72	def getGlyphSet(self):
73		"""Return a generic GlyphSet, which is a dict-like object
74		mapping glyph names to glyph objects. The returned glyph objects
75		have a .draw() method that supports the Pen protocol, and will
76		have an attribute named 'width', but only *after* the .draw() method
77		has been called.
78
79		In the case of Type 1, the GlyphSet is simply the CharStrings dict.
80		"""
81		return self["CharStrings"]
82
83	def __getitem__(self, key):
84		if not hasattr(self, "font"):
85			self.parse()
86		return self.font[key]
87
88	def parse(self):
89		from fontTools.misc import psLib
90		from fontTools.misc import psCharStrings
91		self.font = psLib.suckfont(self.data, self.encoding)
92		charStrings = self.font["CharStrings"]
93		lenIV = self.font["Private"].get("lenIV", 4)
94		assert lenIV >= 0
95		subrs = self.font["Private"]["Subrs"]
96		for glyphName, charString in charStrings.items():
97			charString, R = eexec.decrypt(charString, 4330)
98			charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:],
99					subrs=subrs)
100		for i in range(len(subrs)):
101			charString, R = eexec.decrypt(subrs[i], 4330)
102			subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
103		del self.data
104
105
106# low level T1 data read and write functions
107
108def read(path, onlyHeader=False):
109	"""reads any Type 1 font file, returns raw data"""
110	_, ext = os.path.splitext(path)
111	ext = ext.lower()
112	creator, typ = getMacCreatorAndType(path)
113	if typ == 'LWFN':
114		return readLWFN(path, onlyHeader), 'LWFN'
115	if ext == '.pfb':
116		return readPFB(path, onlyHeader), 'PFB'
117	else:
118		return readOther(path), 'OTHER'
119
120def write(path, data, kind='OTHER', dohex=False):
121	assertType1(data)
122	kind = kind.upper()
123	try:
124		os.remove(path)
125	except os.error:
126		pass
127	err = 1
128	try:
129		if kind == 'LWFN':
130			writeLWFN(path, data)
131		elif kind == 'PFB':
132			writePFB(path, data)
133		else:
134			writeOther(path, data, dohex)
135		err = 0
136	finally:
137		if err and not DEBUG:
138			try:
139				os.remove(path)
140			except os.error:
141				pass
142
143
144# -- internal --
145
146LWFNCHUNKSIZE = 2000
147HEXLINELENGTH = 80
148
149
150def readLWFN(path, onlyHeader=False):
151	"""reads an LWFN font file, returns raw data"""
152	from fontTools.misc.macRes import ResourceReader
153	reader = ResourceReader(path)
154	try:
155		data = []
156		for res in reader.get('POST', []):
157			code = byteord(res.data[0])
158			if byteord(res.data[1]) != 0:
159				raise T1Error('corrupt LWFN file')
160			if code in [1, 2]:
161				if onlyHeader and code == 2:
162					break
163				data.append(res.data[2:])
164			elif code in [3, 5]:
165				break
166			elif code == 4:
167				with open(path, "rb") as f:
168					data.append(f.read())
169			elif code == 0:
170				pass # comment, ignore
171			else:
172				raise T1Error('bad chunk code: ' + repr(code))
173	finally:
174		reader.close()
175	data = bytesjoin(data)
176	assertType1(data)
177	return data
178
179def readPFB(path, onlyHeader=False):
180	"""reads a PFB font file, returns raw data"""
181	data = []
182	with open(path, "rb") as f:
183		while True:
184			if f.read(1) != bytechr(128):
185				raise T1Error('corrupt PFB file')
186			code = byteord(f.read(1))
187			if code in [1, 2]:
188				chunklen = stringToLong(f.read(4))
189				chunk = f.read(chunklen)
190				assert len(chunk) == chunklen
191				data.append(chunk)
192			elif code == 3:
193				break
194			else:
195				raise T1Error('bad chunk code: ' + repr(code))
196			if onlyHeader:
197				break
198	data = bytesjoin(data)
199	assertType1(data)
200	return data
201
202def readOther(path):
203	"""reads any (font) file, returns raw data"""
204	with open(path, "rb") as f:
205		data = f.read()
206	assertType1(data)
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 was deprecated in OS X 10.5
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	with open(path, "wb") as f:
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
254def writeOther(path, data, dohex=False):
255	chunks = findEncryptedChunks(data)
256	with open(path, "wb") as f:
257		hexlinelen = HEXLINELENGTH // 2
258		for isEncrypted, chunk in chunks:
259			if isEncrypted:
260				code = 2
261			else:
262				code = 1
263			if code == 2 and dohex:
264				while chunk:
265					f.write(eexec.hexString(chunk[:hexlinelen]))
266					f.write(b'\r')
267					chunk = chunk[hexlinelen:]
268			else:
269				f.write(chunk)
270
271
272# decryption tools
273
274EEXECBEGIN = b"currentfile eexec"
275# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to
276# follow eexec
277EEXECEND = re.compile(b'(0[ \t\r\n]*){512}', flags=re.M)
278EEXECINTERNALEND = b"currentfile closefile"
279EEXECBEGINMARKER = b"%-- eexec start\r"
280EEXECENDMARKER = b"%-- eexec end\r"
281
282_ishexRE = re.compile(b'[0-9A-Fa-f]*$')
283
284def isHex(text):
285	return _ishexRE.match(text) is not None
286
287
288def decryptType1(data):
289	chunks = findEncryptedChunks(data)
290	data = []
291	for isEncrypted, chunk in chunks:
292		if isEncrypted:
293			if isHex(chunk[:4]):
294				chunk = deHexString(chunk)
295			decrypted, R = eexec.decrypt(chunk, 55665)
296			decrypted = decrypted[4:]
297			if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \
298					and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND:
299				raise T1Error("invalid end of eexec part")
300			decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r'
301			data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
302		else:
303			if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
304				data.append(chunk[:-len(EEXECBEGIN)-1])
305			else:
306				data.append(chunk)
307	return bytesjoin(data)
308
309def findEncryptedChunks(data):
310	chunks = []
311	while True:
312		eBegin = data.find(EEXECBEGIN)
313		if eBegin < 0:
314			break
315		eBegin = eBegin + len(EEXECBEGIN) + 1
316		endMatch = EEXECEND.search(data, eBegin)
317		if endMatch is None:
318			raise T1Error("can't find end of eexec part")
319		eEnd = endMatch.start()
320		cypherText = data[eBegin:eEnd + 2]
321		if isHex(cypherText[:4]):
322			cypherText = deHexString(cypherText)
323		plainText, R = eexec.decrypt(cypherText, 55665)
324		eEndLocal = plainText.find(EEXECINTERNALEND)
325		if eEndLocal < 0:
326			raise T1Error("can't find end of eexec part")
327		chunks.append((0, data[:eBegin]))
328		chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1]))
329		data = data[eEnd:]
330	chunks.append((0, data))
331	return chunks
332
333def deHexString(hexstring):
334	return eexec.deHexString(bytesjoin(hexstring.split()))
335
336
337# Type 1 assertion
338
339_fontType1RE = re.compile(br"/FontType\s+1\s+def")
340
341def assertType1(data):
342	for head in [b'%!PS-AdobeFont', b'%!FontType1']:
343		if data[:len(head)] == head:
344			break
345	else:
346		raise T1Error("not a PostScript font")
347	if not _fontType1RE.search(data):
348		raise T1Error("not a Type 1 font")
349	if data.find(b"currentfile eexec") < 0:
350		raise T1Error("not an encrypted Type 1 font")
351	# XXX what else?
352	return data
353
354
355# pfb helpers
356
357def longToString(long):
358	s = b""
359	for i in range(4):
360		s += bytechr((long & (0xff << (i * 8))) >> i * 8)
361	return s
362
363def stringToLong(s):
364	if len(s) != 4:
365		raise ValueError('string must be 4 bytes long')
366	l = 0
367	for i in range(4):
368		l += byteord(s[i]) << (i * 8)
369	return l
370