Spaces:
Paused
Paused
| """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")] | |