git-svn-id: svn://svn.code.sf.net/p/fonttools/code/trunk@2 4cde692c-a291-49d1-8350-778aa11640f8
348 lines
8.0 KiB
Python
348 lines
8.0 KiB
Python
"""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.
|
|
|
|
write(path, data, kind = 'OTHER', dohex = 0)
|
|
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
|
|
|
|
import eexec
|
|
import string
|
|
import re
|
|
import os
|
|
|
|
if os.name == 'mac':
|
|
import Res
|
|
import macfs
|
|
|
|
error = 't1Lib.error'
|
|
|
|
# work in progress
|
|
|
|
|
|
class T1Font:
|
|
|
|
"""Type 1 font class.
|
|
XXX This is work in progress! For now just use the read()
|
|
and write() functions as described above, they are stable.
|
|
"""
|
|
|
|
def __init__(self, path=None):
|
|
if path is not None:
|
|
self.data, type = read(path)
|
|
else:
|
|
pass # XXX
|
|
|
|
def saveAs(self, path, type):
|
|
self.write(path, self.getData(), type)
|
|
|
|
def getData(self):
|
|
return self.data
|
|
|
|
def __getitem__(self, key):
|
|
if not hasattr(self, "font"):
|
|
self.parse()
|
|
return self.font[key]
|
|
else:
|
|
return self.font[key]
|
|
|
|
def parse(self):
|
|
import psLib
|
|
import psCharStrings
|
|
self.font = psLib.suckfont(self.data)
|
|
charStrings = self.font["CharStrings"]
|
|
lenIV = self.font["Private"].get("lenIV", 4)
|
|
assert lenIV >= 0
|
|
for glyphName, charString in charStrings.items():
|
|
charString, R = eexec.Decrypt(charString, 4330)
|
|
charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:])
|
|
subrs = self.font["Private"]["Subrs"]
|
|
for i in range(len(subrs)):
|
|
charString, R = eexec.Decrypt(subrs[i], 4330)
|
|
subrs[i] = psCharStrings.T1CharString(charString[lenIV:])
|
|
del self.data
|
|
|
|
|
|
|
|
# public functions
|
|
|
|
def read(path):
|
|
"""reads any Type 1 font file, returns raw data"""
|
|
normpath = string.lower(path)
|
|
if os.name == 'mac':
|
|
fss = macfs.FSSpec(path)
|
|
creator, type = fss.GetCreatorType()
|
|
if type == 'LWFN':
|
|
return readlwfn(path), 'LWFN'
|
|
if normpath[-4:] == '.pfb':
|
|
return readpfb(path), 'PFB'
|
|
else:
|
|
return readother(path), 'OTHER'
|
|
|
|
def write(path, data, kind='OTHER', dohex=0):
|
|
asserttype1(data)
|
|
kind = string.upper(kind)
|
|
try:
|
|
os.remove(path)
|
|
except os.error:
|
|
pass
|
|
err = 1
|
|
try:
|
|
if kind == 'LWFN':
|
|
writelwfn(path, data)
|
|
elif kind == 'PFB':
|
|
writepfb(path, data)
|
|
else:
|
|
writeother(path, data, dohex)
|
|
err = 0
|
|
finally:
|
|
if err and not DEBUG:
|
|
try:
|
|
os.remove(path)
|
|
except os.error:
|
|
pass
|
|
|
|
|
|
# -- internal --
|
|
|
|
LWFNCHUNKSIZE = 2000
|
|
HEXLINELENGTH = 80
|
|
|
|
|
|
def readlwfn(path):
|
|
"""reads an LWFN font file, returns raw data"""
|
|
resref = Res.OpenResFile(path)
|
|
try:
|
|
Res.UseResFile(resref)
|
|
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:
|
|
raise error, 'corrupt LWFN file'
|
|
if code in [1, 2]:
|
|
data.append(res.data[2:])
|
|
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:
|
|
raise error, 'bad chunk code: ' + `code`
|
|
finally:
|
|
Res.CloseResFile(resref)
|
|
data = string.join(data, '')
|
|
asserttype1(data)
|
|
return data
|
|
|
|
def readpfb(path):
|
|
"""reads a PFB font file, returns raw data"""
|
|
f = open(path, "rb")
|
|
data = []
|
|
while 1:
|
|
if f.read(1) <> chr(128):
|
|
raise error, 'corrupt PFB file'
|
|
code = ord(f.read(1))
|
|
if code in [1, 2]:
|
|
chunklen = string2long(f.read(4))
|
|
data.append(f.read(chunklen))
|
|
elif code == 3:
|
|
break
|
|
else:
|
|
raise error, 'bad chunk code: ' + `code`
|
|
f.close()
|
|
data = string.join(data, '')
|
|
asserttype1(data)
|
|
return data
|
|
|
|
def readother(path):
|
|
"""reads any (font) file, returns raw data"""
|
|
f = open(path, "rb")
|
|
data = f.read()
|
|
f.close()
|
|
asserttype1(data)
|
|
|
|
chunks = findencryptedchunks(data)
|
|
data = []
|
|
for isencrypted, chunk in chunks:
|
|
if isencrypted and ishex(chunk[:4]):
|
|
data.append(dehexstring(chunk))
|
|
else:
|
|
data.append(chunk)
|
|
return string.join(data, '')
|
|
|
|
# file writing tools
|
|
|
|
def writelwfn(path, data):
|
|
Res.CreateResFile(path)
|
|
fss = macfs.FSSpec(path)
|
|
fss.SetCreatorType('just', 'LWFN')
|
|
resref = Res.OpenResFile(path)
|
|
try:
|
|
Res.UseResFile(resref)
|
|
resID = 501
|
|
chunks = findencryptedchunks(data)
|
|
for isencrypted, chunk in chunks:
|
|
if isencrypted:
|
|
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:
|
|
Res.CloseResFile(resref)
|
|
|
|
def writepfb(path, data):
|
|
chunks = findencryptedchunks(data)
|
|
f = open(dstpath, "wb")
|
|
try:
|
|
for isencrypted, chunk in chunks:
|
|
if isencrypted:
|
|
code = 2
|
|
else:
|
|
code = 1
|
|
f.write(chr(128) + chr(code))
|
|
f.write(long2string(len(chunk)))
|
|
f.write(chunk)
|
|
f.write(chr(128) + chr(3))
|
|
finally:
|
|
f.close()
|
|
if os.name == 'mac':
|
|
fss = macfs.FSSpec(dstpath)
|
|
fss.SetCreatorType('mdos', 'BINA')
|
|
|
|
def writeother(path, data, dohex = 0):
|
|
chunks = findencryptedchunks(data)
|
|
f = open(path, "wb")
|
|
try:
|
|
hexlinelen = HEXLINELENGTH / 2
|
|
for isencrypted, chunk in chunks:
|
|
if isencrypted:
|
|
code = 2
|
|
else:
|
|
code = 1
|
|
if code == 2 and dohex:
|
|
while chunk:
|
|
f.write(eexec.hexstring(chunk[:hexlinelen]))
|
|
f.write('\r')
|
|
chunk = chunk[hexlinelen:]
|
|
else:
|
|
f.write(chunk)
|
|
finally:
|
|
f.close()
|
|
if os.name == 'mac':
|
|
fss = macfs.FSSpec(path)
|
|
fss.SetCreatorType('R*ch', 'TEXT') # BBEdit text file
|
|
|
|
|
|
# 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]*$')
|
|
|
|
def ishex(text):
|
|
return _ishexRE.match(text) is not None
|
|
|
|
|
|
def decrypttype1(data):
|
|
chunks = findencryptedchunks(data)
|
|
data = []
|
|
for isencrypted, chunk in chunks:
|
|
if isencrypted:
|
|
if ishex(chunk[:4]):
|
|
chunk = dehexstring(chunk)
|
|
decrypted, R = eexec.Decrypt(chunk, 55665)
|
|
decrypted = decrypted[4:]
|
|
if decrypted[-len(EEXECINTERNALEND)-1:-1] <> EEXECINTERNALEND \
|
|
and decrypted[-len(EEXECINTERNALEND)-2:-2] <> EEXECINTERNALEND:
|
|
raise error, "invalid end of eexec part"
|
|
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, '')
|
|
|
|
def findencryptedchunks(data):
|
|
chunks = []
|
|
while 1:
|
|
ebegin = string.find(data, EEXECBEGIN)
|
|
if ebegin < 0:
|
|
break
|
|
eend = string.find(data, EEXECEND, ebegin)
|
|
if eend < 0:
|
|
raise error, "can't find end of eexec part"
|
|
chunks.append((0, data[:ebegin + len(EEXECBEGIN) + 1]))
|
|
chunks.append((1, data[ebegin + len(EEXECBEGIN) + 1:eend]))
|
|
data = data[eend:]
|
|
chunks.append((0, data))
|
|
return chunks
|
|
|
|
def dehexstring(hexstring):
|
|
return eexec.dehexstring(string.join(string.split(hexstring), ""))
|
|
|
|
|
|
# Type 1 assertion
|
|
|
|
_fontType1RE = re.compile(r"/FontType\s+1\s+def")
|
|
|
|
def asserttype1(data):
|
|
for head in ['%!PS-AdobeFont', '%!FontType1-1.0']:
|
|
if data[:len(head)] == head:
|
|
break
|
|
else:
|
|
raise error, "not a PostScript font"
|
|
if not _fontType1RE.search(data):
|
|
raise error, "not a Type 1 font"
|
|
if string.find(data, "currentfile eexec") < 0:
|
|
raise error, "not an encrypted Type 1 font"
|
|
# XXX what else?
|
|
return data
|
|
|
|
|
|
# pfb helpers
|
|
|
|
def long2string(long):
|
|
str = ""
|
|
for i in range(4):
|
|
str = str + chr((long & (0xff << (i * 8))) >> i * 8)
|
|
return str
|
|
|
|
def string2long(str):
|
|
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
|