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