649 lines
20 KiB
Python
Raw Normal View History

2023-08-04 07:37:59 +02:00
"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts.
Functions for reading and writing raw Type 1 data:
read(path)
2015-04-26 02:01:01 -04:00
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.
2013-12-04 21:28:50 -05:00
write(path, data, kind='OTHER', dohex=False)
2015-04-26 02:01:01 -04: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 'OTHER'.
"""
2024-02-06 15:47:35 +02:00
import fontTools
from fontTools.misc import eexec
from fontTools.misc.macCreatorType import getMacCreatorAndType
from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes
2022-12-13 11:26:36 +00:00
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:
2022-12-13 11:26:36 +00:00
try:
from Carbon import Res
except ImportError:
import Res # MacPython < 2.2
except ImportError:
2022-12-13 11:26:36 +00:00
haveMacSupport = 0
else:
2022-12-13 11:26:36 +00:00
haveMacSupport = 1
2015-04-26 02:01:01 -04:00
2022-12-13 11:26:36 +00:00
class T1Error(Exception):
pass
class T1Font(object):
2022-12-13 11:26:36 +00:00
"""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
2022-12-13 11:26:36 +00:00
# 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:
2022-12-13 11:26:36 +00:00
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:
2022-12-13 11:26:36 +00:00
ND_key = subkey
elif not NP_key and subvalue in PD_values:
2022-12-13 11:26:36 +00:00
NP_key = subkey
if subkey == "lenIV":
lenIV = subvalue
2022-12-13 11:26:36 +00:00
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):
2022-12-13 11:26:36 +00:00
encrypted_subr, R = eexec.encrypt(
bytesjoin([char_IV[:lenIV], subr_bin]), 4330
2022-12-13 11:26:36 +00:00
)
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
2022-12-13 11:26:36 +00:00
)
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
2022-12-13 11:26:36 +00:00
2013-12-04 21:28:50 -05:00
def read(path, onlyHeader=False):
2022-12-13 11:26:36 +00:00
"""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
2015-04-26 02:01:01 -04:00
# -- internal --
LWFNCHUNKSIZE = 2000
HEXLINELENGTH = 80
2013-12-04 21:28:50 -05:00
def readLWFN(path, onlyHeader=False):
2022-12-13 11:26:36 +00:00
"""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
2013-12-04 21:28:50 -05:00
def readPFB(path, onlyHeader=False):
2022-12-13 11:26:36 +00:00
"""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):
2022-12-13 11:26:36 +00:00
"""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
2022-12-13 11:26:36 +00:00
def writeLWFN(path, data):
2022-12-13 11:26:36 +00:00
# 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):
2022-12-13 11:26:36 +00:00
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))
2013-12-04 21:28:50 -05:00
def writeOther(path, data, dohex=False):
2022-12-13 11:26:36 +00:00
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
2015-10-21 11:46:11 +01:00
EEXECBEGIN = b"currentfile eexec"
# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to
# follow eexec
2022-12-13 11:26:36 +00:00
EEXECEND = re.compile(b"(0[ \t\r\n]*){512}", flags=re.M)
2015-10-21 11:46:11 +01:00
EEXECINTERNALEND = b"currentfile closefile"
EEXECBEGINMARKER = b"%-- eexec start\r"
EEXECENDMARKER = b"%-- eexec end\r"
2022-12-13 11:26:36 +00:00
_ishexRE = re.compile(b"[0-9A-Fa-f]*$")
def isHex(text):
2022-12-13 11:26:36 +00:00
return _ishexRE.match(text) is not None
def decryptType1(data):
2022-12-13 11:26:36 +00:00
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):
2022-12-13 11:26:36 +00:00
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):
2022-12-13 11:26:36 +00:00
return eexec.deHexString(bytesjoin(hexstring.split()))
# Type 1 assertion
2022-12-13 11:26:36 +00:00
_fontType1RE = re.compile(rb"/FontType\s+1\s+def")
def assertType1(data):
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
def longToString(long):
2022-12-13 11:26:36 +00:00
s = b""
for i in range(4):
s += bytechr((long & (0xFF << (i * 8))) >> i * 8)
return s
def stringToLong(s):
2022-12-13 11:26:36 +00:00
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
2022-12-13 11:26:36 +00:00
FontInfo_dictionary_keys.extend(
[
"FSType",
"Copyright",
]
)
Private_dictionary_keys = [
2022-12-13 11:26:36 +00:00
# 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 = [
2022-12-13 11:26:36 +00:00
# 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")]