Spaces:
Sleeping
Sleeping
"""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 # MacPython < 2.2 | |
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']}"), | |
] | |
) | |
# follow t1write.c:writeRegNameKeyedFont | |
size = 3 # Headroom for new key addition | |
size += 1 # FontMatrix is always counted | |
size += 1 + 1 # Private, CharStings | |
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"] | |
# follow t1write.c:writeFontInfoDict | |
size = 3 # Headroom for new key addition | |
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: # usually 'Private' | |
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 | |
# Ensure we look at Private first, because we need RD_key, ND_key, NP_key and lenIV | |
sortedItems = sorted(eexec_dict.items(), key=lambda item: item[0] != "Private") | |
for key, value in sortedItems: | |
if key == "Private": | |
pr = eexec_dict["Private"] | |
# follow t1write.c:writePrivateDict | |
size = 3 # for RD, ND, NP | |
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": | |
# XXX: assert that no flex hint is used | |
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: | |
# follow fontTools.misc.psOperators._type1_Encoding_repr | |
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) | |
# low level T1 data read and write functions | |
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 | |
# -- internal -- | |
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 # comment, ignore | |
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) | |
# file writing tools | |
def writeLWFN(path, data): | |
# Res.FSpCreateResFile was deprecated in OS X 10.5 | |
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) | |
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) | |
# decryption tools | |
EEXECBEGIN = b"currentfile eexec" | |
# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to | |
# follow 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())) | |
# Type 1 assertion | |
_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") | |
# XXX what else? | |
return data | |
# pfb helpers | |
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 | |
# PS stream helpers | |
font_dictionary_keys = list(_type1_pre_eexec_order) | |
# t1write.c:writeRegNameKeyedFont | |
# always counts following keys | |
font_dictionary_keys.remove("FontMatrix") | |
FontInfo_dictionary_keys = list(_type1_fontinfo_order) | |
# extend because AFDKO tx may use following keys | |
FontInfo_dictionary_keys.extend( | |
[ | |
"FSType", | |
"Copyright", | |
] | |
) | |
Private_dictionary_keys = [ | |
# We don't know what names will be actually used. | |
# "RD", | |
# "ND", | |
# "NP", | |
"Subrs", | |
"OtherSubrs", | |
"UniqueID", | |
"BlueValues", | |
"OtherBlues", | |
"FamilyBlues", | |
"FamilyOtherBlues", | |
"BlueScale", | |
"BlueShift", | |
"BlueFuzz", | |
"StdHW", | |
"StdVW", | |
"StemSnapH", | |
"StemSnapV", | |
"ForceBold", | |
"LanguageGroup", | |
"password", | |
"lenIV", | |
"MinFeature", | |
"RndStemUp", | |
] | |
# t1write_hintothers.h | |
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""" | |
# t1write.c:saveStdSubrs | |
std_subrs = [ | |
# 3 0 callother pop pop setcurrentpoint return | |
b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b", | |
# 0 1 callother return | |
b"\x8b\x8c\x0c\x10\x0b", | |
# 0 2 callother return | |
b"\x8b\x8d\x0c\x10\x0b", | |
# return | |
b"\x0b", | |
# 3 1 3 callother pop callsubr return | |
b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b", | |
] | |
# follow t1write.c:writeRegNameKeyedFont | |
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")] | |