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