1999-12-16 21:34:53 +00:00
|
|
|
"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts
|
|
|
|
|
|
|
|
Functions for reading and writing raw Type 1 data:
|
|
|
|
|
|
|
|
read(path)
|
|
|
|
reads any Type 1 font file, returns the raw data and a type indicator:
|
|
|
|
'LWFN', 'PFB' or 'OTHER', depending on the format of the file pointed
|
|
|
|
to by 'path'.
|
|
|
|
Raises an error when the file does not contain valid Type 1 data.
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
write(path, data, kind='OTHER', dohex=0)
|
1999-12-16 21:34:53 +00:00
|
|
|
writes raw Type 1 data to the file pointed to by 'path'.
|
|
|
|
'kind' can be one of 'LWFN', 'PFB' or 'OTHER'; it defaults to 'OTHER'.
|
|
|
|
'dohex' is a flag which determines whether the eexec encrypted
|
|
|
|
part should be written as hexadecimal or binary, but only if kind
|
|
|
|
is 'LWFN' or 'PFB'.
|
|
|
|
"""
|
|
|
|
|
|
|
|
__author__ = "jvr"
|
|
|
|
__version__ = "1.0b2"
|
|
|
|
DEBUG = 0
|
|
|
|
|
2000-01-12 19:15:57 +00:00
|
|
|
from fontTools.misc import eexec
|
1999-12-16 21:34:53 +00:00
|
|
|
import string
|
|
|
|
import re
|
|
|
|
import os
|
|
|
|
|
2002-07-12 19:20:19 +00:00
|
|
|
|
|
|
|
try:
|
2002-05-03 19:38:07 +00:00
|
|
|
try:
|
|
|
|
from Carbon import Res
|
|
|
|
except ImportError:
|
|
|
|
import Res # MacPython < 2.2
|
2002-07-23 09:25:42 +00:00
|
|
|
except ImportError:
|
|
|
|
haveMacSupport = 0
|
|
|
|
else:
|
|
|
|
haveMacSupport = 1
|
2003-06-07 15:15:51 +00:00
|
|
|
import MacOS
|
2003-05-24 12:34:11 +00:00
|
|
|
|
1999-12-16 21:34:53 +00:00
|
|
|
|
2002-07-23 09:25:42 +00:00
|
|
|
class T1Error(Exception): pass
|
1999-12-16 21:34:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
class T1Font:
|
|
|
|
|
2000-03-28 10:38:43 +00:00
|
|
|
"""Type 1 font class.
|
|
|
|
|
|
|
|
Uses a minimal interpeter that supports just about enough PS to parse
|
|
|
|
Type 1 fonts.
|
1999-12-16 21:34:53 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self, path=None):
|
|
|
|
if path is not None:
|
|
|
|
self.data, type = read(path)
|
|
|
|
else:
|
|
|
|
pass # XXX
|
|
|
|
|
|
|
|
def saveAs(self, path, type):
|
2001-08-16 10:34:22 +00:00
|
|
|
write(path, self.getData(), type)
|
1999-12-16 21:34:53 +00:00
|
|
|
|
|
|
|
def getData(self):
|
2000-03-28 10:38:43 +00:00
|
|
|
# XXX Todo: if the data has been converted to Python object,
|
|
|
|
# recreate the PS stream
|
1999-12-16 21:34:53 +00:00
|
|
|
return self.data
|
|
|
|
|
2003-08-25 17:53:29 +00:00
|
|
|
def getGlyphSet(self):
|
|
|
|
"""Return a generic GlyphSet, which is a dict-like object
|
|
|
|
mapping glyph names to glyph objects. The returned glyph objects
|
|
|
|
have a .draw() method that supports the Pen protocol, and will
|
|
|
|
have an attribute named 'width', but only *after* the .draw() method
|
|
|
|
has been called.
|
|
|
|
|
|
|
|
In the case of Type 1, the GlyphSet is simply the CharStrings dict.
|
|
|
|
"""
|
|
|
|
return self["CharStrings"]
|
|
|
|
|
1999-12-16 21:34:53 +00:00
|
|
|
def __getitem__(self, key):
|
|
|
|
if not hasattr(self, "font"):
|
|
|
|
self.parse()
|
2000-03-28 10:38:43 +00:00
|
|
|
return self.font[key]
|
1999-12-16 21:34:53 +00:00
|
|
|
|
|
|
|
def parse(self):
|
2000-01-16 22:14:02 +00:00
|
|
|
from fontTools.misc import psLib
|
|
|
|
from fontTools.misc import psCharStrings
|
1999-12-16 21:34:53 +00:00
|
|
|
self.font = psLib.suckfont(self.data)
|
|
|
|
charStrings = self.font["CharStrings"]
|
|
|
|
lenIV = self.font["Private"].get("lenIV", 4)
|
|
|
|
assert lenIV >= 0
|
2003-08-24 19:56:16 +00:00
|
|
|
subrs = self.font["Private"]["Subrs"]
|
1999-12-16 21:34:53 +00:00
|
|
|
for glyphName, charString in charStrings.items():
|
2000-01-12 19:15:57 +00:00
|
|
|
charString, R = eexec.decrypt(charString, 4330)
|
2003-08-24 19:56:16 +00:00
|
|
|
charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:],
|
|
|
|
subrs=subrs)
|
1999-12-16 21:34:53 +00:00
|
|
|
for i in range(len(subrs)):
|
2000-01-12 19:15:57 +00:00
|
|
|
charString, R = eexec.decrypt(subrs[i], 4330)
|
2003-08-24 19:56:16 +00:00
|
|
|
subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
|
1999-12-16 21:34:53 +00:00
|
|
|
del self.data
|
|
|
|
|
|
|
|
|
2000-03-28 10:38:43 +00:00
|
|
|
# low level T1 data read and write functions
|
1999-12-16 21:34:53 +00:00
|
|
|
|
2005-01-25 19:06:51 +00:00
|
|
|
def read(path, onlyHeader=0):
|
1999-12-16 21:34:53 +00:00
|
|
|
"""reads any Type 1 font file, returns raw data"""
|
|
|
|
normpath = string.lower(path)
|
2002-07-12 19:20:19 +00:00
|
|
|
if haveMacSupport:
|
2003-06-07 15:15:51 +00:00
|
|
|
creator, type = MacOS.GetCreatorAndType(path)
|
1999-12-16 21:34:53 +00:00
|
|
|
if type == 'LWFN':
|
2005-01-25 19:06:51 +00:00
|
|
|
return readLWFN(path, onlyHeader), 'LWFN'
|
1999-12-16 21:34:53 +00:00
|
|
|
if normpath[-4:] == '.pfb':
|
2005-01-25 19:06:51 +00:00
|
|
|
return readPFB(path, onlyHeader), 'PFB'
|
1999-12-16 21:34:53 +00:00
|
|
|
else:
|
2001-06-24 15:12:48 +00:00
|
|
|
return readOther(path), 'OTHER'
|
1999-12-16 21:34:53 +00:00
|
|
|
|
|
|
|
def write(path, data, kind='OTHER', dohex=0):
|
2001-06-24 15:12:48 +00:00
|
|
|
assertType1(data)
|
1999-12-16 21:34:53 +00:00
|
|
|
kind = string.upper(kind)
|
|
|
|
try:
|
|
|
|
os.remove(path)
|
|
|
|
except os.error:
|
|
|
|
pass
|
|
|
|
err = 1
|
|
|
|
try:
|
|
|
|
if kind == 'LWFN':
|
2001-06-24 15:12:48 +00:00
|
|
|
writeLWFN(path, data)
|
1999-12-16 21:34:53 +00:00
|
|
|
elif kind == 'PFB':
|
2001-06-24 15:12:48 +00:00
|
|
|
writePFB(path, data)
|
1999-12-16 21:34:53 +00:00
|
|
|
else:
|
2001-06-24 15:12:48 +00:00
|
|
|
writeOther(path, data, dohex)
|
1999-12-16 21:34:53 +00:00
|
|
|
err = 0
|
|
|
|
finally:
|
|
|
|
if err and not DEBUG:
|
|
|
|
try:
|
|
|
|
os.remove(path)
|
|
|
|
except os.error:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# -- internal --
|
|
|
|
|
|
|
|
LWFNCHUNKSIZE = 2000
|
|
|
|
HEXLINELENGTH = 80
|
|
|
|
|
|
|
|
|
2002-07-29 21:33:46 +00:00
|
|
|
def readLWFN(path, onlyHeader=0):
|
1999-12-16 21:34:53 +00:00
|
|
|
"""reads an LWFN font file, returns raw data"""
|
2001-07-30 19:04:40 +00:00
|
|
|
resRef = Res.FSpOpenResFile(path, 1) # read-only
|
1999-12-16 21:34:53 +00:00
|
|
|
try:
|
2001-07-30 19:04:40 +00:00
|
|
|
Res.UseResFile(resRef)
|
1999-12-16 21:34:53 +00:00
|
|
|
n = Res.Count1Resources('POST')
|
|
|
|
data = []
|
|
|
|
for i in range(501, 501 + n):
|
|
|
|
res = Res.Get1Resource('POST', i)
|
|
|
|
code = ord(res.data[0])
|
|
|
|
if ord(res.data[1]) <> 0:
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, 'corrupt LWFN file'
|
1999-12-16 21:34:53 +00:00
|
|
|
if code in [1, 2]:
|
2002-07-29 21:33:46 +00:00
|
|
|
if onlyHeader and code == 2:
|
|
|
|
break
|
2002-07-29 21:39:06 +00:00
|
|
|
data.append(res.data[2:])
|
1999-12-16 21:34:53 +00:00
|
|
|
elif code in [3, 5]:
|
|
|
|
break
|
|
|
|
elif code == 4:
|
|
|
|
f = open(path, "rb")
|
|
|
|
data.append(f.read())
|
|
|
|
f.close()
|
|
|
|
elif code == 0:
|
|
|
|
pass # comment, ignore
|
|
|
|
else:
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, 'bad chunk code: ' + `code`
|
1999-12-16 21:34:53 +00:00
|
|
|
finally:
|
2001-07-30 19:04:40 +00:00
|
|
|
Res.CloseResFile(resRef)
|
1999-12-16 21:34:53 +00:00
|
|
|
data = string.join(data, '')
|
2001-06-24 15:12:48 +00:00
|
|
|
assertType1(data)
|
1999-12-16 21:34:53 +00:00
|
|
|
return data
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def readPFB(path, onlyHeader=0):
|
1999-12-16 21:34:53 +00:00
|
|
|
"""reads a PFB font file, returns raw data"""
|
|
|
|
f = open(path, "rb")
|
|
|
|
data = []
|
|
|
|
while 1:
|
|
|
|
if f.read(1) <> chr(128):
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, 'corrupt PFB file'
|
1999-12-16 21:34:53 +00:00
|
|
|
code = ord(f.read(1))
|
|
|
|
if code in [1, 2]:
|
2001-06-24 15:12:48 +00:00
|
|
|
chunklen = stringToLong(f.read(4))
|
2000-03-28 10:38:43 +00:00
|
|
|
chunk = f.read(chunklen)
|
|
|
|
assert len(chunk) == chunklen
|
|
|
|
data.append(chunk)
|
1999-12-16 21:34:53 +00:00
|
|
|
elif code == 3:
|
|
|
|
break
|
|
|
|
else:
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, 'bad chunk code: ' + `code`
|
2001-06-24 15:12:48 +00:00
|
|
|
if onlyHeader:
|
|
|
|
break
|
1999-12-16 21:34:53 +00:00
|
|
|
f.close()
|
|
|
|
data = string.join(data, '')
|
2001-06-24 15:12:48 +00:00
|
|
|
assertType1(data)
|
1999-12-16 21:34:53 +00:00
|
|
|
return data
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def readOther(path):
|
1999-12-16 21:34:53 +00:00
|
|
|
"""reads any (font) file, returns raw data"""
|
|
|
|
f = open(path, "rb")
|
|
|
|
data = f.read()
|
|
|
|
f.close()
|
2001-06-24 15:12:48 +00:00
|
|
|
assertType1(data)
|
1999-12-16 21:34:53 +00:00
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
chunks = findEncryptedChunks(data)
|
1999-12-16 21:34:53 +00:00
|
|
|
data = []
|
2001-06-24 15:12:48 +00:00
|
|
|
for isEncrypted, chunk in chunks:
|
|
|
|
if isEncrypted and isHex(chunk[:4]):
|
|
|
|
data.append(deHexString(chunk))
|
1999-12-16 21:34:53 +00:00
|
|
|
else:
|
|
|
|
data.append(chunk)
|
|
|
|
return string.join(data, '')
|
|
|
|
|
|
|
|
# file writing tools
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def writeLWFN(path, data):
|
2001-07-30 19:04:40 +00:00
|
|
|
Res.FSpCreateResFile(path, "just", "LWFN", 0)
|
|
|
|
resRef = Res.FSpOpenResFile(path, 2) # write-only
|
1999-12-16 21:34:53 +00:00
|
|
|
try:
|
2001-07-30 19:04:40 +00:00
|
|
|
Res.UseResFile(resRef)
|
1999-12-16 21:34:53 +00:00
|
|
|
resID = 501
|
2001-06-24 15:12:48 +00:00
|
|
|
chunks = findEncryptedChunks(data)
|
|
|
|
for isEncrypted, chunk in chunks:
|
|
|
|
if isEncrypted:
|
1999-12-16 21:34:53 +00:00
|
|
|
code = 2
|
|
|
|
else:
|
|
|
|
code = 1
|
|
|
|
while chunk:
|
|
|
|
res = Res.Resource(chr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2])
|
|
|
|
res.AddResource('POST', resID, '')
|
|
|
|
chunk = chunk[LWFNCHUNKSIZE - 2:]
|
|
|
|
resID = resID + 1
|
|
|
|
res = Res.Resource(chr(5) + '\0')
|
|
|
|
res.AddResource('POST', resID, '')
|
|
|
|
finally:
|
2001-07-30 19:04:40 +00:00
|
|
|
Res.CloseResFile(resRef)
|
1999-12-16 21:34:53 +00:00
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def writePFB(path, data):
|
|
|
|
chunks = findEncryptedChunks(data)
|
2000-03-28 10:38:43 +00:00
|
|
|
f = open(path, "wb")
|
1999-12-16 21:34:53 +00:00
|
|
|
try:
|
2001-06-24 15:12:48 +00:00
|
|
|
for isEncrypted, chunk in chunks:
|
|
|
|
if isEncrypted:
|
1999-12-16 21:34:53 +00:00
|
|
|
code = 2
|
|
|
|
else:
|
|
|
|
code = 1
|
|
|
|
f.write(chr(128) + chr(code))
|
2001-06-24 15:12:48 +00:00
|
|
|
f.write(longToString(len(chunk)))
|
1999-12-16 21:34:53 +00:00
|
|
|
f.write(chunk)
|
|
|
|
f.write(chr(128) + chr(3))
|
|
|
|
finally:
|
|
|
|
f.close()
|
2002-07-12 19:20:19 +00:00
|
|
|
if haveMacSupport:
|
2003-06-07 15:15:51 +00:00
|
|
|
MacOS.SetCreatorAndType(path, 'mdos', 'BINA')
|
1999-12-16 21:34:53 +00:00
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def writeOther(path, data, dohex = 0):
|
|
|
|
chunks = findEncryptedChunks(data)
|
1999-12-16 21:34:53 +00:00
|
|
|
f = open(path, "wb")
|
|
|
|
try:
|
|
|
|
hexlinelen = HEXLINELENGTH / 2
|
2001-06-24 15:12:48 +00:00
|
|
|
for isEncrypted, chunk in chunks:
|
|
|
|
if isEncrypted:
|
1999-12-16 21:34:53 +00:00
|
|
|
code = 2
|
|
|
|
else:
|
|
|
|
code = 1
|
|
|
|
if code == 2 and dohex:
|
|
|
|
while chunk:
|
2000-01-12 19:15:57 +00:00
|
|
|
f.write(eexec.hexString(chunk[:hexlinelen]))
|
1999-12-16 21:34:53 +00:00
|
|
|
f.write('\r')
|
|
|
|
chunk = chunk[hexlinelen:]
|
|
|
|
else:
|
|
|
|
f.write(chunk)
|
|
|
|
finally:
|
|
|
|
f.close()
|
2002-07-12 19:20:19 +00:00
|
|
|
if haveMacSupport:
|
2003-06-07 15:15:51 +00:00
|
|
|
MacOS.SetCreatorAndType(path, 'R*ch', 'TEXT') # BBEdit text file
|
1999-12-16 21:34:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
# decryption tools
|
|
|
|
|
|
|
|
EEXECBEGIN = "currentfile eexec"
|
|
|
|
EEXECEND = '0' * 64
|
|
|
|
EEXECINTERNALEND = "currentfile closefile"
|
|
|
|
EEXECBEGINMARKER = "%-- eexec start\r"
|
|
|
|
EEXECENDMARKER = "%-- eexec end\r"
|
|
|
|
|
|
|
|
_ishexRE = re.compile('[0-9A-Fa-f]*$')
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def isHex(text):
|
1999-12-16 21:34:53 +00:00
|
|
|
return _ishexRE.match(text) is not None
|
|
|
|
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def decryptType1(data):
|
|
|
|
chunks = findEncryptedChunks(data)
|
1999-12-16 21:34:53 +00:00
|
|
|
data = []
|
2001-06-24 15:12:48 +00:00
|
|
|
for isEncrypted, chunk in chunks:
|
|
|
|
if isEncrypted:
|
|
|
|
if isHex(chunk[:4]):
|
|
|
|
chunk = deHexString(chunk)
|
2000-01-12 19:15:57 +00:00
|
|
|
decrypted, R = eexec.decrypt(chunk, 55665)
|
1999-12-16 21:34:53 +00:00
|
|
|
decrypted = decrypted[4:]
|
|
|
|
if decrypted[-len(EEXECINTERNALEND)-1:-1] <> EEXECINTERNALEND \
|
|
|
|
and decrypted[-len(EEXECINTERNALEND)-2:-2] <> EEXECINTERNALEND:
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, "invalid end of eexec part"
|
1999-12-16 21:34:53 +00:00
|
|
|
decrypted = decrypted[:-len(EEXECINTERNALEND)-2] + '\r'
|
|
|
|
data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
|
|
|
|
else:
|
|
|
|
if chunk[-len(EEXECBEGIN)-1:-1] == EEXECBEGIN:
|
|
|
|
data.append(chunk[:-len(EEXECBEGIN)-1])
|
|
|
|
else:
|
|
|
|
data.append(chunk)
|
|
|
|
return string.join(data, '')
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def findEncryptedChunks(data):
|
1999-12-16 21:34:53 +00:00
|
|
|
chunks = []
|
|
|
|
while 1:
|
2001-06-24 15:12:48 +00:00
|
|
|
eBegin = string.find(data, EEXECBEGIN)
|
|
|
|
if eBegin < 0:
|
1999-12-16 21:34:53 +00:00
|
|
|
break
|
2001-11-28 12:22:15 +00:00
|
|
|
eBegin = eBegin + len(EEXECBEGIN) + 1
|
2001-06-24 15:12:48 +00:00
|
|
|
eEnd = string.find(data, EEXECEND, eBegin)
|
|
|
|
if eEnd < 0:
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, "can't find end of eexec part"
|
2002-07-23 14:54:47 +00:00
|
|
|
cypherText = data[eBegin:eEnd + 2]
|
|
|
|
plainText, R = eexec.decrypt(cypherText, 55665)
|
|
|
|
eEndLocal = string.find(plainText, EEXECINTERNALEND)
|
|
|
|
if eEndLocal < 0:
|
|
|
|
raise T1Error, "can't find end of eexec part"
|
|
|
|
eEnd = eBegin + eEndLocal + len(EEXECINTERNALEND) + 1
|
2001-11-28 12:22:15 +00:00
|
|
|
chunks.append((0, data[:eBegin]))
|
|
|
|
chunks.append((1, data[eBegin:eEnd]))
|
2001-06-24 15:12:48 +00:00
|
|
|
data = data[eEnd:]
|
1999-12-16 21:34:53 +00:00
|
|
|
chunks.append((0, data))
|
|
|
|
return chunks
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def deHexString(hexstring):
|
2000-01-12 19:15:57 +00:00
|
|
|
return eexec.deHexString(string.join(string.split(hexstring), ""))
|
1999-12-16 21:34:53 +00:00
|
|
|
|
|
|
|
|
|
|
|
# Type 1 assertion
|
|
|
|
|
|
|
|
_fontType1RE = re.compile(r"/FontType\s+1\s+def")
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def assertType1(data):
|
1999-12-16 21:34:53 +00:00
|
|
|
for head in ['%!PS-AdobeFont', '%!FontType1-1.0']:
|
|
|
|
if data[:len(head)] == head:
|
|
|
|
break
|
|
|
|
else:
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, "not a PostScript font"
|
1999-12-16 21:34:53 +00:00
|
|
|
if not _fontType1RE.search(data):
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, "not a Type 1 font"
|
1999-12-16 21:34:53 +00:00
|
|
|
if string.find(data, "currentfile eexec") < 0:
|
2002-07-23 09:25:42 +00:00
|
|
|
raise T1Error, "not an encrypted Type 1 font"
|
1999-12-16 21:34:53 +00:00
|
|
|
# XXX what else?
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
# pfb helpers
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def longToString(long):
|
1999-12-16 21:34:53 +00:00
|
|
|
str = ""
|
|
|
|
for i in range(4):
|
|
|
|
str = str + chr((long & (0xff << (i * 8))) >> i * 8)
|
|
|
|
return str
|
|
|
|
|
2001-06-24 15:12:48 +00:00
|
|
|
def stringToLong(str):
|
1999-12-16 21:34:53 +00:00
|
|
|
if len(str) <> 4:
|
|
|
|
raise ValueError, 'string must be 4 bytes long'
|
|
|
|
long = 0
|
|
|
|
for i in range(4):
|
|
|
|
long = long + (ord(str[i]) << (i * 8))
|
|
|
|
return long
|
2001-06-24 15:12:48 +00:00
|
|
|
|