• 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"""
18import fontTools
19from fontTools.misc import eexec
20from fontTools.misc.macCreatorType import getMacCreatorAndType
21from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes
22from fontTools.misc.psOperators import _type1_pre_eexec_order, _type1_fontinfo_order, _type1_post_eexec_order
23from fontTools.encodings.StandardEncoding import StandardEncoding
24import os
25import re
26
27__author__ = "jvr"
28__version__ = "1.0b3"
29DEBUG = 0
30
31
32try:
33	try:
34		from Carbon import Res
35	except ImportError:
36		import Res  # MacPython < 2.2
37except ImportError:
38	haveMacSupport = 0
39else:
40	haveMacSupport = 1
41
42
43class T1Error(Exception): pass
44
45
46class T1Font(object):
47
48	"""Type 1 font class.
49
50	Uses a minimal interpeter that supports just about enough PS to parse
51	Type 1 fonts.
52	"""
53
54	def __init__(self, path, encoding="ascii", kind=None):
55		if kind is None:
56			self.data, _ = read(path)
57		elif kind == "LWFN":
58			self.data = readLWFN(path)
59		elif kind == "PFB":
60			self.data = readPFB(path)
61		elif kind == "OTHER":
62			self.data = readOther(path)
63		else:
64			raise ValueError(kind)
65		self.encoding = encoding
66
67	def saveAs(self, path, type, dohex=False):
68		write(path, self.getData(), type, dohex)
69
70	def getData(self):
71		if not hasattr(self, "data"):
72			self.data = self.createData()
73		return self.data
74
75	def getGlyphSet(self):
76		"""Return a generic GlyphSet, which is a dict-like object
77		mapping glyph names to glyph objects. The returned glyph objects
78		have a .draw() method that supports the Pen protocol, and will
79		have an attribute named 'width', but only *after* the .draw() method
80		has been called.
81
82		In the case of Type 1, the GlyphSet is simply the CharStrings dict.
83		"""
84		return self["CharStrings"]
85
86	def __getitem__(self, key):
87		if not hasattr(self, "font"):
88			self.parse()
89		return self.font[key]
90
91	def parse(self):
92		from fontTools.misc import psLib
93		from fontTools.misc import psCharStrings
94		self.font = psLib.suckfont(self.data, self.encoding)
95		charStrings = self.font["CharStrings"]
96		lenIV = self.font["Private"].get("lenIV", 4)
97		assert lenIV >= 0
98		subrs = self.font["Private"]["Subrs"]
99		for glyphName, charString in charStrings.items():
100			charString, R = eexec.decrypt(charString, 4330)
101			charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:],
102					subrs=subrs)
103		for i in range(len(subrs)):
104			charString, R = eexec.decrypt(subrs[i], 4330)
105			subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
106		del self.data
107
108	def createData(self):
109		sf = self.font
110
111		eexec_began = False
112		eexec_dict = {}
113		lines = []
114		lines.extend([self._tobytes(f"%!FontType1-1.1: {sf['FontName']}"),
115					  self._tobytes(f"%t1Font: ({fontTools.version})"),
116					  self._tobytes(f"%%BeginResource: font {sf['FontName']}")])
117		# follow t1write.c:writeRegNameKeyedFont
118		size = 3 # Headroom for new key addition
119		size += 1 # FontMatrix is always counted
120		size += 1 + 1 # Private, CharStings
121		for key in font_dictionary_keys:
122			size += int(key in sf)
123		lines.append(self._tobytes(f"{size} dict dup begin"))
124
125		for key, value in sf.items():
126			if eexec_began:
127				eexec_dict[key] = value
128				continue
129
130			if key == "FontInfo":
131				fi = sf["FontInfo"]
132				# follow t1write.c:writeFontInfoDict
133				size = 3 # Headroom for new key addition
134				for subkey in FontInfo_dictionary_keys:
135					size += int(subkey in fi)
136				lines.append(self._tobytes(f"/FontInfo {size} dict dup begin"))
137
138				for subkey, subvalue in fi.items():
139					lines.extend(self._make_lines(subkey, subvalue))
140				lines.append(b"end def")
141			elif key in _type1_post_eexec_order: # usually 'Private'
142				eexec_dict[key] = value
143				eexec_began = True
144			else:
145				lines.extend(self._make_lines(key, value))
146		lines.append(b"end")
147		eexec_portion = self.encode_eexec(eexec_dict)
148		lines.append(bytesjoin([b"currentfile eexec ", eexec_portion]))
149
150		for _ in range(8):
151			lines.append(self._tobytes("0"*64))
152		lines.extend([b"cleartomark",
153					  b"%%EndResource",
154					  b"%%EOF"])
155
156		data = bytesjoin(lines, "\n")
157		return data
158
159	def encode_eexec(self, eexec_dict):
160		lines = []
161
162		# '-|', '|-', '|'
163		RD_key, ND_key, NP_key = None, None, None
164
165		for key, value in eexec_dict.items():
166			if key == "Private":
167				pr = eexec_dict["Private"]
168				# follow t1write.c:writePrivateDict
169				size = 3 # for RD, ND, NP
170				for subkey in Private_dictionary_keys:
171					size += int(subkey in pr)
172				lines.append(b"dup /Private")
173				lines.append(self._tobytes(f"{size} dict dup begin"))
174				for subkey, subvalue in pr.items():
175					if not RD_key and subvalue == RD_value:
176						RD_key = subkey
177					elif not ND_key and subvalue == ND_value:
178						ND_key = subkey
179					elif not NP_key and subvalue == PD_value:
180						NP_key = subkey
181
182					if subkey == 'OtherSubrs':
183						# XXX: assert that no flex hint is used
184						lines.append(self._tobytes(hintothers))
185					elif subkey == "Subrs":
186						# XXX: standard Subrs only
187						lines.append(b"/Subrs 5 array")
188						for i, subr_bin in enumerate(std_subrs):
189							encrypted_subr, R = eexec.encrypt(bytesjoin([char_IV, subr_bin]), 4330)
190							lines.append(bytesjoin([self._tobytes(f"dup {i} {len(encrypted_subr)} {RD_key} "), encrypted_subr, self._tobytes(f" {NP_key}")]))
191						lines.append(b'def')
192
193						lines.append(b"put")
194					else:
195						lines.extend(self._make_lines(subkey, subvalue))
196			elif key == "CharStrings":
197				lines.append(b"dup /CharStrings")
198				lines.append(self._tobytes(f"{len(eexec_dict['CharStrings'])} dict dup begin"))
199				for glyph_name, char_bin in eexec_dict["CharStrings"].items():
200					char_bin.compile()
201					encrypted_char, R = eexec.encrypt(bytesjoin([char_IV, char_bin.bytecode]), 4330)
202					lines.append(bytesjoin([self._tobytes(f"/{glyph_name} {len(encrypted_char)} {RD_key} "), encrypted_char, self._tobytes(f" {ND_key}")]))
203				lines.append(b"end put")
204			else:
205				lines.extend(self._make_lines(key, value))
206
207		lines.extend([b"end",
208					  b"dup /FontName get exch definefont pop",
209					  b"mark",
210					  b"currentfile closefile\n"])
211
212		eexec_portion = bytesjoin(lines, "\n")
213		encrypted_eexec, R = eexec.encrypt(bytesjoin([eexec_IV, eexec_portion]), 55665)
214
215		return encrypted_eexec
216
217	def _make_lines(self, key, value):
218		if key == "FontName":
219			return [self._tobytes(f"/{key} /{value} def")]
220		if key in ["isFixedPitch", "ForceBold", "RndStemUp"]:
221			return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
222		elif key == "Encoding":
223			if value == StandardEncoding:
224				return [self._tobytes(f"/{key} StandardEncoding def")]
225			else:
226				# follow fontTools.misc.psOperators._type1_Encoding_repr
227				lines = []
228				lines.append(b"/Encoding 256 array")
229				lines.append(b"0 1 255 {1 index exch /.notdef put} for")
230				for i in range(256):
231					name = value[i]
232					if name != ".notdef":
233						lines.append(self._tobytes(f"dup {i} /{name} put"))
234				lines.append(b"def")
235				return lines
236		if isinstance(value, str):
237			return [self._tobytes(f"/{key} ({value}) def")]
238		elif isinstance(value, bool):
239			return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
240		elif isinstance(value, list):
241			return [self._tobytes(f"/{key} [{' '.join(str(v) for v in value)}] def")]
242		elif isinstance(value, tuple):
243			return [self._tobytes(f"/{key} {{{' '.join(str(v) for v in value)}}} def")]
244		else:
245			return [self._tobytes(f"/{key} {value} def")]
246
247	def _tobytes(self, s, errors="strict"):
248		return tobytes(s, self.encoding, errors)
249
250
251# low level T1 data read and write functions
252
253def read(path, onlyHeader=False):
254	"""reads any Type 1 font file, returns raw data"""
255	_, ext = os.path.splitext(path)
256	ext = ext.lower()
257	creator, typ = getMacCreatorAndType(path)
258	if typ == 'LWFN':
259		return readLWFN(path, onlyHeader), 'LWFN'
260	if ext == '.pfb':
261		return readPFB(path, onlyHeader), 'PFB'
262	else:
263		return readOther(path), 'OTHER'
264
265def write(path, data, kind='OTHER', dohex=False):
266	assertType1(data)
267	kind = kind.upper()
268	try:
269		os.remove(path)
270	except os.error:
271		pass
272	err = 1
273	try:
274		if kind == 'LWFN':
275			writeLWFN(path, data)
276		elif kind == 'PFB':
277			writePFB(path, data)
278		else:
279			writeOther(path, data, dohex)
280		err = 0
281	finally:
282		if err and not DEBUG:
283			try:
284				os.remove(path)
285			except os.error:
286				pass
287
288
289# -- internal --
290
291LWFNCHUNKSIZE = 2000
292HEXLINELENGTH = 80
293
294
295def readLWFN(path, onlyHeader=False):
296	"""reads an LWFN font file, returns raw data"""
297	from fontTools.misc.macRes import ResourceReader
298	reader = ResourceReader(path)
299	try:
300		data = []
301		for res in reader.get('POST', []):
302			code = byteord(res.data[0])
303			if byteord(res.data[1]) != 0:
304				raise T1Error('corrupt LWFN file')
305			if code in [1, 2]:
306				if onlyHeader and code == 2:
307					break
308				data.append(res.data[2:])
309			elif code in [3, 5]:
310				break
311			elif code == 4:
312				with open(path, "rb") as f:
313					data.append(f.read())
314			elif code == 0:
315				pass # comment, ignore
316			else:
317				raise T1Error('bad chunk code: ' + repr(code))
318	finally:
319		reader.close()
320	data = bytesjoin(data)
321	assertType1(data)
322	return data
323
324def readPFB(path, onlyHeader=False):
325	"""reads a PFB font file, returns raw data"""
326	data = []
327	with open(path, "rb") as f:
328		while True:
329			if f.read(1) != bytechr(128):
330				raise T1Error('corrupt PFB file')
331			code = byteord(f.read(1))
332			if code in [1, 2]:
333				chunklen = stringToLong(f.read(4))
334				chunk = f.read(chunklen)
335				assert len(chunk) == chunklen
336				data.append(chunk)
337			elif code == 3:
338				break
339			else:
340				raise T1Error('bad chunk code: ' + repr(code))
341			if onlyHeader:
342				break
343	data = bytesjoin(data)
344	assertType1(data)
345	return data
346
347def readOther(path):
348	"""reads any (font) file, returns raw data"""
349	with open(path, "rb") as f:
350		data = f.read()
351	assertType1(data)
352	chunks = findEncryptedChunks(data)
353	data = []
354	for isEncrypted, chunk in chunks:
355		if isEncrypted and isHex(chunk[:4]):
356			data.append(deHexString(chunk))
357		else:
358			data.append(chunk)
359	return bytesjoin(data)
360
361# file writing tools
362
363def writeLWFN(path, data):
364	# Res.FSpCreateResFile was deprecated in OS X 10.5
365	Res.FSpCreateResFile(path, "just", "LWFN", 0)
366	resRef = Res.FSOpenResFile(path, 2)  # write-only
367	try:
368		Res.UseResFile(resRef)
369		resID = 501
370		chunks = findEncryptedChunks(data)
371		for isEncrypted, chunk in chunks:
372			if isEncrypted:
373				code = 2
374			else:
375				code = 1
376			while chunk:
377				res = Res.Resource(bytechr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
378				res.AddResource('POST', resID, '')
379				chunk = chunk[LWFNCHUNKSIZE - 2:]
380				resID = resID + 1
381		res = Res.Resource(bytechr(5) + '\0')
382		res.AddResource('POST', resID, '')
383	finally:
384		Res.CloseResFile(resRef)
385
386def writePFB(path, data):
387	chunks = findEncryptedChunks(data)
388	with open(path, "wb") as f:
389		for isEncrypted, chunk in chunks:
390			if isEncrypted:
391				code = 2
392			else:
393				code = 1
394			f.write(bytechr(128) + bytechr(code))
395			f.write(longToString(len(chunk)))
396			f.write(chunk)
397		f.write(bytechr(128) + bytechr(3))
398
399def writeOther(path, data, dohex=False):
400	chunks = findEncryptedChunks(data)
401	with open(path, "wb") as f:
402		hexlinelen = HEXLINELENGTH // 2
403		for isEncrypted, chunk in chunks:
404			if isEncrypted:
405				code = 2
406			else:
407				code = 1
408			if code == 2 and dohex:
409				while chunk:
410					f.write(eexec.hexString(chunk[:hexlinelen]))
411					f.write(b'\r')
412					chunk = chunk[hexlinelen:]
413			else:
414				f.write(chunk)
415
416
417# decryption tools
418
419EEXECBEGIN = b"currentfile eexec"
420# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to
421# follow eexec
422EEXECEND = re.compile(b'(0[ \t\r\n]*){512}', flags=re.M)
423EEXECINTERNALEND = b"currentfile closefile"
424EEXECBEGINMARKER = b"%-- eexec start\r"
425EEXECENDMARKER = b"%-- eexec end\r"
426
427_ishexRE = re.compile(b'[0-9A-Fa-f]*$')
428
429def isHex(text):
430	return _ishexRE.match(text) is not None
431
432
433def decryptType1(data):
434	chunks = findEncryptedChunks(data)
435	data = []
436	for isEncrypted, chunk in chunks:
437		if isEncrypted:
438			if isHex(chunk[:4]):
439				chunk = deHexString(chunk)
440			decrypted, R = eexec.decrypt(chunk, 55665)
441			decrypted = decrypted[4:]
442			if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \
443					and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND:
444				raise T1Error("invalid end of eexec part")
445			decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + b'\r'
446			data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
447		else:
448			if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
449				data.append(chunk[:-len(EEXECBEGIN)-1])
450			else:
451				data.append(chunk)
452	return bytesjoin(data)
453
454def findEncryptedChunks(data):
455	chunks = []
456	while True:
457		eBegin = data.find(EEXECBEGIN)
458		if eBegin < 0:
459			break
460		eBegin = eBegin + len(EEXECBEGIN) + 1
461		endMatch = EEXECEND.search(data, eBegin)
462		if endMatch is None:
463			raise T1Error("can't find end of eexec part")
464		eEnd = endMatch.start()
465		cypherText = data[eBegin:eEnd + 2]
466		if isHex(cypherText[:4]):
467			cypherText = deHexString(cypherText)
468		plainText, R = eexec.decrypt(cypherText, 55665)
469		eEndLocal = plainText.find(EEXECINTERNALEND)
470		if eEndLocal < 0:
471			raise T1Error("can't find end of eexec part")
472		chunks.append((0, data[:eBegin]))
473		chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1]))
474		data = data[eEnd:]
475	chunks.append((0, data))
476	return chunks
477
478def deHexString(hexstring):
479	return eexec.deHexString(bytesjoin(hexstring.split()))
480
481
482# Type 1 assertion
483
484_fontType1RE = re.compile(br"/FontType\s+1\s+def")
485
486def assertType1(data):
487	for head in [b'%!PS-AdobeFont', b'%!FontType1']:
488		if data[:len(head)] == head:
489			break
490	else:
491		raise T1Error("not a PostScript font")
492	if not _fontType1RE.search(data):
493		raise T1Error("not a Type 1 font")
494	if data.find(b"currentfile eexec") < 0:
495		raise T1Error("not an encrypted Type 1 font")
496	# XXX what else?
497	return data
498
499
500# pfb helpers
501
502def longToString(long):
503	s = b""
504	for i in range(4):
505		s += bytechr((long & (0xff << (i * 8))) >> i * 8)
506	return s
507
508def stringToLong(s):
509	if len(s) != 4:
510		raise ValueError('string must be 4 bytes long')
511	l = 0
512	for i in range(4):
513		l += byteord(s[i]) << (i * 8)
514	return l
515
516
517# PS stream helpers
518
519font_dictionary_keys = list(_type1_pre_eexec_order)
520# t1write.c:writeRegNameKeyedFont
521# always counts following keys
522font_dictionary_keys.remove("FontMatrix")
523
524FontInfo_dictionary_keys = list(_type1_fontinfo_order)
525# extend because AFDKO tx may use following keys
526FontInfo_dictionary_keys.extend([
527	"FSType",
528	"Copyright",
529])
530
531Private_dictionary_keys = [
532	# We don't know what names will be actually used.
533	# "RD",
534	# "ND",
535	# "NP",
536	"Subrs",
537	"OtherSubrs",
538	"UniqueID",
539	"BlueValues",
540	"OtherBlues",
541	"FamilyBlues",
542	"FamilyOtherBlues",
543	"BlueScale",
544	"BlueShift",
545	"BlueFuzz",
546	"StdHW",
547	"StdVW",
548	"StemSnapH",
549	"StemSnapV",
550	"ForceBold",
551	"LanguageGroup",
552	"password",
553	"lenIV",
554	"MinFeature",
555	"RndStemUp",
556]
557
558# t1write_hintothers.h
559hintothers = """/OtherSubrs[{}{}{}{systemdict/internaldict known not{pop 3}{1183615869
560systemdict/internaldict get exec dup/startlock known{/startlock get exec}{dup
561/strtlck known{/strtlck get exec}{pop 3}ifelse}ifelse}ifelse}executeonly]def"""
562# t1write.c:saveStdSubrs
563std_subrs = [
564	# 3 0 callother pop pop setcurrentpoint return
565	b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b",
566	# 0 1 callother return
567	b"\x8b\x8c\x0c\x10\x0b",
568	# 0 2 callother return
569	b"\x8b\x8d\x0c\x10\x0b",
570	# return
571	b"\x0b",
572	# 3 1 3 callother pop callsubr return
573	b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b"
574]
575# follow t1write.c:writeRegNameKeyedFont
576eexec_IV = b"cccc"
577char_IV = b"\x0c\x0c\x0c\x0c"
578RD_value = ("string", "currentfile", "exch", "readstring", "pop")
579ND_value = ("def",)
580PD_value = ("put",)
581