Home | History | Annotate | Download | only in fontTools
      1 """fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts
      2 
      3 Functions for reading and writing raw Type 1 data:
      4 
      5 read(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 
     11 write(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 'LWFN' or 'PFB'.
     17 """
     18 from __future__ import print_function, division, absolute_import
     19 from fontTools.misc.py23 import *
     20 from fontTools.misc import eexec
     21 from fontTools.misc.macCreatorType import getMacCreatorAndType
     22 import os
     23 import re
     24 
     25 __author__ = "jvr"
     26 __version__ = "1.0b2"
     27 DEBUG = 0
     28 
     29 
     30 try:
     31 	try:
     32 		from Carbon import Res
     33 	except ImportError:
     34 		import Res  # MacPython < 2.2
     35 except ImportError:
     36 	haveMacSupport = 0
     37 else:
     38 	haveMacSupport = 1
     39 	import MacOS
     40 	
     41 
     42 class T1Error(Exception): pass
     43 
     44 
     45 class T1Font(object):
     46 	
     47 	"""Type 1 font class.
     48 	
     49 	Uses a minimal interpeter that supports just about enough PS to parse
     50 	Type 1 fonts.
     51 	"""
     52 	
     53 	def __init__(self, path=None):
     54 		if path is not None:
     55 			self.data, type = read(path)
     56 		else:
     57 			pass # XXX
     58 	
     59 	def saveAs(self, path, type):
     60 		write(path, self.getData(), type)
     61 	
     62 	def getData(self):
     63 		# XXX Todo: if the data has been converted to Python object,
     64 		# recreate the PS stream
     65 		return self.data
     66 	
     67 	def getGlyphSet(self):
     68 		"""Return a generic GlyphSet, which is a dict-like object
     69 		mapping glyph names to glyph objects. The returned glyph objects
     70 		have a .draw() method that supports the Pen protocol, and will
     71 		have an attribute named 'width', but only *after* the .draw() method
     72 		has been called.
     73 		
     74 		In the case of Type 1, the GlyphSet is simply the CharStrings dict.
     75 		"""
     76 		return self["CharStrings"]
     77 	
     78 	def __getitem__(self, key):
     79 		if not hasattr(self, "font"):
     80 			self.parse()
     81 		return self.font[key]
     82 	
     83 	def parse(self):
     84 		from fontTools.misc import psLib
     85 		from fontTools.misc import psCharStrings
     86 		self.font = psLib.suckfont(self.data)
     87 		charStrings = self.font["CharStrings"]
     88 		lenIV = self.font["Private"].get("lenIV", 4)
     89 		assert lenIV >= 0
     90 		subrs = self.font["Private"]["Subrs"]
     91 		for glyphName, charString in charStrings.items():
     92 			charString, R = eexec.decrypt(charString, 4330)
     93 			charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:],
     94 					subrs=subrs)
     95 		for i in range(len(subrs)):
     96 			charString, R = eexec.decrypt(subrs[i], 4330)
     97 			subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
     98 		del self.data
     99 
    100 
    101 # low level T1 data read and write functions
    102 
    103 def read(path, onlyHeader=False):
    104 	"""reads any Type 1 font file, returns raw data"""
    105 	normpath = path.lower()
    106 	creator, typ = getMacCreatorAndType(path)
    107 	if typ == 'LWFN':
    108 		return readLWFN(path, onlyHeader), 'LWFN'
    109 	if normpath[-4:] == '.pfb':
    110 		return readPFB(path, onlyHeader), 'PFB'
    111 	else:
    112 		return readOther(path), 'OTHER'
    113 
    114 def write(path, data, kind='OTHER', dohex=False):
    115 	assertType1(data)
    116 	kind = kind.upper()
    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 
    140 LWFNCHUNKSIZE = 2000
    141 HEXLINELENGTH = 80
    142 
    143 
    144 def readLWFN(path, onlyHeader=False):
    145 	"""reads an LWFN font file, returns raw data"""
    146 	resRef = Res.FSOpenResFile(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 = byteord(res.data[0])
    154 			if byteord(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: ' + repr(code))
    170 	finally:
    171 		Res.CloseResFile(resRef)
    172 	data = bytesjoin(data)
    173 	assertType1(data)
    174 	return data
    175 
    176 def readPFB(path, onlyHeader=False):
    177 	"""reads a PFB font file, returns raw data"""
    178 	f = open(path, "rb")
    179 	data = []
    180 	while True:
    181 		if f.read(1) != bytechr(128):
    182 			raise T1Error('corrupt PFB file')
    183 		code = byteord(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: ' + repr(code))
    193 		if onlyHeader:
    194 			break
    195 	f.close()
    196 	data = bytesjoin(data)
    197 	assertType1(data)
    198 	return data
    199 
    200 def 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 bytesjoin(data)
    215 
    216 # file writing tools
    217 
    218 def writeLWFN(path, data):
    219 	Res.FSpCreateResFile(path, "just", "LWFN", 0)
    220 	resRef = Res.FSOpenResFile(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(bytechr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
    232 				res.AddResource('POST', resID, '')
    233 				chunk = chunk[LWFNCHUNKSIZE - 2:]
    234 				resID = resID + 1
    235 		res = Res.Resource(bytechr(5) + '\0')
    236 		res.AddResource('POST', resID, '')
    237 	finally:
    238 		Res.CloseResFile(resRef)
    239 
    240 def 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(bytechr(128) + bytechr(code))
    250 			f.write(longToString(len(chunk)))
    251 			f.write(chunk)
    252 		f.write(bytechr(128) + bytechr(3))
    253 	finally:
    254 		f.close()
    255 
    256 def writeOther(path, data, dohex=False):
    257 	chunks = findEncryptedChunks(data)
    258 	f = open(path, "wb")
    259 	try:
    260 		hexlinelen = HEXLINELENGTH // 2
    261 		for isEncrypted, chunk in chunks:
    262 			if isEncrypted:
    263 				code = 2
    264 			else:
    265 				code = 1
    266 			if code == 2 and dohex:
    267 				while chunk:
    268 					f.write(eexec.hexString(chunk[:hexlinelen]))
    269 					f.write('\r')
    270 					chunk = chunk[hexlinelen:]
    271 			else:
    272 				f.write(chunk)
    273 	finally:
    274 		f.close()
    275 
    276 
    277 # decryption tools
    278 
    279 EEXECBEGIN = "currentfile eexec"
    280 EEXECEND = '0' * 64
    281 EEXECINTERNALEND = "currentfile closefile"
    282 EEXECBEGINMARKER = "%-- eexec start\r"
    283 EEXECENDMARKER = "%-- eexec end\r"
    284 
    285 _ishexRE = re.compile('[0-9A-Fa-f]*$')
    286 
    287 def isHex(text):
    288 	return _ishexRE.match(text) is not None
    289 
    290 
    291 def decryptType1(data):
    292 	chunks = findEncryptedChunks(data)
    293 	data = []
    294 	for isEncrypted, chunk in chunks:
    295 		if isEncrypted:
    296 			if isHex(chunk[:4]):
    297 				chunk = deHexString(chunk)
    298 			decrypted, R = eexec.decrypt(chunk, 55665)
    299 			decrypted = decrypted[4:]
    300 			if decrypted[-len(EEXECINTERNALEND)-1:-1] != EEXECINTERNALEND \
    301 					and decrypted[-len(EEXECINTERNALEND)-2:-2] != EEXECINTERNALEND:
    302 				raise T1Error("invalid end of eexec part")
    303 			decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r'
    304 			data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
    305 		else:
    306 			if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
    307 				data.append(chunk[:-len(EEXECBEGIN)-1])
    308 			else:
    309 				data.append(chunk)
    310 	return bytesjoin(data)
    311 
    312 def findEncryptedChunks(data):
    313 	chunks = []
    314 	while True:
    315 		eBegin = data.find(EEXECBEGIN)
    316 		if eBegin < 0:
    317 			break
    318 		eBegin = eBegin + len(EEXECBEGIN) + 1
    319 		eEnd = data.find(EEXECEND, eBegin)
    320 		if eEnd < 0:
    321 			raise T1Error("can't find end of eexec part")
    322 		cypherText = data[eBegin:eEnd + 2]
    323 		if isHex(cypherText[:4]):
    324 			cypherText = deHexString(cypherText)
    325 		plainText, R = eexec.decrypt(cypherText, 55665)
    326 		eEndLocal = plainText.find(EEXECINTERNALEND)
    327 		if eEndLocal < 0:
    328 			raise T1Error("can't find end of eexec part")
    329 		chunks.append((0, data[:eBegin]))
    330 		chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1]))
    331 		data = data[eEnd:]
    332 	chunks.append((0, data))
    333 	return chunks
    334 
    335 def deHexString(hexstring):
    336 	return eexec.deHexString(strjoin(hexstring.split()))
    337 
    338 
    339 # Type 1 assertion
    340 
    341 _fontType1RE = re.compile(br"/FontType\s+1\s+def")
    342 
    343 def assertType1(data):
    344 	for head in [b'%!PS-AdobeFont', b'%!FontType1']:
    345 		if data[:len(head)] == head:
    346 			break
    347 	else:
    348 		raise T1Error("not a PostScript font")
    349 	if not _fontType1RE.search(data):
    350 		raise T1Error("not a Type 1 font")
    351 	if data.find(b"currentfile eexec") < 0:
    352 		raise T1Error("not an encrypted Type 1 font")
    353 	# XXX what else?
    354 	return data
    355 
    356 
    357 # pfb helpers
    358 
    359 def longToString(long):
    360 	s = ""
    361 	for i in range(4):
    362 		s += bytechr((long & (0xff << (i * 8))) >> i * 8)
    363 	return s
    364 
    365 def stringToLong(s):
    366 	if len(s) != 4:
    367 		raise ValueError('string must be 4 bytes long')
    368 	l = 0
    369 	for i in range(4):
    370 		l += byteord(s[i]) << (i * 8)
    371 	return l
    372 
    373