123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- """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")]
|