• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"
276EEXECEND = b'0' * 64
277EEXECINTERNALEND = b"currentfile closefile"
278EEXECBEGINMARKER = b"%-- eexec start\r"
279EEXECENDMARKER = b"%-- eexec end\r"
280
281_ishexRE = re.compile(b'[0-9A-Fa-f]*$')
282
283def isHex(text):
284	return _ishexRE.match(text) is not None
285
286
287def decryptType1(data):
288	chunks = findEncryptedChunks(data)
289	data = []
290	for isEncrypted, chunk in chunks:
291		if isEncrypted:
292			if isHex(chunk[:4]):
293				chunk = deHexString(chunk)
294			decrypted, R = eexec.decrypt(chunk, 55665)
295			decrypted = decrypted[4:]
296			if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \
297					and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND:
298				raise T1Error("invalid end of eexec part")
299			decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r'
300			data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
301		else:
302			if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
303				data.append(chunk[:-len(EEXECBEGIN)-1])
304			else:
305				data.append(chunk)
306	return bytesjoin(data)
307
308def findEncryptedChunks(data):
309	chunks = []
310	while True:
311		eBegin = data.find(EEXECBEGIN)
312		if eBegin < 0:
313			break
314		eBegin = eBegin + len(EEXECBEGIN) + 1
315		eEnd = data.find(EEXECEND, eBegin)
316		if eEnd < 0:
317			raise T1Error("can't find end of eexec part")
318		cypherText = data[eBegin:eEnd + 2]
319		if isHex(cypherText[:4]):
320			cypherText = deHexString(cypherText)
321		plainText, R = eexec.decrypt(cypherText, 55665)
322		eEndLocal = plainText.find(EEXECINTERNALEND)
323		if eEndLocal < 0:
324			raise T1Error("can't find end of eexec part")
325		chunks.append((0, data[:eBegin]))
326		chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1]))
327		data = data[eEnd:]
328	chunks.append((0, data))
329	return chunks
330
331def deHexString(hexstring):
332	return eexec.deHexString(bytesjoin(hexstring.split()))
333
334
335# Type 1 assertion
336
337_fontType1RE = re.compile(br"/FontType\s+1\s+def")
338
339def assertType1(data):
340	for head in [b'%!PS-AdobeFont', b'%!FontType1']:
341		if data[:len(head)] == head:
342			break
343	else:
344		raise T1Error("not a PostScript font")
345	if not _fontType1RE.search(data):
346		raise T1Error("not a Type 1 font")
347	if data.find(b"currentfile eexec") < 0:
348		raise T1Error("not an encrypted Type 1 font")
349	# XXX what else?
350	return data
351
352
353# pfb helpers
354
355def longToString(long):
356	s = b""
357	for i in range(4):
358		s += bytechr((long & (0xff << (i * 8))) >> i * 8)
359	return s
360
361def stringToLong(s):
362	if len(s) != 4:
363		raise ValueError('string must be 4 bytes long')
364	l = 0
365	for i in range(4):
366		l += byteord(s[i]) << (i * 8)
367	return l
368