|
"""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 'OTHER'. |
|
""" |
|
import fontTools |
|
from fontTools.misc import eexec |
|
from fontTools.misc.macCreatorType import getMacCreatorAndType |
|
from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes |
|
from fontTools.misc.psOperators import ( |
|
_type1_pre_eexec_order, |
|
_type1_fontinfo_order, |
|
_type1_post_eexec_order, |
|
) |
|
from fontTools.encodings.StandardEncoding import StandardEncoding |
|
import os |
|
import re |
|
|
|
__author__ = "jvr" |
|
__version__ = "1.0b3" |
|
DEBUG = 0 |
|
|
|
|
|
try: |
|
try: |
|
from Carbon import Res |
|
except ImportError: |
|
import Res |
|
except ImportError: |
|
haveMacSupport = 0 |
|
else: |
|
haveMacSupport = 1 |
|
|
|
|
|
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, encoding="ascii", kind=None): |
|
if kind is None: |
|
self.data, _ = read(path) |
|
elif kind == "LWFN": |
|
self.data = readLWFN(path) |
|
elif kind == "PFB": |
|
self.data = readPFB(path) |
|
elif kind == "OTHER": |
|
self.data = readOther(path) |
|
else: |
|
raise ValueError(kind) |
|
self.encoding = encoding |
|
|
|
def saveAs(self, path, type, dohex=False): |
|
write(path, self.getData(), type, dohex) |
|
|
|
def getData(self): |
|
if not hasattr(self, "data"): |
|
self.data = self.createData() |
|
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, self.encoding) |
|
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 |
|
|
|
def createData(self): |
|
sf = self.font |
|
|
|
eexec_began = False |
|
eexec_dict = {} |
|
lines = [] |
|
lines.extend( |
|
[ |
|
self._tobytes(f"%!FontType1-1.1: {sf['FontName']}"), |
|
self._tobytes(f"%t1Font: ({fontTools.version})"), |
|
self._tobytes(f"%%BeginResource: font {sf['FontName']}"), |
|
] |
|
) |
|
|
|
size = 3 |
|
size += 1 |
|
size += 1 + 1 |
|
for key in font_dictionary_keys: |
|
size += int(key in sf) |
|
lines.append(self._tobytes(f"{size} dict dup begin")) |
|
|
|
for key, value in sf.items(): |
|
if eexec_began: |
|
eexec_dict[key] = value |
|
continue |
|
|
|
if key == "FontInfo": |
|
fi = sf["FontInfo"] |
|
|
|
size = 3 |
|
for subkey in FontInfo_dictionary_keys: |
|
size += int(subkey in fi) |
|
lines.append(self._tobytes(f"/FontInfo {size} dict dup begin")) |
|
|
|
for subkey, subvalue in fi.items(): |
|
lines.extend(self._make_lines(subkey, subvalue)) |
|
lines.append(b"end def") |
|
elif key in _type1_post_eexec_order: |
|
eexec_dict[key] = value |
|
eexec_began = True |
|
else: |
|
lines.extend(self._make_lines(key, value)) |
|
lines.append(b"end") |
|
eexec_portion = self.encode_eexec(eexec_dict) |
|
lines.append(bytesjoin([b"currentfile eexec ", eexec_portion])) |
|
|
|
for _ in range(8): |
|
lines.append(self._tobytes("0" * 64)) |
|
lines.extend([b"cleartomark", b"%%EndResource", b"%%EOF"]) |
|
|
|
data = bytesjoin(lines, "\n") |
|
return data |
|
|
|
def encode_eexec(self, eexec_dict): |
|
lines = [] |
|
|
|
|
|
RD_key, ND_key, NP_key = None, None, None |
|
lenIV = 4 |
|
subrs = std_subrs |
|
|
|
|
|
sortedItems = sorted(eexec_dict.items(), key=lambda item: item[0] != "Private") |
|
|
|
for key, value in sortedItems: |
|
if key == "Private": |
|
pr = eexec_dict["Private"] |
|
|
|
size = 3 |
|
for subkey in Private_dictionary_keys: |
|
size += int(subkey in pr) |
|
lines.append(b"dup /Private") |
|
lines.append(self._tobytes(f"{size} dict dup begin")) |
|
for subkey, subvalue in pr.items(): |
|
if not RD_key and subvalue == RD_value: |
|
RD_key = subkey |
|
elif not ND_key and subvalue in ND_values: |
|
ND_key = subkey |
|
elif not NP_key and subvalue in PD_values: |
|
NP_key = subkey |
|
|
|
if subkey == "lenIV": |
|
lenIV = subvalue |
|
|
|
if subkey == "OtherSubrs": |
|
|
|
lines.append(self._tobytes(hintothers)) |
|
elif subkey == "Subrs": |
|
for subr_bin in subvalue: |
|
subr_bin.compile() |
|
subrs = [subr_bin.bytecode for subr_bin in subvalue] |
|
lines.append(f"/Subrs {len(subrs)} array".encode("ascii")) |
|
for i, subr_bin in enumerate(subrs): |
|
encrypted_subr, R = eexec.encrypt( |
|
bytesjoin([char_IV[:lenIV], subr_bin]), 4330 |
|
) |
|
lines.append( |
|
bytesjoin( |
|
[ |
|
self._tobytes( |
|
f"dup {i} {len(encrypted_subr)} {RD_key} " |
|
), |
|
encrypted_subr, |
|
self._tobytes(f" {NP_key}"), |
|
] |
|
) |
|
) |
|
lines.append(b"def") |
|
|
|
lines.append(b"put") |
|
else: |
|
lines.extend(self._make_lines(subkey, subvalue)) |
|
elif key == "CharStrings": |
|
lines.append(b"dup /CharStrings") |
|
lines.append( |
|
self._tobytes(f"{len(eexec_dict['CharStrings'])} dict dup begin") |
|
) |
|
for glyph_name, char_bin in eexec_dict["CharStrings"].items(): |
|
char_bin.compile() |
|
encrypted_char, R = eexec.encrypt( |
|
bytesjoin([char_IV[:lenIV], char_bin.bytecode]), 4330 |
|
) |
|
lines.append( |
|
bytesjoin( |
|
[ |
|
self._tobytes( |
|
f"/{glyph_name} {len(encrypted_char)} {RD_key} " |
|
), |
|
encrypted_char, |
|
self._tobytes(f" {ND_key}"), |
|
] |
|
) |
|
) |
|
lines.append(b"end put") |
|
else: |
|
lines.extend(self._make_lines(key, value)) |
|
|
|
lines.extend( |
|
[ |
|
b"end", |
|
b"dup /FontName get exch definefont pop", |
|
b"mark", |
|
b"currentfile closefile\n", |
|
] |
|
) |
|
|
|
eexec_portion = bytesjoin(lines, "\n") |
|
encrypted_eexec, R = eexec.encrypt(bytesjoin([eexec_IV, eexec_portion]), 55665) |
|
|
|
return encrypted_eexec |
|
|
|
def _make_lines(self, key, value): |
|
if key == "FontName": |
|
return [self._tobytes(f"/{key} /{value} def")] |
|
if key in ["isFixedPitch", "ForceBold", "RndStemUp"]: |
|
return [self._tobytes(f"/{key} {'true' if value else 'false'} def")] |
|
elif key == "Encoding": |
|
if value == StandardEncoding: |
|
return [self._tobytes(f"/{key} StandardEncoding def")] |
|
else: |
|
|
|
lines = [] |
|
lines.append(b"/Encoding 256 array") |
|
lines.append(b"0 1 255 {1 index exch /.notdef put} for") |
|
for i in range(256): |
|
name = value[i] |
|
if name != ".notdef": |
|
lines.append(self._tobytes(f"dup {i} /{name} put")) |
|
lines.append(b"def") |
|
return lines |
|
if isinstance(value, str): |
|
return [self._tobytes(f"/{key} ({value}) def")] |
|
elif isinstance(value, bool): |
|
return [self._tobytes(f"/{key} {'true' if value else 'false'} def")] |
|
elif isinstance(value, list): |
|
return [self._tobytes(f"/{key} [{' '.join(str(v) for v in value)}] def")] |
|
elif isinstance(value, tuple): |
|
return [self._tobytes(f"/{key} {{{' '.join(str(v) for v in value)}}} def")] |
|
else: |
|
return [self._tobytes(f"/{key} {value} def")] |
|
|
|
def _tobytes(self, s, errors="strict"): |
|
return tobytes(s, self.encoding, errors) |
|
|
|
|
|
|
|
|
|
|
|
def read(path, onlyHeader=False): |
|
"""reads any Type 1 font file, returns raw data""" |
|
_, ext = os.path.splitext(path) |
|
ext = ext.lower() |
|
creator, typ = getMacCreatorAndType(path) |
|
if typ == "LWFN": |
|
return readLWFN(path, onlyHeader), "LWFN" |
|
if ext == ".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 |
|
|
|
|
|
|
|
|
|
LWFNCHUNKSIZE = 2000 |
|
HEXLINELENGTH = 80 |
|
|
|
|
|
def readLWFN(path, onlyHeader=False): |
|
"""reads an LWFN font file, returns raw data""" |
|
from fontTools.misc.macRes import ResourceReader |
|
|
|
reader = ResourceReader(path) |
|
try: |
|
data = [] |
|
for res in reader.get("POST", []): |
|
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: |
|
with open(path, "rb") as f: |
|
data.append(f.read()) |
|
elif code == 0: |
|
pass |
|
else: |
|
raise T1Error("bad chunk code: " + repr(code)) |
|
finally: |
|
reader.close() |
|
data = bytesjoin(data) |
|
assertType1(data) |
|
return data |
|
|
|
|
|
def readPFB(path, onlyHeader=False): |
|
"""reads a PFB font file, returns raw data""" |
|
data = [] |
|
with open(path, "rb") as f: |
|
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 |
|
data = bytesjoin(data) |
|
assertType1(data) |
|
return data |
|
|
|
|
|
def readOther(path): |
|
"""reads any (font) file, returns raw data""" |
|
with open(path, "rb") as f: |
|
data = f.read() |
|
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) |
|
|
|
|
|
|
|
|
|
|
|
def writeLWFN(path, data): |
|
|
|
Res.FSpCreateResFile(path, "just", "LWFN", 0) |
|
resRef = Res.FSOpenResFile(path, 2) |
|
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) |
|
with open(path, "wb") as f: |
|
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)) |
|
|
|
|
|
def writeOther(path, data, dohex=False): |
|
chunks = findEncryptedChunks(data) |
|
with open(path, "wb") as f: |
|
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(b"\r") |
|
chunk = chunk[hexlinelen:] |
|
else: |
|
f.write(chunk) |
|
|
|
|
|
|
|
|
|
EEXECBEGIN = b"currentfile eexec" |
|
|
|
|
|
EEXECEND = re.compile(b"(0[ \t\r\n]*){512}", flags=re.M) |
|
EEXECINTERNALEND = b"currentfile closefile" |
|
EEXECBEGINMARKER = b"%-- eexec start\r" |
|
EEXECENDMARKER = b"%-- eexec end\r" |
|
|
|
_ishexRE = re.compile(b"[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] + b"\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 |
|
endMatch = EEXECEND.search(data, eBegin) |
|
if endMatch is None: |
|
raise T1Error("can't find end of eexec part") |
|
eEnd = endMatch.start() |
|
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(bytesjoin(hexstring.split())) |
|
|
|
|
|
|
|
|
|
_fontType1RE = re.compile(rb"/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") |
|
|
|
return data |
|
|
|
|
|
|
|
|
|
|
|
def longToString(long): |
|
s = b"" |
|
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 |
|
|
|
|
|
|
|
|
|
font_dictionary_keys = list(_type1_pre_eexec_order) |
|
|
|
|
|
font_dictionary_keys.remove("FontMatrix") |
|
|
|
FontInfo_dictionary_keys = list(_type1_fontinfo_order) |
|
|
|
FontInfo_dictionary_keys.extend( |
|
[ |
|
"FSType", |
|
"Copyright", |
|
] |
|
) |
|
|
|
Private_dictionary_keys = [ |
|
|
|
|
|
|
|
|
|
"Subrs", |
|
"OtherSubrs", |
|
"UniqueID", |
|
"BlueValues", |
|
"OtherBlues", |
|
"FamilyBlues", |
|
"FamilyOtherBlues", |
|
"BlueScale", |
|
"BlueShift", |
|
"BlueFuzz", |
|
"StdHW", |
|
"StdVW", |
|
"StemSnapH", |
|
"StemSnapV", |
|
"ForceBold", |
|
"LanguageGroup", |
|
"password", |
|
"lenIV", |
|
"MinFeature", |
|
"RndStemUp", |
|
] |
|
|
|
|
|
hintothers = """/OtherSubrs[{}{}{}{systemdict/internaldict known not{pop 3}{1183615869 |
|
systemdict/internaldict get exec dup/startlock known{/startlock get exec}{dup |
|
/strtlck known{/strtlck get exec}{pop 3}ifelse}ifelse}ifelse}executeonly]def""" |
|
|
|
std_subrs = [ |
|
|
|
b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b", |
|
|
|
b"\x8b\x8c\x0c\x10\x0b", |
|
|
|
b"\x8b\x8d\x0c\x10\x0b", |
|
|
|
b"\x0b", |
|
|
|
b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b", |
|
] |
|
|
|
eexec_IV = b"cccc" |
|
char_IV = b"\x0c\x0c\x0c\x0c" |
|
RD_value = ("string", "currentfile", "exch", "readstring", "pop") |
|
ND_values = [("def",), ("noaccess", "def")] |
|
PD_values = [("put",), ("noaccess", "put")] |
|
|