You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
372 lines
9.0 KiB
372 lines
9.0 KiB
"""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=False) |
|
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'. |
|
""" |
|
from __future__ import print_function, division, absolute_import |
|
from fontTools.misc.py23 import * |
|
from fontTools.misc import eexec |
|
from fontTools.misc.macCreatorType import getMacCreatorAndType |
|
import os |
|
import re |
|
|
|
__author__ = "jvr" |
|
__version__ = "1.0b2" |
|
DEBUG = 0 |
|
|
|
|
|
try: |
|
try: |
|
from Carbon import Res |
|
except ImportError: |
|
import Res # MacPython < 2.2 |
|
except ImportError: |
|
haveMacSupport = 0 |
|
else: |
|
haveMacSupport = 1 |
|
import MacOS |
|
|
|
|
|
class T1Error(Exception): pass |
|
|
|
|
|
class T1Font(object): |
|
|
|
"""Type 1 font class. |
|
|
|
Uses a minimal interpeter that supports just about enough PS to parse |
|
Type 1 fonts. |
|
""" |
|
|
|
def __init__(self, path=None): |
|
if path is not None: |
|
self.data, type = read(path) |
|
else: |
|
pass # XXX |
|
|
|
def saveAs(self, path, type): |
|
write(path, self.getData(), type) |
|
|
|
def getData(self): |
|
# XXX Todo: if the data has been converted to Python object, |
|
# recreate the PS stream |
|
return self.data |
|
|
|
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"] |
|
|
|
def __getitem__(self, key): |
|
if not hasattr(self, "font"): |
|
self.parse() |
|
return self.font[key] |
|
|
|
def parse(self): |
|
from fontTools.misc import psLib |
|
from fontTools.misc import psCharStrings |
|
self.font = psLib.suckfont(self.data) |
|
charStrings = self.font["CharStrings"] |
|
lenIV = self.font["Private"].get("lenIV", 4) |
|
assert lenIV >= 0 |
|
subrs = self.font["Private"]["Subrs"] |
|
for glyphName, charString in charStrings.items(): |
|
charString, R = eexec.decrypt(charString, 4330) |
|
charStrings[glyphName] = psCharStrings.T1CharString(charString[lenIV:], |
|
subrs=subrs) |
|
for i in range(len(subrs)): |
|
charString, R = eexec.decrypt(subrs[i], 4330) |
|
subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs) |
|
del self.data |
|
|
|
|
|
# low level T1 data read and write functions |
|
|
|
def read(path, onlyHeader=False): |
|
"""reads any Type 1 font file, returns raw data""" |
|
normpath = path.lower() |
|
creator, typ = getMacCreatorAndType(path) |
|
if typ == 'LWFN': |
|
return readLWFN(path, onlyHeader), 'LWFN' |
|
if normpath[-4:] == '.pfb': |
|
return readPFB(path, onlyHeader), 'PFB' |
|
else: |
|
return readOther(path), 'OTHER' |
|
|
|
def write(path, data, kind='OTHER', dohex=False): |
|
assertType1(data) |
|
kind = kind.upper() |
|
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, onlyHeader=False): |
|
"""reads an LWFN font file, returns raw data""" |
|
resRef = Res.FSOpenResFile(path, 1) # read-only |
|
try: |
|
Res.UseResFile(resRef) |
|
n = Res.Count1Resources('POST') |
|
data = [] |
|
for i in range(501, 501 + n): |
|
res = Res.Get1Resource('POST', i) |
|
code = byteord(res.data[0]) |
|
if byteord(res.data[1]) != 0: |
|
raise T1Error('corrupt LWFN file') |
|
if code in [1, 2]: |
|
if onlyHeader and code == 2: |
|
break |
|
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 T1Error('bad chunk code: ' + repr(code)) |
|
finally: |
|
Res.CloseResFile(resRef) |
|
data = bytesjoin(data) |
|
assertType1(data) |
|
return data |
|
|
|
def readPFB(path, onlyHeader=False): |
|
"""reads a PFB font file, returns raw data""" |
|
f = open(path, "rb") |
|
data = [] |
|
while True: |
|
if f.read(1) != bytechr(128): |
|
raise T1Error('corrupt PFB file') |
|
code = byteord(f.read(1)) |
|
if code in [1, 2]: |
|
chunklen = stringToLong(f.read(4)) |
|
chunk = f.read(chunklen) |
|
assert len(chunk) == chunklen |
|
data.append(chunk) |
|
elif code == 3: |
|
break |
|
else: |
|
raise T1Error('bad chunk code: ' + repr(code)) |
|
if onlyHeader: |
|
break |
|
f.close() |
|
data = bytesjoin(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 bytesjoin(data) |
|
|
|
# file writing tools |
|
|
|
def writeLWFN(path, data): |
|
Res.FSpCreateResFile(path, "just", "LWFN", 0) |
|
resRef = Res.FSOpenResFile(path, 2) # write-only |
|
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(bytechr(code) + '\0' + chunk[:LWFNCHUNKSIZE - 2]) |
|
res.AddResource('POST', resID, '') |
|
chunk = chunk[LWFNCHUNKSIZE - 2:] |
|
resID = resID + 1 |
|
res = Res.Resource(bytechr(5) + '\0') |
|
res.AddResource('POST', resID, '') |
|
finally: |
|
Res.CloseResFile(resRef) |
|
|
|
def writePFB(path, data): |
|
chunks = findEncryptedChunks(data) |
|
f = open(path, "wb") |
|
try: |
|
for isEncrypted, chunk in chunks: |
|
if isEncrypted: |
|
code = 2 |
|
else: |
|
code = 1 |
|
f.write(bytechr(128) + bytechr(code)) |
|
f.write(longToString(len(chunk))) |
|
f.write(chunk) |
|
f.write(bytechr(128) + bytechr(3)) |
|
finally: |
|
f.close() |
|
|
|
def writeOther(path, data, dohex=False): |
|
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() |
|
|
|
|
|
# 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 T1Error("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 bytesjoin(data) |
|
|
|
def findEncryptedChunks(data): |
|
chunks = [] |
|
while True: |
|
eBegin = data.find(EEXECBEGIN) |
|
if eBegin < 0: |
|
break |
|
eBegin = eBegin + len(EEXECBEGIN) + 1 |
|
eEnd = data.find(EEXECEND, eBegin) |
|
if eEnd < 0: |
|
raise T1Error("can't find end of eexec part") |
|
cypherText = data[eBegin:eEnd + 2] |
|
if isHex(cypherText[:4]): |
|
cypherText = deHexString(cypherText) |
|
plainText, R = eexec.decrypt(cypherText, 55665) |
|
eEndLocal = plainText.find(EEXECINTERNALEND) |
|
if eEndLocal < 0: |
|
raise T1Error("can't find end of eexec part") |
|
chunks.append((0, data[:eBegin])) |
|
chunks.append((1, cypherText[:eEndLocal + len(EEXECINTERNALEND) + 1])) |
|
data = data[eEnd:] |
|
chunks.append((0, data)) |
|
return chunks |
|
|
|
def deHexString(hexstring): |
|
return eexec.deHexString(strjoin(hexstring.split())) |
|
|
|
|
|
# Type 1 assertion |
|
|
|
_fontType1RE = re.compile(br"/FontType\s+1\s+def") |
|
|
|
def assertType1(data): |
|
for head in [b'%!PS-AdobeFont', b'%!FontType1']: |
|
if data[:len(head)] == head: |
|
break |
|
else: |
|
raise T1Error("not a PostScript font") |
|
if not _fontType1RE.search(data): |
|
raise T1Error("not a Type 1 font") |
|
if data.find(b"currentfile eexec") < 0: |
|
raise T1Error("not an encrypted Type 1 font") |
|
# XXX what else? |
|
return data |
|
|
|
|
|
# pfb helpers |
|
|
|
def longToString(long): |
|
s = "" |
|
for i in range(4): |
|
s += bytechr((long & (0xff << (i * 8))) >> i * 8) |
|
return s |
|
|
|
def stringToLong(s): |
|
if len(s) != 4: |
|
raise ValueError('string must be 4 bytes long') |
|
l = 0 |
|
for i in range(4): |
|
l += byteord(s[i]) << (i * 8) |
|
return l |
|
|
|
|