xref: /aosp_15_r20/external/fonttools/Lib/fontTools/t1Lib/__init__.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""fontTools.t1Lib.py -- Tools for PostScript Type 1 fonts.
2
3Functions for reading and writing raw Type 1 data:
4
5read(path)
6	reads any Type 1 font file, returns the raw data and a type indicator:
7	'LWFN', 'PFB' or 'OTHER', depending on the format of the file pointed
8	to by 'path'.
9	Raises an error when the file does not contain valid Type 1 data.
10
11write(path, data, kind='OTHER', dohex=False)
12	writes raw Type 1 data to the file pointed to by 'path'.
13	'kind' can be one of 'LWFN', 'PFB' or 'OTHER'; it defaults to 'OTHER'.
14	'dohex' is a flag which determines whether the eexec encrypted
15	part should be written as hexadecimal or binary, but only if kind
16	is 'OTHER'.
17"""
18
19import fontTools
20from fontTools.misc import eexec
21from fontTools.misc.macCreatorType import getMacCreatorAndType
22from fontTools.misc.textTools import bytechr, byteord, bytesjoin, tobytes
23from fontTools.misc.psOperators import (
24    _type1_pre_eexec_order,
25    _type1_fontinfo_order,
26    _type1_post_eexec_order,
27)
28from fontTools.encodings.StandardEncoding import StandardEncoding
29import os
30import re
31
32__author__ = "jvr"
33__version__ = "1.0b3"
34DEBUG = 0
35
36
37try:
38    try:
39        from Carbon import Res
40    except ImportError:
41        import Res  # MacPython < 2.2
42except ImportError:
43    haveMacSupport = 0
44else:
45    haveMacSupport = 1
46
47
48class T1Error(Exception):
49    pass
50
51
52class T1Font(object):
53    """Type 1 font class.
54
55    Uses a minimal interpeter that supports just about enough PS to parse
56    Type 1 fonts.
57    """
58
59    def __init__(self, path, encoding="ascii", kind=None):
60        if kind is None:
61            self.data, _ = read(path)
62        elif kind == "LWFN":
63            self.data = readLWFN(path)
64        elif kind == "PFB":
65            self.data = readPFB(path)
66        elif kind == "OTHER":
67            self.data = readOther(path)
68        else:
69            raise ValueError(kind)
70        self.encoding = encoding
71
72    def saveAs(self, path, type, dohex=False):
73        write(path, self.getData(), type, dohex)
74
75    def getData(self):
76        if not hasattr(self, "data"):
77            self.data = self.createData()
78        return self.data
79
80    def getGlyphSet(self):
81        """Return a generic GlyphSet, which is a dict-like object
82        mapping glyph names to glyph objects. The returned glyph objects
83        have a .draw() method that supports the Pen protocol, and will
84        have an attribute named 'width', but only *after* the .draw() method
85        has been called.
86
87        In the case of Type 1, the GlyphSet is simply the CharStrings dict.
88        """
89        return self["CharStrings"]
90
91    def __getitem__(self, key):
92        if not hasattr(self, "font"):
93            self.parse()
94        return self.font[key]
95
96    def parse(self):
97        from fontTools.misc import psLib
98        from fontTools.misc import psCharStrings
99
100        self.font = psLib.suckfont(self.data, self.encoding)
101        charStrings = self.font["CharStrings"]
102        lenIV = self.font["Private"].get("lenIV", 4)
103        assert lenIV >= 0
104        subrs = self.font["Private"]["Subrs"]
105        for glyphName, charString in charStrings.items():
106            charString, R = eexec.decrypt(charString, 4330)
107            charStrings[glyphName] = psCharStrings.T1CharString(
108                charString[lenIV:], subrs=subrs
109            )
110        for i in range(len(subrs)):
111            charString, R = eexec.decrypt(subrs[i], 4330)
112            subrs[i] = psCharStrings.T1CharString(charString[lenIV:], subrs=subrs)
113        del self.data
114
115    def createData(self):
116        sf = self.font
117
118        eexec_began = False
119        eexec_dict = {}
120        lines = []
121        lines.extend(
122            [
123                self._tobytes(f"%!FontType1-1.1: {sf['FontName']}"),
124                self._tobytes(f"%t1Font: ({fontTools.version})"),
125                self._tobytes(f"%%BeginResource: font {sf['FontName']}"),
126            ]
127        )
128        # follow t1write.c:writeRegNameKeyedFont
129        size = 3  # Headroom for new key addition
130        size += 1  # FontMatrix is always counted
131        size += 1 + 1  # Private, CharStings
132        for key in font_dictionary_keys:
133            size += int(key in sf)
134        lines.append(self._tobytes(f"{size} dict dup begin"))
135
136        for key, value in sf.items():
137            if eexec_began:
138                eexec_dict[key] = value
139                continue
140
141            if key == "FontInfo":
142                fi = sf["FontInfo"]
143                # follow t1write.c:writeFontInfoDict
144                size = 3  # Headroom for new key addition
145                for subkey in FontInfo_dictionary_keys:
146                    size += int(subkey in fi)
147                lines.append(self._tobytes(f"/FontInfo {size} dict dup begin"))
148
149                for subkey, subvalue in fi.items():
150                    lines.extend(self._make_lines(subkey, subvalue))
151                lines.append(b"end def")
152            elif key in _type1_post_eexec_order:  # usually 'Private'
153                eexec_dict[key] = value
154                eexec_began = True
155            else:
156                lines.extend(self._make_lines(key, value))
157        lines.append(b"end")
158        eexec_portion = self.encode_eexec(eexec_dict)
159        lines.append(bytesjoin([b"currentfile eexec ", eexec_portion]))
160
161        for _ in range(8):
162            lines.append(self._tobytes("0" * 64))
163        lines.extend([b"cleartomark", b"%%EndResource", b"%%EOF"])
164
165        data = bytesjoin(lines, "\n")
166        return data
167
168    def encode_eexec(self, eexec_dict):
169        lines = []
170
171        # '-|', '|-', '|'
172        RD_key, ND_key, NP_key = None, None, None
173        lenIV = 4
174        subrs = std_subrs
175
176        # Ensure we look at Private first, because we need RD_key, ND_key, NP_key and lenIV
177        sortedItems = sorted(eexec_dict.items(), key=lambda item: item[0] != "Private")
178
179        for key, value in sortedItems:
180            if key == "Private":
181                pr = eexec_dict["Private"]
182                # follow t1write.c:writePrivateDict
183                size = 3  # for RD, ND, NP
184                for subkey in Private_dictionary_keys:
185                    size += int(subkey in pr)
186                lines.append(b"dup /Private")
187                lines.append(self._tobytes(f"{size} dict dup begin"))
188                for subkey, subvalue in pr.items():
189                    if not RD_key and subvalue == RD_value:
190                        RD_key = subkey
191                    elif not ND_key and subvalue in ND_values:
192                        ND_key = subkey
193                    elif not NP_key and subvalue in PD_values:
194                        NP_key = subkey
195
196                    if subkey == "lenIV":
197                        lenIV = subvalue
198
199                    if subkey == "OtherSubrs":
200                        # XXX: assert that no flex hint is used
201                        lines.append(self._tobytes(hintothers))
202                    elif subkey == "Subrs":
203                        for subr_bin in subvalue:
204                            subr_bin.compile()
205                        subrs = [subr_bin.bytecode for subr_bin in subvalue]
206                        lines.append(f"/Subrs {len(subrs)} array".encode("ascii"))
207                        for i, subr_bin in enumerate(subrs):
208                            encrypted_subr, R = eexec.encrypt(
209                                bytesjoin([char_IV[:lenIV], subr_bin]), 4330
210                            )
211                            lines.append(
212                                bytesjoin(
213                                    [
214                                        self._tobytes(
215                                            f"dup {i} {len(encrypted_subr)} {RD_key} "
216                                        ),
217                                        encrypted_subr,
218                                        self._tobytes(f" {NP_key}"),
219                                    ]
220                                )
221                            )
222                        lines.append(b"def")
223
224                        lines.append(b"put")
225                    else:
226                        lines.extend(self._make_lines(subkey, subvalue))
227            elif key == "CharStrings":
228                lines.append(b"dup /CharStrings")
229                lines.append(
230                    self._tobytes(f"{len(eexec_dict['CharStrings'])} dict dup begin")
231                )
232                for glyph_name, char_bin in eexec_dict["CharStrings"].items():
233                    char_bin.compile()
234                    encrypted_char, R = eexec.encrypt(
235                        bytesjoin([char_IV[:lenIV], char_bin.bytecode]), 4330
236                    )
237                    lines.append(
238                        bytesjoin(
239                            [
240                                self._tobytes(
241                                    f"/{glyph_name} {len(encrypted_char)} {RD_key} "
242                                ),
243                                encrypted_char,
244                                self._tobytes(f" {ND_key}"),
245                            ]
246                        )
247                    )
248                lines.append(b"end put")
249            else:
250                lines.extend(self._make_lines(key, value))
251
252        lines.extend(
253            [
254                b"end",
255                b"dup /FontName get exch definefont pop",
256                b"mark",
257                b"currentfile closefile\n",
258            ]
259        )
260
261        eexec_portion = bytesjoin(lines, "\n")
262        encrypted_eexec, R = eexec.encrypt(bytesjoin([eexec_IV, eexec_portion]), 55665)
263
264        return encrypted_eexec
265
266    def _make_lines(self, key, value):
267        if key == "FontName":
268            return [self._tobytes(f"/{key} /{value} def")]
269        if key in ["isFixedPitch", "ForceBold", "RndStemUp"]:
270            return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
271        elif key == "Encoding":
272            if value == StandardEncoding:
273                return [self._tobytes(f"/{key} StandardEncoding def")]
274            else:
275                # follow fontTools.misc.psOperators._type1_Encoding_repr
276                lines = []
277                lines.append(b"/Encoding 256 array")
278                lines.append(b"0 1 255 {1 index exch /.notdef put} for")
279                for i in range(256):
280                    name = value[i]
281                    if name != ".notdef":
282                        lines.append(self._tobytes(f"dup {i} /{name} put"))
283                lines.append(b"def")
284                return lines
285        if isinstance(value, str):
286            return [self._tobytes(f"/{key} ({value}) def")]
287        elif isinstance(value, bool):
288            return [self._tobytes(f"/{key} {'true' if value else 'false'} def")]
289        elif isinstance(value, list):
290            return [self._tobytes(f"/{key} [{' '.join(str(v) for v in value)}] def")]
291        elif isinstance(value, tuple):
292            return [self._tobytes(f"/{key} {{{' '.join(str(v) for v in value)}}} def")]
293        else:
294            return [self._tobytes(f"/{key} {value} def")]
295
296    def _tobytes(self, s, errors="strict"):
297        return tobytes(s, self.encoding, errors)
298
299
300# low level T1 data read and write functions
301
302
303def read(path, onlyHeader=False):
304    """reads any Type 1 font file, returns raw data"""
305    _, ext = os.path.splitext(path)
306    ext = ext.lower()
307    creator, typ = getMacCreatorAndType(path)
308    if typ == "LWFN":
309        return readLWFN(path, onlyHeader), "LWFN"
310    if ext == ".pfb":
311        return readPFB(path, onlyHeader), "PFB"
312    else:
313        return readOther(path), "OTHER"
314
315
316def write(path, data, kind="OTHER", dohex=False):
317    assertType1(data)
318    kind = kind.upper()
319    try:
320        os.remove(path)
321    except os.error:
322        pass
323    err = 1
324    try:
325        if kind == "LWFN":
326            writeLWFN(path, data)
327        elif kind == "PFB":
328            writePFB(path, data)
329        else:
330            writeOther(path, data, dohex)
331        err = 0
332    finally:
333        if err and not DEBUG:
334            try:
335                os.remove(path)
336            except os.error:
337                pass
338
339
340# -- internal --
341
342LWFNCHUNKSIZE = 2000
343HEXLINELENGTH = 80
344
345
346def readLWFN(path, onlyHeader=False):
347    """reads an LWFN font file, returns raw data"""
348    from fontTools.misc.macRes import ResourceReader
349
350    reader = ResourceReader(path)
351    try:
352        data = []
353        for res in reader.get("POST", []):
354            code = byteord(res.data[0])
355            if byteord(res.data[1]) != 0:
356                raise T1Error("corrupt LWFN file")
357            if code in [1, 2]:
358                if onlyHeader and code == 2:
359                    break
360                data.append(res.data[2:])
361            elif code in [3, 5]:
362                break
363            elif code == 4:
364                with open(path, "rb") as f:
365                    data.append(f.read())
366            elif code == 0:
367                pass  # comment, ignore
368            else:
369                raise T1Error("bad chunk code: " + repr(code))
370    finally:
371        reader.close()
372    data = bytesjoin(data)
373    assertType1(data)
374    return data
375
376
377def readPFB(path, onlyHeader=False):
378    """reads a PFB font file, returns raw data"""
379    data = []
380    with open(path, "rb") as f:
381        while True:
382            if f.read(1) != bytechr(128):
383                raise T1Error("corrupt PFB file")
384            code = byteord(f.read(1))
385            if code in [1, 2]:
386                chunklen = stringToLong(f.read(4))
387                chunk = f.read(chunklen)
388                assert len(chunk) == chunklen
389                data.append(chunk)
390            elif code == 3:
391                break
392            else:
393                raise T1Error("bad chunk code: " + repr(code))
394            if onlyHeader:
395                break
396    data = bytesjoin(data)
397    assertType1(data)
398    return data
399
400
401def readOther(path):
402    """reads any (font) file, returns raw data"""
403    with open(path, "rb") as f:
404        data = f.read()
405    assertType1(data)
406    chunks = findEncryptedChunks(data)
407    data = []
408    for isEncrypted, chunk in chunks:
409        if isEncrypted and isHex(chunk[:4]):
410            data.append(deHexString(chunk))
411        else:
412            data.append(chunk)
413    return bytesjoin(data)
414
415
416# file writing tools
417
418
419def writeLWFN(path, data):
420    # Res.FSpCreateResFile was deprecated in OS X 10.5
421    Res.FSpCreateResFile(path, "just", "LWFN", 0)
422    resRef = Res.FSOpenResFile(path, 2)  # write-only
423    try:
424        Res.UseResFile(resRef)
425        resID = 501
426        chunks = findEncryptedChunks(data)
427        for isEncrypted, chunk in chunks:
428            if isEncrypted:
429                code = 2
430            else:
431                code = 1
432            while chunk:
433                res = Res.Resource(bytechr(code) + "\0" + chunk[: LWFNCHUNKSIZE - 2])
434                res.AddResource("POST", resID, "")
435                chunk = chunk[LWFNCHUNKSIZE - 2 :]
436                resID = resID + 1
437        res = Res.Resource(bytechr(5) + "\0")
438        res.AddResource("POST", resID, "")
439    finally:
440        Res.CloseResFile(resRef)
441
442
443def writePFB(path, data):
444    chunks = findEncryptedChunks(data)
445    with open(path, "wb") as f:
446        for isEncrypted, chunk in chunks:
447            if isEncrypted:
448                code = 2
449            else:
450                code = 1
451            f.write(bytechr(128) + bytechr(code))
452            f.write(longToString(len(chunk)))
453            f.write(chunk)
454        f.write(bytechr(128) + bytechr(3))
455
456
457def writeOther(path, data, dohex=False):
458    chunks = findEncryptedChunks(data)
459    with open(path, "wb") as f:
460        hexlinelen = HEXLINELENGTH // 2
461        for isEncrypted, chunk in chunks:
462            if isEncrypted:
463                code = 2
464            else:
465                code = 1
466            if code == 2 and dohex:
467                while chunk:
468                    f.write(eexec.hexString(chunk[:hexlinelen]))
469                    f.write(b"\r")
470                    chunk = chunk[hexlinelen:]
471            else:
472                f.write(chunk)
473
474
475# decryption tools
476
477EEXECBEGIN = b"currentfile eexec"
478# The spec allows for 512 ASCII zeros interrupted by arbitrary whitespace to
479# follow eexec
480EEXECEND = re.compile(b"(0[ \t\r\n]*){512}", flags=re.M)
481EEXECINTERNALEND = b"currentfile closefile"
482EEXECBEGINMARKER = b"%-- eexec start\r"
483EEXECENDMARKER = b"%-- eexec end\r"
484
485_ishexRE = re.compile(b"[0-9A-Fa-f]*$")
486
487
488def isHex(text):
489    return _ishexRE.match(text) is not None
490
491
492def decryptType1(data):
493    chunks = findEncryptedChunks(data)
494    data = []
495    for isEncrypted, chunk in chunks:
496        if isEncrypted:
497            if isHex(chunk[:4]):
498                chunk = deHexString(chunk)
499            decrypted, R = eexec.decrypt(chunk, 55665)
500            decrypted = decrypted[4:]
501            if (
502                decrypted[-len(EEXECINTERNALEND) - 1 : -1] != EEXECINTERNALEND
503                and decrypted[-len(EEXECINTERNALEND) - 2 : -2] != EEXECINTERNALEND
504            ):
505                raise T1Error("invalid end of eexec part")
506            decrypted = decrypted[: -len(EEXECINTERNALEND) - 2] + b"\r"
507            data.append(EEXECBEGINMARKER + decrypted + EEXECENDMARKER)
508        else:
509            if chunk[-len(EEXECBEGIN) - 1 : -1] == EEXECBEGIN:
510                data.append(chunk[: -len(EEXECBEGIN) - 1])
511            else:
512                data.append(chunk)
513    return bytesjoin(data)
514
515
516def findEncryptedChunks(data):
517    chunks = []
518    while True:
519        eBegin = data.find(EEXECBEGIN)
520        if eBegin < 0:
521            break
522        eBegin = eBegin + len(EEXECBEGIN) + 1
523        endMatch = EEXECEND.search(data, eBegin)
524        if endMatch is None:
525            raise T1Error("can't find end of eexec part")
526        eEnd = endMatch.start()
527        cypherText = data[eBegin : eEnd + 2]
528        if isHex(cypherText[:4]):
529            cypherText = deHexString(cypherText)
530        plainText, R = eexec.decrypt(cypherText, 55665)
531        eEndLocal = plainText.find(EEXECINTERNALEND)
532        if eEndLocal < 0:
533            raise T1Error("can't find end of eexec part")
534        chunks.append((0, data[:eBegin]))
535        chunks.append((1, cypherText[: eEndLocal + len(EEXECINTERNALEND) + 1]))
536        data = data[eEnd:]
537    chunks.append((0, data))
538    return chunks
539
540
541def deHexString(hexstring):
542    return eexec.deHexString(bytesjoin(hexstring.split()))
543
544
545# Type 1 assertion
546
547_fontType1RE = re.compile(rb"/FontType\s+1\s+def")
548
549
550def assertType1(data):
551    for head in [b"%!PS-AdobeFont", b"%!FontType1"]:
552        if data[: len(head)] == head:
553            break
554    else:
555        raise T1Error("not a PostScript font")
556    if not _fontType1RE.search(data):
557        raise T1Error("not a Type 1 font")
558    if data.find(b"currentfile eexec") < 0:
559        raise T1Error("not an encrypted Type 1 font")
560    # XXX what else?
561    return data
562
563
564# pfb helpers
565
566
567def longToString(long):
568    s = b""
569    for i in range(4):
570        s += bytechr((long & (0xFF << (i * 8))) >> i * 8)
571    return s
572
573
574def stringToLong(s):
575    if len(s) != 4:
576        raise ValueError("string must be 4 bytes long")
577    l = 0
578    for i in range(4):
579        l += byteord(s[i]) << (i * 8)
580    return l
581
582
583# PS stream helpers
584
585font_dictionary_keys = list(_type1_pre_eexec_order)
586# t1write.c:writeRegNameKeyedFont
587# always counts following keys
588font_dictionary_keys.remove("FontMatrix")
589
590FontInfo_dictionary_keys = list(_type1_fontinfo_order)
591# extend because AFDKO tx may use following keys
592FontInfo_dictionary_keys.extend(
593    [
594        "FSType",
595        "Copyright",
596    ]
597)
598
599Private_dictionary_keys = [
600    # We don't know what names will be actually used.
601    # "RD",
602    # "ND",
603    # "NP",
604    "Subrs",
605    "OtherSubrs",
606    "UniqueID",
607    "BlueValues",
608    "OtherBlues",
609    "FamilyBlues",
610    "FamilyOtherBlues",
611    "BlueScale",
612    "BlueShift",
613    "BlueFuzz",
614    "StdHW",
615    "StdVW",
616    "StemSnapH",
617    "StemSnapV",
618    "ForceBold",
619    "LanguageGroup",
620    "password",
621    "lenIV",
622    "MinFeature",
623    "RndStemUp",
624]
625
626# t1write_hintothers.h
627hintothers = """/OtherSubrs[{}{}{}{systemdict/internaldict known not{pop 3}{1183615869
628systemdict/internaldict get exec dup/startlock known{/startlock get exec}{dup
629/strtlck known{/strtlck get exec}{pop 3}ifelse}ifelse}ifelse}executeonly]def"""
630# t1write.c:saveStdSubrs
631std_subrs = [
632    # 3 0 callother pop pop setcurrentpoint return
633    b"\x8e\x8b\x0c\x10\x0c\x11\x0c\x11\x0c\x21\x0b",
634    # 0 1 callother return
635    b"\x8b\x8c\x0c\x10\x0b",
636    # 0 2 callother return
637    b"\x8b\x8d\x0c\x10\x0b",
638    # return
639    b"\x0b",
640    # 3 1 3 callother pop callsubr return
641    b"\x8e\x8c\x8e\x0c\x10\x0c\x11\x0a\x0b",
642]
643# follow t1write.c:writeRegNameKeyedFont
644eexec_IV = b"cccc"
645char_IV = b"\x0c\x0c\x0c\x0c"
646RD_value = ("string", "currentfile", "exch", "readstring", "pop")
647ND_values = [("def",), ("noaccess", "def")]
648PD_values = [("put",), ("noaccess", "put")]
649