xref: /aosp_15_r20/external/fonttools/Lib/fontTools/cffLib/__init__.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1"""cffLib: read/write Adobe CFF fonts
2
3OpenType fonts with PostScript outlines contain a completely independent
4font file, Adobe's *Compact Font Format*. So dealing with OpenType fonts
5requires also dealing with CFF. This module allows you to read and write
6fonts written in the CFF format.
7
8In 2016, OpenType 1.8 introduced the `CFF2 <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2>`_
9format which, along with other changes, extended the CFF format to deal with
10the demands of variable fonts. This module parses both original CFF and CFF2.
11
12"""
13
14from fontTools.misc import sstruct
15from fontTools.misc import psCharStrings
16from fontTools.misc.arrayTools import unionRect, intRect
17from fontTools.misc.textTools import (
18    bytechr,
19    byteord,
20    bytesjoin,
21    tobytes,
22    tostr,
23    safeEval,
24)
25from fontTools.ttLib import TTFont
26from fontTools.ttLib.tables.otBase import OTTableWriter
27from fontTools.ttLib.tables.otBase import OTTableReader
28from fontTools.ttLib.tables import otTables as ot
29from io import BytesIO
30import struct
31import logging
32import re
33
34# mute cffLib debug messages when running ttx in verbose mode
35DEBUG = logging.DEBUG - 1
36log = logging.getLogger(__name__)
37
38cffHeaderFormat = """
39	major:   B
40	minor:   B
41	hdrSize: B
42"""
43
44maxStackLimit = 513
45# maxstack operator has been deprecated. max stack is now always 513.
46
47
48class StopHintCountEvent(Exception):
49    pass
50
51
52class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
53    stop_hintcount_ops = (
54        "op_hintmask",
55        "op_cntrmask",
56        "op_rmoveto",
57        "op_hmoveto",
58        "op_vmoveto",
59    )
60
61    def __init__(self, localSubrs, globalSubrs, private=None):
62        psCharStrings.SimpleT2Decompiler.__init__(
63            self, localSubrs, globalSubrs, private
64        )
65
66    def execute(self, charString):
67        self.need_hintcount = True  # until proven otherwise
68        for op_name in self.stop_hintcount_ops:
69            setattr(self, op_name, self.stop_hint_count)
70
71        if hasattr(charString, "_desubroutinized"):
72            # If a charstring has already been desubroutinized, we will still
73            # need to execute it if we need to count hints in order to
74            # compute the byte length for mask arguments, and haven't finished
75            # counting hints pairs.
76            if self.need_hintcount and self.callingStack:
77                try:
78                    psCharStrings.SimpleT2Decompiler.execute(self, charString)
79                except StopHintCountEvent:
80                    del self.callingStack[-1]
81            return
82
83        charString._patches = []
84        psCharStrings.SimpleT2Decompiler.execute(self, charString)
85        desubroutinized = charString.program[:]
86        for idx, expansion in reversed(charString._patches):
87            assert idx >= 2
88            assert desubroutinized[idx - 1] in [
89                "callsubr",
90                "callgsubr",
91            ], desubroutinized[idx - 1]
92            assert type(desubroutinized[idx - 2]) == int
93            if expansion[-1] == "return":
94                expansion = expansion[:-1]
95            desubroutinized[idx - 2 : idx] = expansion
96        if not self.private.in_cff2:
97            if "endchar" in desubroutinized:
98                # Cut off after first endchar
99                desubroutinized = desubroutinized[
100                    : desubroutinized.index("endchar") + 1
101                ]
102            else:
103                if not len(desubroutinized) or desubroutinized[-1] != "return":
104                    desubroutinized.append("return")
105
106        charString._desubroutinized = desubroutinized
107        del charString._patches
108
109    def op_callsubr(self, index):
110        subr = self.localSubrs[self.operandStack[-1] + self.localBias]
111        psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
112        self.processSubr(index, subr)
113
114    def op_callgsubr(self, index):
115        subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
116        psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
117        self.processSubr(index, subr)
118
119    def stop_hint_count(self, *args):
120        self.need_hintcount = False
121        for op_name in self.stop_hintcount_ops:
122            setattr(self, op_name, None)
123        cs = self.callingStack[-1]
124        if hasattr(cs, "_desubroutinized"):
125            raise StopHintCountEvent()
126
127    def op_hintmask(self, index):
128        psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
129        if self.need_hintcount:
130            self.stop_hint_count()
131
132    def processSubr(self, index, subr):
133        cs = self.callingStack[-1]
134        if not hasattr(cs, "_desubroutinized"):
135            cs._patches.append((index, subr._desubroutinized))
136
137
138class CFFFontSet(object):
139    """A CFF font "file" can contain more than one font, although this is
140    extremely rare (and not allowed within OpenType fonts).
141
142    This class is the entry point for parsing a CFF table. To actually
143    manipulate the data inside the CFF font, you will want to access the
144    ``CFFFontSet``'s :class:`TopDict` object. To do this, a ``CFFFontSet``
145    object can either be treated as a dictionary (with appropriate
146    ``keys()`` and ``values()`` methods) mapping font names to :class:`TopDict`
147    objects, or as a list.
148
149    .. code:: python
150
151            from fontTools import ttLib
152            tt = ttLib.TTFont("Tests/cffLib/data/LinLibertine_RBI.otf")
153            tt["CFF "].cff
154            # <fontTools.cffLib.CFFFontSet object at 0x101e24c90>
155            tt["CFF "].cff[0] # Here's your actual font data
156            # <fontTools.cffLib.TopDict object at 0x1020f1fd0>
157
158    """
159
160    def decompile(self, file, otFont, isCFF2=None):
161        """Parse a binary CFF file into an internal representation. ``file``
162        should be a file handle object. ``otFont`` is the top-level
163        :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file.
164
165        If ``isCFF2`` is passed and set to ``True`` or ``False``, then the
166        library makes an assertion that the CFF header is of the appropriate
167        version.
168        """
169
170        self.otFont = otFont
171        sstruct.unpack(cffHeaderFormat, file.read(3), self)
172        if isCFF2 is not None:
173            # called from ttLib: assert 'major' as read from file matches the
174            # expected version
175            expected_major = 2 if isCFF2 else 1
176            if self.major != expected_major:
177                raise ValueError(
178                    "Invalid CFF 'major' version: expected %d, found %d"
179                    % (expected_major, self.major)
180                )
181        else:
182            # use 'major' version from file to determine if isCFF2
183            assert self.major in (1, 2), "Unknown CFF format"
184            isCFF2 = self.major == 2
185        if not isCFF2:
186            self.offSize = struct.unpack("B", file.read(1))[0]
187            file.seek(self.hdrSize)
188            self.fontNames = list(tostr(s) for s in Index(file, isCFF2=isCFF2))
189            self.topDictIndex = TopDictIndex(file, isCFF2=isCFF2)
190            self.strings = IndexedStrings(file)
191        else:  # isCFF2
192            self.topDictSize = struct.unpack(">H", file.read(2))[0]
193            file.seek(self.hdrSize)
194            self.fontNames = ["CFF2Font"]
195            cff2GetGlyphOrder = otFont.getGlyphOrder
196            # in CFF2, offsetSize is the size of the TopDict data.
197            self.topDictIndex = TopDictIndex(
198                file, cff2GetGlyphOrder, self.topDictSize, isCFF2=isCFF2
199            )
200            self.strings = None
201        self.GlobalSubrs = GlobalSubrsIndex(file, isCFF2=isCFF2)
202        self.topDictIndex.strings = self.strings
203        self.topDictIndex.GlobalSubrs = self.GlobalSubrs
204
205    def __len__(self):
206        return len(self.fontNames)
207
208    def keys(self):
209        return list(self.fontNames)
210
211    def values(self):
212        return self.topDictIndex
213
214    def __getitem__(self, nameOrIndex):
215        """Return TopDict instance identified by name (str) or index (int
216        or any object that implements `__index__`).
217        """
218        if hasattr(nameOrIndex, "__index__"):
219            index = nameOrIndex.__index__()
220        elif isinstance(nameOrIndex, str):
221            name = nameOrIndex
222            try:
223                index = self.fontNames.index(name)
224            except ValueError:
225                raise KeyError(nameOrIndex)
226        else:
227            raise TypeError(nameOrIndex)
228        return self.topDictIndex[index]
229
230    def compile(self, file, otFont, isCFF2=None):
231        """Write the object back into binary representation onto the given file.
232        ``file`` should be a file handle object. ``otFont`` is the top-level
233        :py:class:`fontTools.ttLib.ttFont.TTFont` object containing this CFF file.
234
235        If ``isCFF2`` is passed and set to ``True`` or ``False``, then the
236        library makes an assertion that the CFF header is of the appropriate
237        version.
238        """
239        self.otFont = otFont
240        if isCFF2 is not None:
241            # called from ttLib: assert 'major' value matches expected version
242            expected_major = 2 if isCFF2 else 1
243            if self.major != expected_major:
244                raise ValueError(
245                    "Invalid CFF 'major' version: expected %d, found %d"
246                    % (expected_major, self.major)
247                )
248        else:
249            # use current 'major' value to determine output format
250            assert self.major in (1, 2), "Unknown CFF format"
251            isCFF2 = self.major == 2
252
253        if otFont.recalcBBoxes and not isCFF2:
254            for topDict in self.topDictIndex:
255                topDict.recalcFontBBox()
256
257        if not isCFF2:
258            strings = IndexedStrings()
259        else:
260            strings = None
261        writer = CFFWriter(isCFF2)
262        topCompiler = self.topDictIndex.getCompiler(strings, self, isCFF2=isCFF2)
263        if isCFF2:
264            self.hdrSize = 5
265            writer.add(sstruct.pack(cffHeaderFormat, self))
266            # Note: topDictSize will most likely change in CFFWriter.toFile().
267            self.topDictSize = topCompiler.getDataLength()
268            writer.add(struct.pack(">H", self.topDictSize))
269        else:
270            self.hdrSize = 4
271            self.offSize = 4  # will most likely change in CFFWriter.toFile().
272            writer.add(sstruct.pack(cffHeaderFormat, self))
273            writer.add(struct.pack("B", self.offSize))
274        if not isCFF2:
275            fontNames = Index()
276            for name in self.fontNames:
277                fontNames.append(name)
278            writer.add(fontNames.getCompiler(strings, self, isCFF2=isCFF2))
279        writer.add(topCompiler)
280        if not isCFF2:
281            writer.add(strings.getCompiler())
282        writer.add(self.GlobalSubrs.getCompiler(strings, self, isCFF2=isCFF2))
283
284        for topDict in self.topDictIndex:
285            if not hasattr(topDict, "charset") or topDict.charset is None:
286                charset = otFont.getGlyphOrder()
287                topDict.charset = charset
288        children = topCompiler.getChildren(strings)
289        for child in children:
290            writer.add(child)
291
292        writer.toFile(file)
293
294    def toXML(self, xmlWriter):
295        """Write the object into XML representation onto the given
296        :class:`fontTools.misc.xmlWriter.XMLWriter`.
297
298        .. code:: python
299
300                writer = xmlWriter.XMLWriter(sys.stdout)
301                tt["CFF "].cff.toXML(writer)
302
303        """
304
305        xmlWriter.simpletag("major", value=self.major)
306        xmlWriter.newline()
307        xmlWriter.simpletag("minor", value=self.minor)
308        xmlWriter.newline()
309        for fontName in self.fontNames:
310            xmlWriter.begintag("CFFFont", name=tostr(fontName))
311            xmlWriter.newline()
312            font = self[fontName]
313            font.toXML(xmlWriter)
314            xmlWriter.endtag("CFFFont")
315            xmlWriter.newline()
316        xmlWriter.newline()
317        xmlWriter.begintag("GlobalSubrs")
318        xmlWriter.newline()
319        self.GlobalSubrs.toXML(xmlWriter)
320        xmlWriter.endtag("GlobalSubrs")
321        xmlWriter.newline()
322
323    def fromXML(self, name, attrs, content, otFont=None):
324        """Reads data from the XML element into the ``CFFFontSet`` object."""
325        self.otFont = otFont
326
327        # set defaults. These will be replaced if there are entries for them
328        # in the XML file.
329        if not hasattr(self, "major"):
330            self.major = 1
331        if not hasattr(self, "minor"):
332            self.minor = 0
333
334        if name == "CFFFont":
335            if self.major == 1:
336                if not hasattr(self, "offSize"):
337                    # this will be recalculated when the cff is compiled.
338                    self.offSize = 4
339                if not hasattr(self, "hdrSize"):
340                    self.hdrSize = 4
341                if not hasattr(self, "GlobalSubrs"):
342                    self.GlobalSubrs = GlobalSubrsIndex()
343                if not hasattr(self, "fontNames"):
344                    self.fontNames = []
345                    self.topDictIndex = TopDictIndex()
346                fontName = attrs["name"]
347                self.fontNames.append(fontName)
348                topDict = TopDict(GlobalSubrs=self.GlobalSubrs)
349                topDict.charset = None  # gets filled in later
350            elif self.major == 2:
351                if not hasattr(self, "hdrSize"):
352                    self.hdrSize = 5
353                if not hasattr(self, "GlobalSubrs"):
354                    self.GlobalSubrs = GlobalSubrsIndex()
355                if not hasattr(self, "fontNames"):
356                    self.fontNames = ["CFF2Font"]
357                cff2GetGlyphOrder = self.otFont.getGlyphOrder
358                topDict = TopDict(
359                    GlobalSubrs=self.GlobalSubrs, cff2GetGlyphOrder=cff2GetGlyphOrder
360                )
361                self.topDictIndex = TopDictIndex(None, cff2GetGlyphOrder)
362            self.topDictIndex.append(topDict)
363            for element in content:
364                if isinstance(element, str):
365                    continue
366                name, attrs, content = element
367                topDict.fromXML(name, attrs, content)
368
369            if hasattr(topDict, "VarStore") and topDict.FDArray[0].vstore is None:
370                fdArray = topDict.FDArray
371                for fontDict in fdArray:
372                    if hasattr(fontDict, "Private"):
373                        fontDict.Private.vstore = topDict.VarStore
374
375        elif name == "GlobalSubrs":
376            subrCharStringClass = psCharStrings.T2CharString
377            if not hasattr(self, "GlobalSubrs"):
378                self.GlobalSubrs = GlobalSubrsIndex()
379            for element in content:
380                if isinstance(element, str):
381                    continue
382                name, attrs, content = element
383                subr = subrCharStringClass()
384                subr.fromXML(name, attrs, content)
385                self.GlobalSubrs.append(subr)
386        elif name == "major":
387            self.major = int(attrs["value"])
388        elif name == "minor":
389            self.minor = int(attrs["value"])
390
391    def convertCFFToCFF2(self, otFont):
392        """Converts this object from CFF format to CFF2 format. This conversion
393        is done 'in-place'. The conversion cannot be reversed.
394
395        This assumes a decompiled CFF table. (i.e. that the object has been
396        filled via :meth:`decompile`.)"""
397        self.major = 2
398        cff2GetGlyphOrder = self.otFont.getGlyphOrder
399        topDictData = TopDictIndex(None, cff2GetGlyphOrder)
400        topDictData.items = self.topDictIndex.items
401        self.topDictIndex = topDictData
402        topDict = topDictData[0]
403        if hasattr(topDict, "Private"):
404            privateDict = topDict.Private
405        else:
406            privateDict = None
407        opOrder = buildOrder(topDictOperators2)
408        topDict.order = opOrder
409        topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
410        for entry in topDictOperators:
411            key = entry[1]
412            if key not in opOrder:
413                if key in topDict.rawDict:
414                    del topDict.rawDict[key]
415                if hasattr(topDict, key):
416                    delattr(topDict, key)
417
418        if not hasattr(topDict, "FDArray"):
419            fdArray = topDict.FDArray = FDArrayIndex()
420            fdArray.strings = None
421            fdArray.GlobalSubrs = topDict.GlobalSubrs
422            topDict.GlobalSubrs.fdArray = fdArray
423            charStrings = topDict.CharStrings
424            if charStrings.charStringsAreIndexed:
425                charStrings.charStringsIndex.fdArray = fdArray
426            else:
427                charStrings.fdArray = fdArray
428            fontDict = FontDict()
429            fontDict.setCFF2(True)
430            fdArray.append(fontDict)
431            fontDict.Private = privateDict
432            privateOpOrder = buildOrder(privateDictOperators2)
433            for entry in privateDictOperators:
434                key = entry[1]
435                if key not in privateOpOrder:
436                    if key in privateDict.rawDict:
437                        # print "Removing private dict", key
438                        del privateDict.rawDict[key]
439                    if hasattr(privateDict, key):
440                        delattr(privateDict, key)
441                        # print "Removing privateDict attr", key
442        else:
443            # clean up the PrivateDicts in the fdArray
444            fdArray = topDict.FDArray
445            privateOpOrder = buildOrder(privateDictOperators2)
446            for fontDict in fdArray:
447                fontDict.setCFF2(True)
448                for key in fontDict.rawDict.keys():
449                    if key not in fontDict.order:
450                        del fontDict.rawDict[key]
451                        if hasattr(fontDict, key):
452                            delattr(fontDict, key)
453
454                privateDict = fontDict.Private
455                for entry in privateDictOperators:
456                    key = entry[1]
457                    if key not in privateOpOrder:
458                        if key in privateDict.rawDict:
459                            # print "Removing private dict", key
460                            del privateDict.rawDict[key]
461                        if hasattr(privateDict, key):
462                            delattr(privateDict, key)
463                            # print "Removing privateDict attr", key
464        # At this point, the Subrs and Charstrings are all still T2Charstring class
465        # easiest to fix this by compiling, then decompiling again
466        file = BytesIO()
467        self.compile(file, otFont, isCFF2=True)
468        file.seek(0)
469        self.decompile(file, otFont, isCFF2=True)
470
471    def desubroutinize(self):
472        for fontName in self.fontNames:
473            font = self[fontName]
474            cs = font.CharStrings
475            for g in font.charset:
476                c, _ = cs.getItemAndSelector(g)
477                c.decompile()
478                subrs = getattr(c.private, "Subrs", [])
479                decompiler = _DesubroutinizingT2Decompiler(
480                    subrs, c.globalSubrs, c.private
481                )
482                decompiler.execute(c)
483                c.program = c._desubroutinized
484                del c._desubroutinized
485            # Delete all the local subrs
486            if hasattr(font, "FDArray"):
487                for fd in font.FDArray:
488                    pd = fd.Private
489                    if hasattr(pd, "Subrs"):
490                        del pd.Subrs
491                    if "Subrs" in pd.rawDict:
492                        del pd.rawDict["Subrs"]
493            else:
494                pd = font.Private
495                if hasattr(pd, "Subrs"):
496                    del pd.Subrs
497                if "Subrs" in pd.rawDict:
498                    del pd.rawDict["Subrs"]
499        # as well as the global subrs
500        self.GlobalSubrs.clear()
501
502
503class CFFWriter(object):
504    """Helper class for serializing CFF data to binary. Used by
505    :meth:`CFFFontSet.compile`."""
506
507    def __init__(self, isCFF2):
508        self.data = []
509        self.isCFF2 = isCFF2
510
511    def add(self, table):
512        self.data.append(table)
513
514    def toFile(self, file):
515        lastPosList = None
516        count = 1
517        while True:
518            log.log(DEBUG, "CFFWriter.toFile() iteration: %d", count)
519            count = count + 1
520            pos = 0
521            posList = [pos]
522            for item in self.data:
523                if hasattr(item, "getDataLength"):
524                    endPos = pos + item.getDataLength()
525                    if isinstance(item, TopDictIndexCompiler) and item.isCFF2:
526                        self.topDictSize = item.getDataLength()
527                else:
528                    endPos = pos + len(item)
529                if hasattr(item, "setPos"):
530                    item.setPos(pos, endPos)
531                pos = endPos
532                posList.append(pos)
533            if posList == lastPosList:
534                break
535            lastPosList = posList
536        log.log(DEBUG, "CFFWriter.toFile() writing to file.")
537        begin = file.tell()
538        if self.isCFF2:
539            self.data[1] = struct.pack(">H", self.topDictSize)
540        else:
541            self.offSize = calcOffSize(lastPosList[-1])
542            self.data[1] = struct.pack("B", self.offSize)
543        posList = [0]
544        for item in self.data:
545            if hasattr(item, "toFile"):
546                item.toFile(file)
547            else:
548                file.write(item)
549            posList.append(file.tell() - begin)
550        assert posList == lastPosList
551
552
553def calcOffSize(largestOffset):
554    if largestOffset < 0x100:
555        offSize = 1
556    elif largestOffset < 0x10000:
557        offSize = 2
558    elif largestOffset < 0x1000000:
559        offSize = 3
560    else:
561        offSize = 4
562    return offSize
563
564
565class IndexCompiler(object):
566    """Base class for writing CFF `INDEX data <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#5-index-data>`_
567    to binary."""
568
569    def __init__(self, items, strings, parent, isCFF2=None):
570        if isCFF2 is None and hasattr(parent, "isCFF2"):
571            isCFF2 = parent.isCFF2
572            assert isCFF2 is not None
573        self.isCFF2 = isCFF2
574        self.items = self.getItems(items, strings)
575        self.parent = parent
576
577    def getItems(self, items, strings):
578        return items
579
580    def getOffsets(self):
581        # An empty INDEX contains only the count field.
582        if self.items:
583            pos = 1
584            offsets = [pos]
585            for item in self.items:
586                if hasattr(item, "getDataLength"):
587                    pos = pos + item.getDataLength()
588                else:
589                    pos = pos + len(item)
590                offsets.append(pos)
591        else:
592            offsets = []
593        return offsets
594
595    def getDataLength(self):
596        if self.isCFF2:
597            countSize = 4
598        else:
599            countSize = 2
600
601        if self.items:
602            lastOffset = self.getOffsets()[-1]
603            offSize = calcOffSize(lastOffset)
604            dataLength = (
605                countSize
606                + 1  # count
607                + (len(self.items) + 1) * offSize  # offSize
608                + lastOffset  # the offsets
609                - 1  # size of object data
610            )
611        else:
612            # count. For empty INDEX tables, this is the only entry.
613            dataLength = countSize
614
615        return dataLength
616
617    def toFile(self, file):
618        offsets = self.getOffsets()
619        if self.isCFF2:
620            writeCard32(file, len(self.items))
621        else:
622            writeCard16(file, len(self.items))
623        # An empty INDEX contains only the count field.
624        if self.items:
625            offSize = calcOffSize(offsets[-1])
626            writeCard8(file, offSize)
627            offSize = -offSize
628            pack = struct.pack
629            for offset in offsets:
630                binOffset = pack(">l", offset)[offSize:]
631                assert len(binOffset) == -offSize
632                file.write(binOffset)
633            for item in self.items:
634                if hasattr(item, "toFile"):
635                    item.toFile(file)
636                else:
637                    data = tobytes(item, encoding="latin1")
638                    file.write(data)
639
640
641class IndexedStringsCompiler(IndexCompiler):
642    def getItems(self, items, strings):
643        return items.strings
644
645
646class TopDictIndexCompiler(IndexCompiler):
647    """Helper class for writing the TopDict to binary."""
648
649    def getItems(self, items, strings):
650        out = []
651        for item in items:
652            out.append(item.getCompiler(strings, self))
653        return out
654
655    def getChildren(self, strings):
656        children = []
657        for topDict in self.items:
658            children.extend(topDict.getChildren(strings))
659        return children
660
661    def getOffsets(self):
662        if self.isCFF2:
663            offsets = [0, self.items[0].getDataLength()]
664            return offsets
665        else:
666            return super(TopDictIndexCompiler, self).getOffsets()
667
668    def getDataLength(self):
669        if self.isCFF2:
670            dataLength = self.items[0].getDataLength()
671            return dataLength
672        else:
673            return super(TopDictIndexCompiler, self).getDataLength()
674
675    def toFile(self, file):
676        if self.isCFF2:
677            self.items[0].toFile(file)
678        else:
679            super(TopDictIndexCompiler, self).toFile(file)
680
681
682class FDArrayIndexCompiler(IndexCompiler):
683    """Helper class for writing the
684    `Font DICT INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#10-font-dict-index-font-dicts-and-fdselect>`_
685    to binary."""
686
687    def getItems(self, items, strings):
688        out = []
689        for item in items:
690            out.append(item.getCompiler(strings, self))
691        return out
692
693    def getChildren(self, strings):
694        children = []
695        for fontDict in self.items:
696            children.extend(fontDict.getChildren(strings))
697        return children
698
699    def toFile(self, file):
700        offsets = self.getOffsets()
701        if self.isCFF2:
702            writeCard32(file, len(self.items))
703        else:
704            writeCard16(file, len(self.items))
705        offSize = calcOffSize(offsets[-1])
706        writeCard8(file, offSize)
707        offSize = -offSize
708        pack = struct.pack
709        for offset in offsets:
710            binOffset = pack(">l", offset)[offSize:]
711            assert len(binOffset) == -offSize
712            file.write(binOffset)
713        for item in self.items:
714            if hasattr(item, "toFile"):
715                item.toFile(file)
716            else:
717                file.write(item)
718
719    def setPos(self, pos, endPos):
720        self.parent.rawDict["FDArray"] = pos
721
722
723class GlobalSubrsCompiler(IndexCompiler):
724    """Helper class for writing the `global subroutine INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#9-local-and-global-subr-indexes>`_
725    to binary."""
726
727    def getItems(self, items, strings):
728        out = []
729        for cs in items:
730            cs.compile(self.isCFF2)
731            out.append(cs.bytecode)
732        return out
733
734
735class SubrsCompiler(GlobalSubrsCompiler):
736    """Helper class for writing the `local subroutine INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#9-local-and-global-subr-indexes>`_
737    to binary."""
738
739    def setPos(self, pos, endPos):
740        offset = pos - self.parent.pos
741        self.parent.rawDict["Subrs"] = offset
742
743
744class CharStringsCompiler(GlobalSubrsCompiler):
745    """Helper class for writing the `CharStrings INDEX <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#9-local-and-global-subr-indexes>`_
746    to binary."""
747
748    def getItems(self, items, strings):
749        out = []
750        for cs in items:
751            cs.compile(self.isCFF2)
752            out.append(cs.bytecode)
753        return out
754
755    def setPos(self, pos, endPos):
756        self.parent.rawDict["CharStrings"] = pos
757
758
759class Index(object):
760    """This class represents what the CFF spec calls an INDEX (an array of
761    variable-sized objects). `Index` items can be addressed and set using
762    Python list indexing."""
763
764    compilerClass = IndexCompiler
765
766    def __init__(self, file=None, isCFF2=None):
767        assert (isCFF2 is None) == (file is None)
768        self.items = []
769        name = self.__class__.__name__
770        if file is None:
771            return
772        self._isCFF2 = isCFF2
773        log.log(DEBUG, "loading %s at %s", name, file.tell())
774        self.file = file
775        if isCFF2:
776            count = readCard32(file)
777        else:
778            count = readCard16(file)
779        if count == 0:
780            return
781        self.items = [None] * count
782        offSize = readCard8(file)
783        log.log(DEBUG, "    index count: %s offSize: %s", count, offSize)
784        assert offSize <= 4, "offSize too large: %s" % offSize
785        self.offsets = offsets = []
786        pad = b"\0" * (4 - offSize)
787        for index in range(count + 1):
788            chunk = file.read(offSize)
789            chunk = pad + chunk
790            (offset,) = struct.unpack(">L", chunk)
791            offsets.append(int(offset))
792        self.offsetBase = file.tell() - 1
793        file.seek(self.offsetBase + offsets[-1])  # pretend we've read the whole lot
794        log.log(DEBUG, "    end of %s at %s", name, file.tell())
795
796    def __len__(self):
797        return len(self.items)
798
799    def __getitem__(self, index):
800        item = self.items[index]
801        if item is not None:
802            return item
803        offset = self.offsets[index] + self.offsetBase
804        size = self.offsets[index + 1] - self.offsets[index]
805        file = self.file
806        file.seek(offset)
807        data = file.read(size)
808        assert len(data) == size
809        item = self.produceItem(index, data, file, offset)
810        self.items[index] = item
811        return item
812
813    def __setitem__(self, index, item):
814        self.items[index] = item
815
816    def produceItem(self, index, data, file, offset):
817        return data
818
819    def append(self, item):
820        """Add an item to an INDEX."""
821        self.items.append(item)
822
823    def getCompiler(self, strings, parent, isCFF2=None):
824        return self.compilerClass(self, strings, parent, isCFF2=isCFF2)
825
826    def clear(self):
827        """Empty the INDEX."""
828        del self.items[:]
829
830
831class GlobalSubrsIndex(Index):
832    """This index contains all the global subroutines in the font. A global
833    subroutine is a set of ``CharString`` data which is accessible to any
834    glyph in the font, and are used to store repeated instructions - for
835    example, components may be encoded as global subroutines, but so could
836    hinting instructions.
837
838    Remember that when interpreting a ``callgsubr`` instruction (or indeed
839    a ``callsubr`` instruction) that you will need to add the "subroutine
840    number bias" to number given:
841
842    .. code:: python
843
844            tt = ttLib.TTFont("Almendra-Bold.otf")
845            u = tt["CFF "].cff[0].CharStrings["udieresis"]
846            u.decompile()
847
848            u.toXML(XMLWriter(sys.stdout))
849            # <some stuff>
850            # -64 callgsubr <-- Subroutine which implements the dieresis mark
851            # <other stuff>
852
853            tt["CFF "].cff[0].GlobalSubrs[-64] # <-- WRONG
854            # <T2CharString (bytecode) at 103451d10>
855
856            tt["CFF "].cff[0].GlobalSubrs[-64 + 107] # <-- RIGHT
857            # <T2CharString (source) at 103451390>
858
859    ("The bias applied depends on the number of subrs (gsubrs). If the number of
860    subrs (gsubrs) is less than 1240, the bias is 107. Otherwise if it is less
861    than 33900, it is 1131; otherwise it is 32768.",
862    `Subroutine Operators <https://docs.microsoft.com/en-us/typography/opentype/otspec180/cff2charstr#section4.4>`)
863    """
864
865    compilerClass = GlobalSubrsCompiler
866    subrClass = psCharStrings.T2CharString
867    charStringClass = psCharStrings.T2CharString
868
869    def __init__(
870        self,
871        file=None,
872        globalSubrs=None,
873        private=None,
874        fdSelect=None,
875        fdArray=None,
876        isCFF2=None,
877    ):
878        super(GlobalSubrsIndex, self).__init__(file, isCFF2=isCFF2)
879        self.globalSubrs = globalSubrs
880        self.private = private
881        if fdSelect:
882            self.fdSelect = fdSelect
883        if fdArray:
884            self.fdArray = fdArray
885
886    def produceItem(self, index, data, file, offset):
887        if self.private is not None:
888            private = self.private
889        elif hasattr(self, "fdArray") and self.fdArray is not None:
890            if hasattr(self, "fdSelect") and self.fdSelect is not None:
891                fdIndex = self.fdSelect[index]
892            else:
893                fdIndex = 0
894            private = self.fdArray[fdIndex].Private
895        else:
896            private = None
897        return self.subrClass(data, private=private, globalSubrs=self.globalSubrs)
898
899    def toXML(self, xmlWriter):
900        """Write the subroutines index into XML representation onto the given
901        :class:`fontTools.misc.xmlWriter.XMLWriter`.
902
903        .. code:: python
904
905                writer = xmlWriter.XMLWriter(sys.stdout)
906                tt["CFF "].cff[0].GlobalSubrs.toXML(writer)
907
908        """
909        xmlWriter.comment(
910            "The 'index' attribute is only for humans; " "it is ignored when parsed."
911        )
912        xmlWriter.newline()
913        for i in range(len(self)):
914            subr = self[i]
915            if subr.needsDecompilation():
916                xmlWriter.begintag("CharString", index=i, raw=1)
917            else:
918                xmlWriter.begintag("CharString", index=i)
919            xmlWriter.newline()
920            subr.toXML(xmlWriter)
921            xmlWriter.endtag("CharString")
922            xmlWriter.newline()
923
924    def fromXML(self, name, attrs, content):
925        if name != "CharString":
926            return
927        subr = self.subrClass()
928        subr.fromXML(name, attrs, content)
929        self.append(subr)
930
931    def getItemAndSelector(self, index):
932        sel = None
933        if hasattr(self, "fdSelect"):
934            sel = self.fdSelect[index]
935        return self[index], sel
936
937
938class SubrsIndex(GlobalSubrsIndex):
939    """This index contains a glyph's local subroutines. A local subroutine is a
940    private set of ``CharString`` data which is accessible only to the glyph to
941    which the index is attached."""
942
943    compilerClass = SubrsCompiler
944
945
946class TopDictIndex(Index):
947    """This index represents the array of ``TopDict`` structures in the font
948    (again, usually only one entry is present). Hence the following calls are
949    equivalent:
950
951    .. code:: python
952
953            tt["CFF "].cff[0]
954            # <fontTools.cffLib.TopDict object at 0x102ed6e50>
955            tt["CFF "].cff.topDictIndex[0]
956            # <fontTools.cffLib.TopDict object at 0x102ed6e50>
957
958    """
959
960    compilerClass = TopDictIndexCompiler
961
962    def __init__(self, file=None, cff2GetGlyphOrder=None, topSize=0, isCFF2=None):
963        assert (isCFF2 is None) == (file is None)
964        self.cff2GetGlyphOrder = cff2GetGlyphOrder
965        if file is not None and isCFF2:
966            self._isCFF2 = isCFF2
967            self.items = []
968            name = self.__class__.__name__
969            log.log(DEBUG, "loading %s at %s", name, file.tell())
970            self.file = file
971            count = 1
972            self.items = [None] * count
973            self.offsets = [0, topSize]
974            self.offsetBase = file.tell()
975            # pretend we've read the whole lot
976            file.seek(self.offsetBase + topSize)
977            log.log(DEBUG, "    end of %s at %s", name, file.tell())
978        else:
979            super(TopDictIndex, self).__init__(file, isCFF2=isCFF2)
980
981    def produceItem(self, index, data, file, offset):
982        top = TopDict(
983            self.strings,
984            file,
985            offset,
986            self.GlobalSubrs,
987            self.cff2GetGlyphOrder,
988            isCFF2=self._isCFF2,
989        )
990        top.decompile(data)
991        return top
992
993    def toXML(self, xmlWriter):
994        for i in range(len(self)):
995            xmlWriter.begintag("FontDict", index=i)
996            xmlWriter.newline()
997            self[i].toXML(xmlWriter)
998            xmlWriter.endtag("FontDict")
999            xmlWriter.newline()
1000
1001
1002class FDArrayIndex(Index):
1003    compilerClass = FDArrayIndexCompiler
1004
1005    def toXML(self, xmlWriter):
1006        for i in range(len(self)):
1007            xmlWriter.begintag("FontDict", index=i)
1008            xmlWriter.newline()
1009            self[i].toXML(xmlWriter)
1010            xmlWriter.endtag("FontDict")
1011            xmlWriter.newline()
1012
1013    def produceItem(self, index, data, file, offset):
1014        fontDict = FontDict(
1015            self.strings,
1016            file,
1017            offset,
1018            self.GlobalSubrs,
1019            isCFF2=self._isCFF2,
1020            vstore=self.vstore,
1021        )
1022        fontDict.decompile(data)
1023        return fontDict
1024
1025    def fromXML(self, name, attrs, content):
1026        if name != "FontDict":
1027            return
1028        fontDict = FontDict()
1029        for element in content:
1030            if isinstance(element, str):
1031                continue
1032            name, attrs, content = element
1033            fontDict.fromXML(name, attrs, content)
1034        self.append(fontDict)
1035
1036
1037class VarStoreData(object):
1038    def __init__(self, file=None, otVarStore=None):
1039        self.file = file
1040        self.data = None
1041        self.otVarStore = otVarStore
1042        self.font = TTFont()  # dummy font for the decompile function.
1043
1044    def decompile(self):
1045        if self.file:
1046            # read data in from file. Assume position is correct.
1047            length = readCard16(self.file)
1048            self.data = self.file.read(length)
1049            globalState = {}
1050            reader = OTTableReader(self.data, globalState)
1051            self.otVarStore = ot.VarStore()
1052            self.otVarStore.decompile(reader, self.font)
1053        return self
1054
1055    def compile(self):
1056        writer = OTTableWriter()
1057        self.otVarStore.compile(writer, self.font)
1058        # Note that this omits the initial Card16 length from the CFF2
1059        # VarStore data block
1060        self.data = writer.getAllData()
1061
1062    def writeXML(self, xmlWriter, name):
1063        self.otVarStore.toXML(xmlWriter, self.font)
1064
1065    def xmlRead(self, name, attrs, content, parent):
1066        self.otVarStore = ot.VarStore()
1067        for element in content:
1068            if isinstance(element, tuple):
1069                name, attrs, content = element
1070                self.otVarStore.fromXML(name, attrs, content, self.font)
1071            else:
1072                pass
1073        return None
1074
1075    def __len__(self):
1076        return len(self.data)
1077
1078    def getNumRegions(self, vsIndex):
1079        if vsIndex is None:
1080            vsIndex = 0
1081        varData = self.otVarStore.VarData[vsIndex]
1082        numRegions = varData.VarRegionCount
1083        return numRegions
1084
1085
1086class FDSelect(object):
1087    def __init__(self, file=None, numGlyphs=None, format=None):
1088        if file:
1089            # read data in from file
1090            self.format = readCard8(file)
1091            if self.format == 0:
1092                from array import array
1093
1094                self.gidArray = array("B", file.read(numGlyphs)).tolist()
1095            elif self.format == 3:
1096                gidArray = [None] * numGlyphs
1097                nRanges = readCard16(file)
1098                fd = None
1099                prev = None
1100                for i in range(nRanges):
1101                    first = readCard16(file)
1102                    if prev is not None:
1103                        for glyphID in range(prev, first):
1104                            gidArray[glyphID] = fd
1105                    prev = first
1106                    fd = readCard8(file)
1107                if prev is not None:
1108                    first = readCard16(file)
1109                    for glyphID in range(prev, first):
1110                        gidArray[glyphID] = fd
1111                self.gidArray = gidArray
1112            elif self.format == 4:
1113                gidArray = [None] * numGlyphs
1114                nRanges = readCard32(file)
1115                fd = None
1116                prev = None
1117                for i in range(nRanges):
1118                    first = readCard32(file)
1119                    if prev is not None:
1120                        for glyphID in range(prev, first):
1121                            gidArray[glyphID] = fd
1122                    prev = first
1123                    fd = readCard16(file)
1124                if prev is not None:
1125                    first = readCard32(file)
1126                    for glyphID in range(prev, first):
1127                        gidArray[glyphID] = fd
1128                self.gidArray = gidArray
1129            else:
1130                assert False, "unsupported FDSelect format: %s" % format
1131        else:
1132            # reading from XML. Make empty gidArray, and leave format as passed in.
1133            # format is None will result in the smallest representation being used.
1134            self.format = format
1135            self.gidArray = []
1136
1137    def __len__(self):
1138        return len(self.gidArray)
1139
1140    def __getitem__(self, index):
1141        return self.gidArray[index]
1142
1143    def __setitem__(self, index, fdSelectValue):
1144        self.gidArray[index] = fdSelectValue
1145
1146    def append(self, fdSelectValue):
1147        self.gidArray.append(fdSelectValue)
1148
1149
1150class CharStrings(object):
1151    """The ``CharStrings`` in the font represent the instructions for drawing
1152    each glyph. This object presents a dictionary interface to the font's
1153    CharStrings, indexed by glyph name:
1154
1155    .. code:: python
1156
1157            tt["CFF "].cff[0].CharStrings["a"]
1158            # <T2CharString (bytecode) at 103451e90>
1159
1160    See :class:`fontTools.misc.psCharStrings.T1CharString` and
1161    :class:`fontTools.misc.psCharStrings.T2CharString` for how to decompile,
1162    compile and interpret the glyph drawing instructions in the returned objects.
1163
1164    """
1165
1166    def __init__(
1167        self,
1168        file,
1169        charset,
1170        globalSubrs,
1171        private,
1172        fdSelect,
1173        fdArray,
1174        isCFF2=None,
1175        varStore=None,
1176    ):
1177        self.globalSubrs = globalSubrs
1178        self.varStore = varStore
1179        if file is not None:
1180            self.charStringsIndex = SubrsIndex(
1181                file, globalSubrs, private, fdSelect, fdArray, isCFF2=isCFF2
1182            )
1183            self.charStrings = charStrings = {}
1184            for i in range(len(charset)):
1185                charStrings[charset[i]] = i
1186            # read from OTF file: charStrings.values() are indices into
1187            # charStringsIndex.
1188            self.charStringsAreIndexed = 1
1189        else:
1190            self.charStrings = {}
1191            # read from ttx file: charStrings.values() are actual charstrings
1192            self.charStringsAreIndexed = 0
1193            self.private = private
1194            if fdSelect is not None:
1195                self.fdSelect = fdSelect
1196            if fdArray is not None:
1197                self.fdArray = fdArray
1198
1199    def keys(self):
1200        return list(self.charStrings.keys())
1201
1202    def values(self):
1203        if self.charStringsAreIndexed:
1204            return self.charStringsIndex
1205        else:
1206            return list(self.charStrings.values())
1207
1208    def has_key(self, name):
1209        return name in self.charStrings
1210
1211    __contains__ = has_key
1212
1213    def __len__(self):
1214        return len(self.charStrings)
1215
1216    def __getitem__(self, name):
1217        charString = self.charStrings[name]
1218        if self.charStringsAreIndexed:
1219            charString = self.charStringsIndex[charString]
1220        return charString
1221
1222    def __setitem__(self, name, charString):
1223        if self.charStringsAreIndexed:
1224            index = self.charStrings[name]
1225            self.charStringsIndex[index] = charString
1226        else:
1227            self.charStrings[name] = charString
1228
1229    def getItemAndSelector(self, name):
1230        if self.charStringsAreIndexed:
1231            index = self.charStrings[name]
1232            return self.charStringsIndex.getItemAndSelector(index)
1233        else:
1234            if hasattr(self, "fdArray"):
1235                if hasattr(self, "fdSelect"):
1236                    sel = self.charStrings[name].fdSelectIndex
1237                else:
1238                    sel = 0
1239            else:
1240                sel = None
1241            return self.charStrings[name], sel
1242
1243    def toXML(self, xmlWriter):
1244        names = sorted(self.keys())
1245        for name in names:
1246            charStr, fdSelectIndex = self.getItemAndSelector(name)
1247            if charStr.needsDecompilation():
1248                raw = [("raw", 1)]
1249            else:
1250                raw = []
1251            if fdSelectIndex is None:
1252                xmlWriter.begintag("CharString", [("name", name)] + raw)
1253            else:
1254                xmlWriter.begintag(
1255                    "CharString",
1256                    [("name", name), ("fdSelectIndex", fdSelectIndex)] + raw,
1257                )
1258            xmlWriter.newline()
1259            charStr.toXML(xmlWriter)
1260            xmlWriter.endtag("CharString")
1261            xmlWriter.newline()
1262
1263    def fromXML(self, name, attrs, content):
1264        for element in content:
1265            if isinstance(element, str):
1266                continue
1267            name, attrs, content = element
1268            if name != "CharString":
1269                continue
1270            fdID = -1
1271            if hasattr(self, "fdArray"):
1272                try:
1273                    fdID = safeEval(attrs["fdSelectIndex"])
1274                except KeyError:
1275                    fdID = 0
1276                private = self.fdArray[fdID].Private
1277            else:
1278                private = self.private
1279
1280            glyphName = attrs["name"]
1281            charStringClass = psCharStrings.T2CharString
1282            charString = charStringClass(private=private, globalSubrs=self.globalSubrs)
1283            charString.fromXML(name, attrs, content)
1284            if fdID >= 0:
1285                charString.fdSelectIndex = fdID
1286            self[glyphName] = charString
1287
1288
1289def readCard8(file):
1290    return byteord(file.read(1))
1291
1292
1293def readCard16(file):
1294    (value,) = struct.unpack(">H", file.read(2))
1295    return value
1296
1297
1298def readCard32(file):
1299    (value,) = struct.unpack(">L", file.read(4))
1300    return value
1301
1302
1303def writeCard8(file, value):
1304    file.write(bytechr(value))
1305
1306
1307def writeCard16(file, value):
1308    file.write(struct.pack(">H", value))
1309
1310
1311def writeCard32(file, value):
1312    file.write(struct.pack(">L", value))
1313
1314
1315def packCard8(value):
1316    return bytechr(value)
1317
1318
1319def packCard16(value):
1320    return struct.pack(">H", value)
1321
1322
1323def packCard32(value):
1324    return struct.pack(">L", value)
1325
1326
1327def buildOperatorDict(table):
1328    d = {}
1329    for op, name, arg, default, conv in table:
1330        d[op] = (name, arg)
1331    return d
1332
1333
1334def buildOpcodeDict(table):
1335    d = {}
1336    for op, name, arg, default, conv in table:
1337        if isinstance(op, tuple):
1338            op = bytechr(op[0]) + bytechr(op[1])
1339        else:
1340            op = bytechr(op)
1341        d[name] = (op, arg)
1342    return d
1343
1344
1345def buildOrder(table):
1346    l = []
1347    for op, name, arg, default, conv in table:
1348        l.append(name)
1349    return l
1350
1351
1352def buildDefaults(table):
1353    d = {}
1354    for op, name, arg, default, conv in table:
1355        if default is not None:
1356            d[name] = default
1357    return d
1358
1359
1360def buildConverters(table):
1361    d = {}
1362    for op, name, arg, default, conv in table:
1363        d[name] = conv
1364    return d
1365
1366
1367class SimpleConverter(object):
1368    def read(self, parent, value):
1369        if not hasattr(parent, "file"):
1370            return self._read(parent, value)
1371        file = parent.file
1372        pos = file.tell()
1373        try:
1374            return self._read(parent, value)
1375        finally:
1376            file.seek(pos)
1377
1378    def _read(self, parent, value):
1379        return value
1380
1381    def write(self, parent, value):
1382        return value
1383
1384    def xmlWrite(self, xmlWriter, name, value):
1385        xmlWriter.simpletag(name, value=value)
1386        xmlWriter.newline()
1387
1388    def xmlRead(self, name, attrs, content, parent):
1389        return attrs["value"]
1390
1391
1392class ASCIIConverter(SimpleConverter):
1393    def _read(self, parent, value):
1394        return tostr(value, encoding="ascii")
1395
1396    def write(self, parent, value):
1397        return tobytes(value, encoding="ascii")
1398
1399    def xmlWrite(self, xmlWriter, name, value):
1400        xmlWriter.simpletag(name, value=tostr(value, encoding="ascii"))
1401        xmlWriter.newline()
1402
1403    def xmlRead(self, name, attrs, content, parent):
1404        return tobytes(attrs["value"], encoding=("ascii"))
1405
1406
1407class Latin1Converter(SimpleConverter):
1408    def _read(self, parent, value):
1409        return tostr(value, encoding="latin1")
1410
1411    def write(self, parent, value):
1412        return tobytes(value, encoding="latin1")
1413
1414    def xmlWrite(self, xmlWriter, name, value):
1415        value = tostr(value, encoding="latin1")
1416        if name in ["Notice", "Copyright"]:
1417            value = re.sub(r"[\r\n]\s+", " ", value)
1418        xmlWriter.simpletag(name, value=value)
1419        xmlWriter.newline()
1420
1421    def xmlRead(self, name, attrs, content, parent):
1422        return tobytes(attrs["value"], encoding=("latin1"))
1423
1424
1425def parseNum(s):
1426    try:
1427        value = int(s)
1428    except:
1429        value = float(s)
1430    return value
1431
1432
1433def parseBlendList(s):
1434    valueList = []
1435    for element in s:
1436        if isinstance(element, str):
1437            continue
1438        name, attrs, content = element
1439        blendList = attrs["value"].split()
1440        blendList = [eval(val) for val in blendList]
1441        valueList.append(blendList)
1442    if len(valueList) == 1:
1443        valueList = valueList[0]
1444    return valueList
1445
1446
1447class NumberConverter(SimpleConverter):
1448    def xmlWrite(self, xmlWriter, name, value):
1449        if isinstance(value, list):
1450            xmlWriter.begintag(name)
1451            xmlWriter.newline()
1452            xmlWriter.indent()
1453            blendValue = " ".join([str(val) for val in value])
1454            xmlWriter.simpletag(kBlendDictOpName, value=blendValue)
1455            xmlWriter.newline()
1456            xmlWriter.dedent()
1457            xmlWriter.endtag(name)
1458            xmlWriter.newline()
1459        else:
1460            xmlWriter.simpletag(name, value=value)
1461            xmlWriter.newline()
1462
1463    def xmlRead(self, name, attrs, content, parent):
1464        valueString = attrs.get("value", None)
1465        if valueString is None:
1466            value = parseBlendList(content)
1467        else:
1468            value = parseNum(attrs["value"])
1469        return value
1470
1471
1472class ArrayConverter(SimpleConverter):
1473    def xmlWrite(self, xmlWriter, name, value):
1474        if value and isinstance(value[0], list):
1475            xmlWriter.begintag(name)
1476            xmlWriter.newline()
1477            xmlWriter.indent()
1478            for valueList in value:
1479                blendValue = " ".join([str(val) for val in valueList])
1480                xmlWriter.simpletag(kBlendDictOpName, value=blendValue)
1481                xmlWriter.newline()
1482            xmlWriter.dedent()
1483            xmlWriter.endtag(name)
1484            xmlWriter.newline()
1485        else:
1486            value = " ".join([str(val) for val in value])
1487            xmlWriter.simpletag(name, value=value)
1488            xmlWriter.newline()
1489
1490    def xmlRead(self, name, attrs, content, parent):
1491        valueString = attrs.get("value", None)
1492        if valueString is None:
1493            valueList = parseBlendList(content)
1494        else:
1495            values = valueString.split()
1496            valueList = [parseNum(value) for value in values]
1497        return valueList
1498
1499
1500class TableConverter(SimpleConverter):
1501    def xmlWrite(self, xmlWriter, name, value):
1502        xmlWriter.begintag(name)
1503        xmlWriter.newline()
1504        value.toXML(xmlWriter)
1505        xmlWriter.endtag(name)
1506        xmlWriter.newline()
1507
1508    def xmlRead(self, name, attrs, content, parent):
1509        ob = self.getClass()()
1510        for element in content:
1511            if isinstance(element, str):
1512                continue
1513            name, attrs, content = element
1514            ob.fromXML(name, attrs, content)
1515        return ob
1516
1517
1518class PrivateDictConverter(TableConverter):
1519    def getClass(self):
1520        return PrivateDict
1521
1522    def _read(self, parent, value):
1523        size, offset = value
1524        file = parent.file
1525        isCFF2 = parent._isCFF2
1526        try:
1527            vstore = parent.vstore
1528        except AttributeError:
1529            vstore = None
1530        priv = PrivateDict(parent.strings, file, offset, isCFF2=isCFF2, vstore=vstore)
1531        file.seek(offset)
1532        data = file.read(size)
1533        assert len(data) == size
1534        priv.decompile(data)
1535        return priv
1536
1537    def write(self, parent, value):
1538        return (0, 0)  # dummy value
1539
1540
1541class SubrsConverter(TableConverter):
1542    def getClass(self):
1543        return SubrsIndex
1544
1545    def _read(self, parent, value):
1546        file = parent.file
1547        isCFF2 = parent._isCFF2
1548        file.seek(parent.offset + value)  # Offset(self)
1549        return SubrsIndex(file, isCFF2=isCFF2)
1550
1551    def write(self, parent, value):
1552        return 0  # dummy value
1553
1554
1555class CharStringsConverter(TableConverter):
1556    def _read(self, parent, value):
1557        file = parent.file
1558        isCFF2 = parent._isCFF2
1559        charset = parent.charset
1560        varStore = getattr(parent, "VarStore", None)
1561        globalSubrs = parent.GlobalSubrs
1562        if hasattr(parent, "FDArray"):
1563            fdArray = parent.FDArray
1564            if hasattr(parent, "FDSelect"):
1565                fdSelect = parent.FDSelect
1566            else:
1567                fdSelect = None
1568            private = None
1569        else:
1570            fdSelect, fdArray = None, None
1571            private = parent.Private
1572        file.seek(value)  # Offset(0)
1573        charStrings = CharStrings(
1574            file,
1575            charset,
1576            globalSubrs,
1577            private,
1578            fdSelect,
1579            fdArray,
1580            isCFF2=isCFF2,
1581            varStore=varStore,
1582        )
1583        return charStrings
1584
1585    def write(self, parent, value):
1586        return 0  # dummy value
1587
1588    def xmlRead(self, name, attrs, content, parent):
1589        if hasattr(parent, "FDArray"):
1590            # if it is a CID-keyed font, then the private Dict is extracted from the
1591            # parent.FDArray
1592            fdArray = parent.FDArray
1593            if hasattr(parent, "FDSelect"):
1594                fdSelect = parent.FDSelect
1595            else:
1596                fdSelect = None
1597            private = None
1598        else:
1599            # if it is a name-keyed font, then the private dict is in the top dict,
1600            # and
1601            # there is no fdArray.
1602            private, fdSelect, fdArray = parent.Private, None, None
1603        charStrings = CharStrings(
1604            None,
1605            None,
1606            parent.GlobalSubrs,
1607            private,
1608            fdSelect,
1609            fdArray,
1610            varStore=getattr(parent, "VarStore", None),
1611        )
1612        charStrings.fromXML(name, attrs, content)
1613        return charStrings
1614
1615
1616class CharsetConverter(SimpleConverter):
1617    def _read(self, parent, value):
1618        isCID = hasattr(parent, "ROS")
1619        if value > 2:
1620            numGlyphs = parent.numGlyphs
1621            file = parent.file
1622            file.seek(value)
1623            log.log(DEBUG, "loading charset at %s", value)
1624            format = readCard8(file)
1625            if format == 0:
1626                charset = parseCharset0(numGlyphs, file, parent.strings, isCID)
1627            elif format == 1 or format == 2:
1628                charset = parseCharset(numGlyphs, file, parent.strings, isCID, format)
1629            else:
1630                raise NotImplementedError
1631            assert len(charset) == numGlyphs
1632            log.log(DEBUG, "    charset end at %s", file.tell())
1633            # make sure glyph names are unique
1634            allNames = {}
1635            newCharset = []
1636            for glyphName in charset:
1637                if glyphName in allNames:
1638                    # make up a new glyphName that's unique
1639                    n = allNames[glyphName]
1640                    while (glyphName + "#" + str(n)) in allNames:
1641                        n += 1
1642                    allNames[glyphName] = n + 1
1643                    glyphName = glyphName + "#" + str(n)
1644                allNames[glyphName] = 1
1645                newCharset.append(glyphName)
1646            charset = newCharset
1647        else:  # offset == 0 -> no charset data.
1648            if isCID or "CharStrings" not in parent.rawDict:
1649                # We get here only when processing fontDicts from the FDArray of
1650                # CFF-CID fonts. Only the real topDict references the chrset.
1651                assert value == 0
1652                charset = None
1653            elif value == 0:
1654                charset = cffISOAdobeStrings
1655            elif value == 1:
1656                charset = cffIExpertStrings
1657            elif value == 2:
1658                charset = cffExpertSubsetStrings
1659        if charset and (len(charset) != parent.numGlyphs):
1660            charset = charset[: parent.numGlyphs]
1661        return charset
1662
1663    def write(self, parent, value):
1664        return 0  # dummy value
1665
1666    def xmlWrite(self, xmlWriter, name, value):
1667        # XXX only write charset when not in OT/TTX context, where we
1668        # dump charset as a separate "GlyphOrder" table.
1669        # # xmlWriter.simpletag("charset")
1670        xmlWriter.comment("charset is dumped separately as the 'GlyphOrder' element")
1671        xmlWriter.newline()
1672
1673    def xmlRead(self, name, attrs, content, parent):
1674        pass
1675
1676
1677class CharsetCompiler(object):
1678    def __init__(self, strings, charset, parent):
1679        assert charset[0] == ".notdef"
1680        isCID = hasattr(parent.dictObj, "ROS")
1681        data0 = packCharset0(charset, isCID, strings)
1682        data = packCharset(charset, isCID, strings)
1683        if len(data) < len(data0):
1684            self.data = data
1685        else:
1686            self.data = data0
1687        self.parent = parent
1688
1689    def setPos(self, pos, endPos):
1690        self.parent.rawDict["charset"] = pos
1691
1692    def getDataLength(self):
1693        return len(self.data)
1694
1695    def toFile(self, file):
1696        file.write(self.data)
1697
1698
1699def getStdCharSet(charset):
1700    # check to see if we can use a predefined charset value.
1701    predefinedCharSetVal = None
1702    predefinedCharSets = [
1703        (cffISOAdobeStringCount, cffISOAdobeStrings, 0),
1704        (cffExpertStringCount, cffIExpertStrings, 1),
1705        (cffExpertSubsetStringCount, cffExpertSubsetStrings, 2),
1706    ]
1707    lcs = len(charset)
1708    for cnt, pcs, csv in predefinedCharSets:
1709        if predefinedCharSetVal is not None:
1710            break
1711        if lcs > cnt:
1712            continue
1713        predefinedCharSetVal = csv
1714        for i in range(lcs):
1715            if charset[i] != pcs[i]:
1716                predefinedCharSetVal = None
1717                break
1718    return predefinedCharSetVal
1719
1720
1721def getCIDfromName(name, strings):
1722    return int(name[3:])
1723
1724
1725def getSIDfromName(name, strings):
1726    return strings.getSID(name)
1727
1728
1729def packCharset0(charset, isCID, strings):
1730    fmt = 0
1731    data = [packCard8(fmt)]
1732    if isCID:
1733        getNameID = getCIDfromName
1734    else:
1735        getNameID = getSIDfromName
1736
1737    for name in charset[1:]:
1738        data.append(packCard16(getNameID(name, strings)))
1739    return bytesjoin(data)
1740
1741
1742def packCharset(charset, isCID, strings):
1743    fmt = 1
1744    ranges = []
1745    first = None
1746    end = 0
1747    if isCID:
1748        getNameID = getCIDfromName
1749    else:
1750        getNameID = getSIDfromName
1751
1752    for name in charset[1:]:
1753        SID = getNameID(name, strings)
1754        if first is None:
1755            first = SID
1756        elif end + 1 != SID:
1757            nLeft = end - first
1758            if nLeft > 255:
1759                fmt = 2
1760            ranges.append((first, nLeft))
1761            first = SID
1762        end = SID
1763    if end:
1764        nLeft = end - first
1765        if nLeft > 255:
1766            fmt = 2
1767        ranges.append((first, nLeft))
1768
1769    data = [packCard8(fmt)]
1770    if fmt == 1:
1771        nLeftFunc = packCard8
1772    else:
1773        nLeftFunc = packCard16
1774    for first, nLeft in ranges:
1775        data.append(packCard16(first) + nLeftFunc(nLeft))
1776    return bytesjoin(data)
1777
1778
1779def parseCharset0(numGlyphs, file, strings, isCID):
1780    charset = [".notdef"]
1781    if isCID:
1782        for i in range(numGlyphs - 1):
1783            CID = readCard16(file)
1784            charset.append("cid" + str(CID).zfill(5))
1785    else:
1786        for i in range(numGlyphs - 1):
1787            SID = readCard16(file)
1788            charset.append(strings[SID])
1789    return charset
1790
1791
1792def parseCharset(numGlyphs, file, strings, isCID, fmt):
1793    charset = [".notdef"]
1794    count = 1
1795    if fmt == 1:
1796        nLeftFunc = readCard8
1797    else:
1798        nLeftFunc = readCard16
1799    while count < numGlyphs:
1800        first = readCard16(file)
1801        nLeft = nLeftFunc(file)
1802        if isCID:
1803            for CID in range(first, first + nLeft + 1):
1804                charset.append("cid" + str(CID).zfill(5))
1805        else:
1806            for SID in range(first, first + nLeft + 1):
1807                charset.append(strings[SID])
1808        count = count + nLeft + 1
1809    return charset
1810
1811
1812class EncodingCompiler(object):
1813    def __init__(self, strings, encoding, parent):
1814        assert not isinstance(encoding, str)
1815        data0 = packEncoding0(parent.dictObj.charset, encoding, parent.strings)
1816        data1 = packEncoding1(parent.dictObj.charset, encoding, parent.strings)
1817        if len(data0) < len(data1):
1818            self.data = data0
1819        else:
1820            self.data = data1
1821        self.parent = parent
1822
1823    def setPos(self, pos, endPos):
1824        self.parent.rawDict["Encoding"] = pos
1825
1826    def getDataLength(self):
1827        return len(self.data)
1828
1829    def toFile(self, file):
1830        file.write(self.data)
1831
1832
1833class EncodingConverter(SimpleConverter):
1834    def _read(self, parent, value):
1835        if value == 0:
1836            return "StandardEncoding"
1837        elif value == 1:
1838            return "ExpertEncoding"
1839        else:
1840            assert value > 1
1841            file = parent.file
1842            file.seek(value)
1843            log.log(DEBUG, "loading Encoding at %s", value)
1844            fmt = readCard8(file)
1845            haveSupplement = fmt & 0x80
1846            if haveSupplement:
1847                raise NotImplementedError("Encoding supplements are not yet supported")
1848            fmt = fmt & 0x7F
1849            if fmt == 0:
1850                encoding = parseEncoding0(
1851                    parent.charset, file, haveSupplement, parent.strings
1852                )
1853            elif fmt == 1:
1854                encoding = parseEncoding1(
1855                    parent.charset, file, haveSupplement, parent.strings
1856                )
1857            return encoding
1858
1859    def write(self, parent, value):
1860        if value == "StandardEncoding":
1861            return 0
1862        elif value == "ExpertEncoding":
1863            return 1
1864        return 0  # dummy value
1865
1866    def xmlWrite(self, xmlWriter, name, value):
1867        if value in ("StandardEncoding", "ExpertEncoding"):
1868            xmlWriter.simpletag(name, name=value)
1869            xmlWriter.newline()
1870            return
1871        xmlWriter.begintag(name)
1872        xmlWriter.newline()
1873        for code in range(len(value)):
1874            glyphName = value[code]
1875            if glyphName != ".notdef":
1876                xmlWriter.simpletag("map", code=hex(code), name=glyphName)
1877                xmlWriter.newline()
1878        xmlWriter.endtag(name)
1879        xmlWriter.newline()
1880
1881    def xmlRead(self, name, attrs, content, parent):
1882        if "name" in attrs:
1883            return attrs["name"]
1884        encoding = [".notdef"] * 256
1885        for element in content:
1886            if isinstance(element, str):
1887                continue
1888            name, attrs, content = element
1889            code = safeEval(attrs["code"])
1890            glyphName = attrs["name"]
1891            encoding[code] = glyphName
1892        return encoding
1893
1894
1895def parseEncoding0(charset, file, haveSupplement, strings):
1896    nCodes = readCard8(file)
1897    encoding = [".notdef"] * 256
1898    for glyphID in range(1, nCodes + 1):
1899        code = readCard8(file)
1900        if code != 0:
1901            encoding[code] = charset[glyphID]
1902    return encoding
1903
1904
1905def parseEncoding1(charset, file, haveSupplement, strings):
1906    nRanges = readCard8(file)
1907    encoding = [".notdef"] * 256
1908    glyphID = 1
1909    for i in range(nRanges):
1910        code = readCard8(file)
1911        nLeft = readCard8(file)
1912        for glyphID in range(glyphID, glyphID + nLeft + 1):
1913            encoding[code] = charset[glyphID]
1914            code = code + 1
1915        glyphID = glyphID + 1
1916    return encoding
1917
1918
1919def packEncoding0(charset, encoding, strings):
1920    fmt = 0
1921    m = {}
1922    for code in range(len(encoding)):
1923        name = encoding[code]
1924        if name != ".notdef":
1925            m[name] = code
1926    codes = []
1927    for name in charset[1:]:
1928        code = m.get(name)
1929        codes.append(code)
1930
1931    while codes and codes[-1] is None:
1932        codes.pop()
1933
1934    data = [packCard8(fmt), packCard8(len(codes))]
1935    for code in codes:
1936        if code is None:
1937            code = 0
1938        data.append(packCard8(code))
1939    return bytesjoin(data)
1940
1941
1942def packEncoding1(charset, encoding, strings):
1943    fmt = 1
1944    m = {}
1945    for code in range(len(encoding)):
1946        name = encoding[code]
1947        if name != ".notdef":
1948            m[name] = code
1949    ranges = []
1950    first = None
1951    end = 0
1952    for name in charset[1:]:
1953        code = m.get(name, -1)
1954        if first is None:
1955            first = code
1956        elif end + 1 != code:
1957            nLeft = end - first
1958            ranges.append((first, nLeft))
1959            first = code
1960        end = code
1961    nLeft = end - first
1962    ranges.append((first, nLeft))
1963
1964    # remove unencoded glyphs at the end.
1965    while ranges and ranges[-1][0] == -1:
1966        ranges.pop()
1967
1968    data = [packCard8(fmt), packCard8(len(ranges))]
1969    for first, nLeft in ranges:
1970        if first == -1:  # unencoded
1971            first = 0
1972        data.append(packCard8(first) + packCard8(nLeft))
1973    return bytesjoin(data)
1974
1975
1976class FDArrayConverter(TableConverter):
1977    def _read(self, parent, value):
1978        try:
1979            vstore = parent.VarStore
1980        except AttributeError:
1981            vstore = None
1982        file = parent.file
1983        isCFF2 = parent._isCFF2
1984        file.seek(value)
1985        fdArray = FDArrayIndex(file, isCFF2=isCFF2)
1986        fdArray.vstore = vstore
1987        fdArray.strings = parent.strings
1988        fdArray.GlobalSubrs = parent.GlobalSubrs
1989        return fdArray
1990
1991    def write(self, parent, value):
1992        return 0  # dummy value
1993
1994    def xmlRead(self, name, attrs, content, parent):
1995        fdArray = FDArrayIndex()
1996        for element in content:
1997            if isinstance(element, str):
1998                continue
1999            name, attrs, content = element
2000            fdArray.fromXML(name, attrs, content)
2001        return fdArray
2002
2003
2004class FDSelectConverter(SimpleConverter):
2005    def _read(self, parent, value):
2006        file = parent.file
2007        file.seek(value)
2008        fdSelect = FDSelect(file, parent.numGlyphs)
2009        return fdSelect
2010
2011    def write(self, parent, value):
2012        return 0  # dummy value
2013
2014    # The FDSelect glyph data is written out to XML in the charstring keys,
2015    # so we write out only the format selector
2016    def xmlWrite(self, xmlWriter, name, value):
2017        xmlWriter.simpletag(name, [("format", value.format)])
2018        xmlWriter.newline()
2019
2020    def xmlRead(self, name, attrs, content, parent):
2021        fmt = safeEval(attrs["format"])
2022        file = None
2023        numGlyphs = None
2024        fdSelect = FDSelect(file, numGlyphs, fmt)
2025        return fdSelect
2026
2027
2028class VarStoreConverter(SimpleConverter):
2029    def _read(self, parent, value):
2030        file = parent.file
2031        file.seek(value)
2032        varStore = VarStoreData(file)
2033        varStore.decompile()
2034        return varStore
2035
2036    def write(self, parent, value):
2037        return 0  # dummy value
2038
2039    def xmlWrite(self, xmlWriter, name, value):
2040        value.writeXML(xmlWriter, name)
2041
2042    def xmlRead(self, name, attrs, content, parent):
2043        varStore = VarStoreData()
2044        varStore.xmlRead(name, attrs, content, parent)
2045        return varStore
2046
2047
2048def packFDSelect0(fdSelectArray):
2049    fmt = 0
2050    data = [packCard8(fmt)]
2051    for index in fdSelectArray:
2052        data.append(packCard8(index))
2053    return bytesjoin(data)
2054
2055
2056def packFDSelect3(fdSelectArray):
2057    fmt = 3
2058    fdRanges = []
2059    lenArray = len(fdSelectArray)
2060    lastFDIndex = -1
2061    for i in range(lenArray):
2062        fdIndex = fdSelectArray[i]
2063        if lastFDIndex != fdIndex:
2064            fdRanges.append([i, fdIndex])
2065            lastFDIndex = fdIndex
2066    sentinelGID = i + 1
2067
2068    data = [packCard8(fmt)]
2069    data.append(packCard16(len(fdRanges)))
2070    for fdRange in fdRanges:
2071        data.append(packCard16(fdRange[0]))
2072        data.append(packCard8(fdRange[1]))
2073    data.append(packCard16(sentinelGID))
2074    return bytesjoin(data)
2075
2076
2077def packFDSelect4(fdSelectArray):
2078    fmt = 4
2079    fdRanges = []
2080    lenArray = len(fdSelectArray)
2081    lastFDIndex = -1
2082    for i in range(lenArray):
2083        fdIndex = fdSelectArray[i]
2084        if lastFDIndex != fdIndex:
2085            fdRanges.append([i, fdIndex])
2086            lastFDIndex = fdIndex
2087    sentinelGID = i + 1
2088
2089    data = [packCard8(fmt)]
2090    data.append(packCard32(len(fdRanges)))
2091    for fdRange in fdRanges:
2092        data.append(packCard32(fdRange[0]))
2093        data.append(packCard16(fdRange[1]))
2094    data.append(packCard32(sentinelGID))
2095    return bytesjoin(data)
2096
2097
2098class FDSelectCompiler(object):
2099    def __init__(self, fdSelect, parent):
2100        fmt = fdSelect.format
2101        fdSelectArray = fdSelect.gidArray
2102        if fmt == 0:
2103            self.data = packFDSelect0(fdSelectArray)
2104        elif fmt == 3:
2105            self.data = packFDSelect3(fdSelectArray)
2106        elif fmt == 4:
2107            self.data = packFDSelect4(fdSelectArray)
2108        else:
2109            # choose smaller of the two formats
2110            data0 = packFDSelect0(fdSelectArray)
2111            data3 = packFDSelect3(fdSelectArray)
2112            if len(data0) < len(data3):
2113                self.data = data0
2114                fdSelect.format = 0
2115            else:
2116                self.data = data3
2117                fdSelect.format = 3
2118
2119        self.parent = parent
2120
2121    def setPos(self, pos, endPos):
2122        self.parent.rawDict["FDSelect"] = pos
2123
2124    def getDataLength(self):
2125        return len(self.data)
2126
2127    def toFile(self, file):
2128        file.write(self.data)
2129
2130
2131class VarStoreCompiler(object):
2132    def __init__(self, varStoreData, parent):
2133        self.parent = parent
2134        if not varStoreData.data:
2135            varStoreData.compile()
2136        data = [packCard16(len(varStoreData.data)), varStoreData.data]
2137        self.data = bytesjoin(data)
2138
2139    def setPos(self, pos, endPos):
2140        self.parent.rawDict["VarStore"] = pos
2141
2142    def getDataLength(self):
2143        return len(self.data)
2144
2145    def toFile(self, file):
2146        file.write(self.data)
2147
2148
2149class ROSConverter(SimpleConverter):
2150    def xmlWrite(self, xmlWriter, name, value):
2151        registry, order, supplement = value
2152        xmlWriter.simpletag(
2153            name,
2154            [
2155                ("Registry", tostr(registry)),
2156                ("Order", tostr(order)),
2157                ("Supplement", supplement),
2158            ],
2159        )
2160        xmlWriter.newline()
2161
2162    def xmlRead(self, name, attrs, content, parent):
2163        return (attrs["Registry"], attrs["Order"], safeEval(attrs["Supplement"]))
2164
2165
2166topDictOperators = [
2167    # 	opcode		name			argument type	default	converter
2168    (25, "maxstack", "number", None, None),
2169    ((12, 30), "ROS", ("SID", "SID", "number"), None, ROSConverter()),
2170    ((12, 20), "SyntheticBase", "number", None, None),
2171    (0, "version", "SID", None, None),
2172    (1, "Notice", "SID", None, Latin1Converter()),
2173    ((12, 0), "Copyright", "SID", None, Latin1Converter()),
2174    (2, "FullName", "SID", None, Latin1Converter()),
2175    ((12, 38), "FontName", "SID", None, Latin1Converter()),
2176    (3, "FamilyName", "SID", None, Latin1Converter()),
2177    (4, "Weight", "SID", None, None),
2178    ((12, 1), "isFixedPitch", "number", 0, None),
2179    ((12, 2), "ItalicAngle", "number", 0, None),
2180    ((12, 3), "UnderlinePosition", "number", -100, None),
2181    ((12, 4), "UnderlineThickness", "number", 50, None),
2182    ((12, 5), "PaintType", "number", 0, None),
2183    ((12, 6), "CharstringType", "number", 2, None),
2184    ((12, 7), "FontMatrix", "array", [0.001, 0, 0, 0.001, 0, 0], None),
2185    (13, "UniqueID", "number", None, None),
2186    (5, "FontBBox", "array", [0, 0, 0, 0], None),
2187    ((12, 8), "StrokeWidth", "number", 0, None),
2188    (14, "XUID", "array", None, None),
2189    ((12, 21), "PostScript", "SID", None, None),
2190    ((12, 22), "BaseFontName", "SID", None, None),
2191    ((12, 23), "BaseFontBlend", "delta", None, None),
2192    ((12, 31), "CIDFontVersion", "number", 0, None),
2193    ((12, 32), "CIDFontRevision", "number", 0, None),
2194    ((12, 33), "CIDFontType", "number", 0, None),
2195    ((12, 34), "CIDCount", "number", 8720, None),
2196    (15, "charset", "number", None, CharsetConverter()),
2197    ((12, 35), "UIDBase", "number", None, None),
2198    (16, "Encoding", "number", 0, EncodingConverter()),
2199    (18, "Private", ("number", "number"), None, PrivateDictConverter()),
2200    ((12, 37), "FDSelect", "number", None, FDSelectConverter()),
2201    ((12, 36), "FDArray", "number", None, FDArrayConverter()),
2202    (17, "CharStrings", "number", None, CharStringsConverter()),
2203    (24, "VarStore", "number", None, VarStoreConverter()),
2204]
2205
2206topDictOperators2 = [
2207    # 	opcode		name			argument type	default	converter
2208    (25, "maxstack", "number", None, None),
2209    ((12, 7), "FontMatrix", "array", [0.001, 0, 0, 0.001, 0, 0], None),
2210    ((12, 37), "FDSelect", "number", None, FDSelectConverter()),
2211    ((12, 36), "FDArray", "number", None, FDArrayConverter()),
2212    (17, "CharStrings", "number", None, CharStringsConverter()),
2213    (24, "VarStore", "number", None, VarStoreConverter()),
2214]
2215
2216# Note! FDSelect and FDArray must both preceed CharStrings in the output XML build order,
2217# in order for the font to compile back from xml.
2218
2219kBlendDictOpName = "blend"
2220blendOp = 23
2221
2222privateDictOperators = [
2223    # 	opcode		name			argument type	default	converter
2224    (22, "vsindex", "number", None, None),
2225    (
2226        blendOp,
2227        kBlendDictOpName,
2228        "blendList",
2229        None,
2230        None,
2231    ),  # This is for reading to/from XML: it not written to CFF.
2232    (6, "BlueValues", "delta", None, None),
2233    (7, "OtherBlues", "delta", None, None),
2234    (8, "FamilyBlues", "delta", None, None),
2235    (9, "FamilyOtherBlues", "delta", None, None),
2236    ((12, 9), "BlueScale", "number", 0.039625, None),
2237    ((12, 10), "BlueShift", "number", 7, None),
2238    ((12, 11), "BlueFuzz", "number", 1, None),
2239    (10, "StdHW", "number", None, None),
2240    (11, "StdVW", "number", None, None),
2241    ((12, 12), "StemSnapH", "delta", None, None),
2242    ((12, 13), "StemSnapV", "delta", None, None),
2243    ((12, 14), "ForceBold", "number", 0, None),
2244    ((12, 15), "ForceBoldThreshold", "number", None, None),  # deprecated
2245    ((12, 16), "lenIV", "number", None, None),  # deprecated
2246    ((12, 17), "LanguageGroup", "number", 0, None),
2247    ((12, 18), "ExpansionFactor", "number", 0.06, None),
2248    ((12, 19), "initialRandomSeed", "number", 0, None),
2249    (20, "defaultWidthX", "number", 0, None),
2250    (21, "nominalWidthX", "number", 0, None),
2251    (19, "Subrs", "number", None, SubrsConverter()),
2252]
2253
2254privateDictOperators2 = [
2255    # 	opcode		name			argument type	default	converter
2256    (22, "vsindex", "number", None, None),
2257    (
2258        blendOp,
2259        kBlendDictOpName,
2260        "blendList",
2261        None,
2262        None,
2263    ),  # This is for reading to/from XML: it not written to CFF.
2264    (6, "BlueValues", "delta", None, None),
2265    (7, "OtherBlues", "delta", None, None),
2266    (8, "FamilyBlues", "delta", None, None),
2267    (9, "FamilyOtherBlues", "delta", None, None),
2268    ((12, 9), "BlueScale", "number", 0.039625, None),
2269    ((12, 10), "BlueShift", "number", 7, None),
2270    ((12, 11), "BlueFuzz", "number", 1, None),
2271    (10, "StdHW", "number", None, None),
2272    (11, "StdVW", "number", None, None),
2273    ((12, 12), "StemSnapH", "delta", None, None),
2274    ((12, 13), "StemSnapV", "delta", None, None),
2275    ((12, 17), "LanguageGroup", "number", 0, None),
2276    ((12, 18), "ExpansionFactor", "number", 0.06, None),
2277    (19, "Subrs", "number", None, SubrsConverter()),
2278]
2279
2280
2281def addConverters(table):
2282    for i in range(len(table)):
2283        op, name, arg, default, conv = table[i]
2284        if conv is not None:
2285            continue
2286        if arg in ("delta", "array"):
2287            conv = ArrayConverter()
2288        elif arg == "number":
2289            conv = NumberConverter()
2290        elif arg == "SID":
2291            conv = ASCIIConverter()
2292        elif arg == "blendList":
2293            conv = None
2294        else:
2295            assert False
2296        table[i] = op, name, arg, default, conv
2297
2298
2299addConverters(privateDictOperators)
2300addConverters(topDictOperators)
2301
2302
2303class TopDictDecompiler(psCharStrings.DictDecompiler):
2304    operators = buildOperatorDict(topDictOperators)
2305
2306
2307class PrivateDictDecompiler(psCharStrings.DictDecompiler):
2308    operators = buildOperatorDict(privateDictOperators)
2309
2310
2311class DictCompiler(object):
2312    maxBlendStack = 0
2313
2314    def __init__(self, dictObj, strings, parent, isCFF2=None):
2315        if strings:
2316            assert isinstance(strings, IndexedStrings)
2317        if isCFF2 is None and hasattr(parent, "isCFF2"):
2318            isCFF2 = parent.isCFF2
2319            assert isCFF2 is not None
2320        self.isCFF2 = isCFF2
2321        self.dictObj = dictObj
2322        self.strings = strings
2323        self.parent = parent
2324        rawDict = {}
2325        for name in dictObj.order:
2326            value = getattr(dictObj, name, None)
2327            if value is None:
2328                continue
2329            conv = dictObj.converters[name]
2330            value = conv.write(dictObj, value)
2331            if value == dictObj.defaults.get(name):
2332                continue
2333            rawDict[name] = value
2334        self.rawDict = rawDict
2335
2336    def setPos(self, pos, endPos):
2337        pass
2338
2339    def getDataLength(self):
2340        return len(self.compile("getDataLength"))
2341
2342    def compile(self, reason):
2343        log.log(DEBUG, "-- compiling %s for %s", self.__class__.__name__, reason)
2344        rawDict = self.rawDict
2345        data = []
2346        for name in self.dictObj.order:
2347            value = rawDict.get(name)
2348            if value is None:
2349                continue
2350            op, argType = self.opcodes[name]
2351            if isinstance(argType, tuple):
2352                l = len(argType)
2353                assert len(value) == l, "value doesn't match arg type"
2354                for i in range(l):
2355                    arg = argType[i]
2356                    v = value[i]
2357                    arghandler = getattr(self, "arg_" + arg)
2358                    data.append(arghandler(v))
2359            else:
2360                arghandler = getattr(self, "arg_" + argType)
2361                data.append(arghandler(value))
2362            data.append(op)
2363        data = bytesjoin(data)
2364        return data
2365
2366    def toFile(self, file):
2367        data = self.compile("toFile")
2368        file.write(data)
2369
2370    def arg_number(self, num):
2371        if isinstance(num, list):
2372            data = [encodeNumber(val) for val in num]
2373            data.append(encodeNumber(1))
2374            data.append(bytechr(blendOp))
2375            datum = bytesjoin(data)
2376        else:
2377            datum = encodeNumber(num)
2378        return datum
2379
2380    def arg_SID(self, s):
2381        return psCharStrings.encodeIntCFF(self.strings.getSID(s))
2382
2383    def arg_array(self, value):
2384        data = []
2385        for num in value:
2386            data.append(self.arg_number(num))
2387        return bytesjoin(data)
2388
2389    def arg_delta(self, value):
2390        if not value:
2391            return b""
2392        val0 = value[0]
2393        if isinstance(val0, list):
2394            data = self.arg_delta_blend(value)
2395        else:
2396            out = []
2397            last = 0
2398            for v in value:
2399                out.append(v - last)
2400                last = v
2401            data = []
2402            for num in out:
2403                data.append(encodeNumber(num))
2404        return bytesjoin(data)
2405
2406    def arg_delta_blend(self, value):
2407        """A delta list with blend lists has to be *all* blend lists.
2408
2409        The value is a list is arranged as follows::
2410
2411                [
2412                        [V0, d0..dn]
2413                        [V1, d0..dn]
2414                        ...
2415                        [Vm, d0..dn]
2416                ]
2417
2418        ``V`` is the absolute coordinate value from the default font, and ``d0-dn``
2419        are the delta values from the *n* regions. Each ``V`` is an absolute
2420        coordinate from the default font.
2421
2422        We want to return a list::
2423
2424                [
2425                        [v0, v1..vm]
2426                        [d0..dn]
2427                        ...
2428                        [d0..dn]
2429                        numBlends
2430                        blendOp
2431                ]
2432
2433        where each ``v`` is relative to the previous default font value.
2434        """
2435        numMasters = len(value[0])
2436        numBlends = len(value)
2437        numStack = (numBlends * numMasters) + 1
2438        if numStack > self.maxBlendStack:
2439            # Figure out the max number of value we can blend
2440            # and divide this list up into chunks of that size.
2441
2442            numBlendValues = int((self.maxBlendStack - 1) / numMasters)
2443            out = []
2444            while True:
2445                numVal = min(len(value), numBlendValues)
2446                if numVal == 0:
2447                    break
2448                valList = value[0:numVal]
2449                out1 = self.arg_delta_blend(valList)
2450                out.extend(out1)
2451                value = value[numVal:]
2452        else:
2453            firstList = [0] * numBlends
2454            deltaList = [None] * numBlends
2455            i = 0
2456            prevVal = 0
2457            while i < numBlends:
2458                # For PrivateDict BlueValues, the default font
2459                # values are absolute, not relative.
2460                # Must convert these back to relative coordinates
2461                # befor writing to CFF2.
2462                defaultValue = value[i][0]
2463                firstList[i] = defaultValue - prevVal
2464                prevVal = defaultValue
2465                deltaList[i] = value[i][1:]
2466                i += 1
2467
2468            relValueList = firstList
2469            for blendList in deltaList:
2470                relValueList.extend(blendList)
2471            out = [encodeNumber(val) for val in relValueList]
2472            out.append(encodeNumber(numBlends))
2473            out.append(bytechr(blendOp))
2474        return out
2475
2476
2477def encodeNumber(num):
2478    if isinstance(num, float):
2479        return psCharStrings.encodeFloat(num)
2480    else:
2481        return psCharStrings.encodeIntCFF(num)
2482
2483
2484class TopDictCompiler(DictCompiler):
2485    opcodes = buildOpcodeDict(topDictOperators)
2486
2487    def getChildren(self, strings):
2488        isCFF2 = self.isCFF2
2489        children = []
2490        if self.dictObj.cff2GetGlyphOrder is None:
2491            if hasattr(self.dictObj, "charset") and self.dictObj.charset:
2492                if hasattr(self.dictObj, "ROS"):  # aka isCID
2493                    charsetCode = None
2494                else:
2495                    charsetCode = getStdCharSet(self.dictObj.charset)
2496                if charsetCode is None:
2497                    children.append(
2498                        CharsetCompiler(strings, self.dictObj.charset, self)
2499                    )
2500                else:
2501                    self.rawDict["charset"] = charsetCode
2502            if hasattr(self.dictObj, "Encoding") and self.dictObj.Encoding:
2503                encoding = self.dictObj.Encoding
2504                if not isinstance(encoding, str):
2505                    children.append(EncodingCompiler(strings, encoding, self))
2506        else:
2507            if hasattr(self.dictObj, "VarStore"):
2508                varStoreData = self.dictObj.VarStore
2509                varStoreComp = VarStoreCompiler(varStoreData, self)
2510                children.append(varStoreComp)
2511        if hasattr(self.dictObj, "FDSelect"):
2512            # I have not yet supported merging a ttx CFF-CID font, as there are
2513            # interesting issues about merging the FDArrays. Here I assume that
2514            # either the font was read from XML, and the FDSelect indices are all
2515            # in the charstring data, or the FDSelect array is already fully defined.
2516            fdSelect = self.dictObj.FDSelect
2517            # probably read in from XML; assume fdIndex in CharString data
2518            if len(fdSelect) == 0:
2519                charStrings = self.dictObj.CharStrings
2520                for name in self.dictObj.charset:
2521                    fdSelect.append(charStrings[name].fdSelectIndex)
2522            fdSelectComp = FDSelectCompiler(fdSelect, self)
2523            children.append(fdSelectComp)
2524        if hasattr(self.dictObj, "CharStrings"):
2525            items = []
2526            charStrings = self.dictObj.CharStrings
2527            for name in self.dictObj.charset:
2528                items.append(charStrings[name])
2529            charStringsComp = CharStringsCompiler(items, strings, self, isCFF2=isCFF2)
2530            children.append(charStringsComp)
2531        if hasattr(self.dictObj, "FDArray"):
2532            # I have not yet supported merging a ttx CFF-CID font, as there are
2533            # interesting issues about merging the FDArrays. Here I assume that the
2534            # FDArray info is correct and complete.
2535            fdArrayIndexComp = self.dictObj.FDArray.getCompiler(strings, self)
2536            children.append(fdArrayIndexComp)
2537            children.extend(fdArrayIndexComp.getChildren(strings))
2538        if hasattr(self.dictObj, "Private"):
2539            privComp = self.dictObj.Private.getCompiler(strings, self)
2540            children.append(privComp)
2541            children.extend(privComp.getChildren(strings))
2542        return children
2543
2544
2545class FontDictCompiler(DictCompiler):
2546    opcodes = buildOpcodeDict(topDictOperators)
2547
2548    def __init__(self, dictObj, strings, parent, isCFF2=None):
2549        super(FontDictCompiler, self).__init__(dictObj, strings, parent, isCFF2=isCFF2)
2550        #
2551        # We now take some effort to detect if there were any key/value pairs
2552        # supplied that were ignored in the FontDict context, and issue a warning
2553        # for those cases.
2554        #
2555        ignoredNames = []
2556        dictObj = self.dictObj
2557        for name in sorted(set(dictObj.converters) - set(dictObj.order)):
2558            if name in dictObj.rawDict:
2559                # The font was directly read from binary. In this
2560                # case, we want to report *all* "useless" key/value
2561                # pairs that are in the font, not just the ones that
2562                # are different from the default.
2563                ignoredNames.append(name)
2564            else:
2565                # The font was probably read from a TTX file. We only
2566                # warn about keys whos value is not the default. The
2567                # ones that have the default value will not be written
2568                # to binary anyway.
2569                default = dictObj.defaults.get(name)
2570                if default is not None:
2571                    conv = dictObj.converters[name]
2572                    default = conv.read(dictObj, default)
2573                if getattr(dictObj, name, None) != default:
2574                    ignoredNames.append(name)
2575        if ignoredNames:
2576            log.warning(
2577                "Some CFF FDArray/FontDict keys were ignored upon compile: "
2578                + " ".join(sorted(ignoredNames))
2579            )
2580
2581    def getChildren(self, strings):
2582        children = []
2583        if hasattr(self.dictObj, "Private"):
2584            privComp = self.dictObj.Private.getCompiler(strings, self)
2585            children.append(privComp)
2586            children.extend(privComp.getChildren(strings))
2587        return children
2588
2589
2590class PrivateDictCompiler(DictCompiler):
2591    maxBlendStack = maxStackLimit
2592    opcodes = buildOpcodeDict(privateDictOperators)
2593
2594    def setPos(self, pos, endPos):
2595        size = endPos - pos
2596        self.parent.rawDict["Private"] = size, pos
2597        self.pos = pos
2598
2599    def getChildren(self, strings):
2600        children = []
2601        if hasattr(self.dictObj, "Subrs"):
2602            children.append(self.dictObj.Subrs.getCompiler(strings, self))
2603        return children
2604
2605
2606class BaseDict(object):
2607    def __init__(self, strings=None, file=None, offset=None, isCFF2=None):
2608        assert (isCFF2 is None) == (file is None)
2609        self.rawDict = {}
2610        self.skipNames = []
2611        self.strings = strings
2612        if file is None:
2613            return
2614        self._isCFF2 = isCFF2
2615        self.file = file
2616        if offset is not None:
2617            log.log(DEBUG, "loading %s at %s", self.__class__.__name__, offset)
2618            self.offset = offset
2619
2620    def decompile(self, data):
2621        log.log(DEBUG, "    length %s is %d", self.__class__.__name__, len(data))
2622        dec = self.decompilerClass(self.strings, self)
2623        dec.decompile(data)
2624        self.rawDict = dec.getDict()
2625        self.postDecompile()
2626
2627    def postDecompile(self):
2628        pass
2629
2630    def getCompiler(self, strings, parent, isCFF2=None):
2631        return self.compilerClass(self, strings, parent, isCFF2=isCFF2)
2632
2633    def __getattr__(self, name):
2634        if name[:2] == name[-2:] == "__":
2635            # to make deepcopy() and pickle.load() work, we need to signal with
2636            # AttributeError that dunder methods like '__deepcopy__' or '__getstate__'
2637            # aren't implemented. For more details, see:
2638            # https://github.com/fonttools/fonttools/pull/1488
2639            raise AttributeError(name)
2640        value = self.rawDict.get(name, None)
2641        if value is None:
2642            value = self.defaults.get(name)
2643        if value is None:
2644            raise AttributeError(name)
2645        conv = self.converters[name]
2646        value = conv.read(self, value)
2647        setattr(self, name, value)
2648        return value
2649
2650    def toXML(self, xmlWriter):
2651        for name in self.order:
2652            if name in self.skipNames:
2653                continue
2654            value = getattr(self, name, None)
2655            # XXX For "charset" we never skip calling xmlWrite even if the
2656            # value is None, so we always write the following XML comment:
2657            #
2658            # <!-- charset is dumped separately as the 'GlyphOrder' element -->
2659            #
2660            # Charset is None when 'CFF ' table is imported from XML into an
2661            # empty TTFont(). By writing this comment all the time, we obtain
2662            # the same XML output whether roundtripping XML-to-XML or
2663            # dumping binary-to-XML
2664            if value is None and name != "charset":
2665                continue
2666            conv = self.converters[name]
2667            conv.xmlWrite(xmlWriter, name, value)
2668        ignoredNames = set(self.rawDict) - set(self.order)
2669        if ignoredNames:
2670            xmlWriter.comment(
2671                "some keys were ignored: %s" % " ".join(sorted(ignoredNames))
2672            )
2673            xmlWriter.newline()
2674
2675    def fromXML(self, name, attrs, content):
2676        conv = self.converters[name]
2677        value = conv.xmlRead(name, attrs, content, self)
2678        setattr(self, name, value)
2679
2680
2681class TopDict(BaseDict):
2682    """The ``TopDict`` represents the top-level dictionary holding font
2683    information. CFF2 tables contain a restricted set of top-level entries
2684    as described `here <https://docs.microsoft.com/en-us/typography/opentype/spec/cff2#7-top-dict-data>`_,
2685    but CFF tables may contain a wider range of information. This information
2686    can be accessed through attributes or through the dictionary returned
2687    through the ``rawDict`` property:
2688
2689    .. code:: python
2690
2691            font = tt["CFF "].cff[0]
2692            font.FamilyName
2693            # 'Linux Libertine O'
2694            font.rawDict["FamilyName"]
2695            # 'Linux Libertine O'
2696
2697    More information is available in the CFF file's private dictionary, accessed
2698    via the ``Private`` property:
2699
2700    .. code:: python
2701
2702            tt["CFF "].cff[0].Private.BlueValues
2703            # [-15, 0, 515, 515, 666, 666]
2704
2705    """
2706
2707    defaults = buildDefaults(topDictOperators)
2708    converters = buildConverters(topDictOperators)
2709    compilerClass = TopDictCompiler
2710    order = buildOrder(topDictOperators)
2711    decompilerClass = TopDictDecompiler
2712
2713    def __init__(
2714        self,
2715        strings=None,
2716        file=None,
2717        offset=None,
2718        GlobalSubrs=None,
2719        cff2GetGlyphOrder=None,
2720        isCFF2=None,
2721    ):
2722        super(TopDict, self).__init__(strings, file, offset, isCFF2=isCFF2)
2723        self.cff2GetGlyphOrder = cff2GetGlyphOrder
2724        self.GlobalSubrs = GlobalSubrs
2725        if isCFF2:
2726            self.defaults = buildDefaults(topDictOperators2)
2727            self.charset = cff2GetGlyphOrder()
2728            self.order = buildOrder(topDictOperators2)
2729        else:
2730            self.defaults = buildDefaults(topDictOperators)
2731            self.order = buildOrder(topDictOperators)
2732
2733    def getGlyphOrder(self):
2734        """Returns a list of glyph names in the CFF font."""
2735        return self.charset
2736
2737    def postDecompile(self):
2738        offset = self.rawDict.get("CharStrings")
2739        if offset is None:
2740            return
2741        # get the number of glyphs beforehand.
2742        self.file.seek(offset)
2743        if self._isCFF2:
2744            self.numGlyphs = readCard32(self.file)
2745        else:
2746            self.numGlyphs = readCard16(self.file)
2747
2748    def toXML(self, xmlWriter):
2749        if hasattr(self, "CharStrings"):
2750            self.decompileAllCharStrings()
2751        if hasattr(self, "ROS"):
2752            self.skipNames = ["Encoding"]
2753        if not hasattr(self, "ROS") or not hasattr(self, "CharStrings"):
2754            # these values have default values, but I only want them to show up
2755            # in CID fonts.
2756            self.skipNames = [
2757                "CIDFontVersion",
2758                "CIDFontRevision",
2759                "CIDFontType",
2760                "CIDCount",
2761            ]
2762        BaseDict.toXML(self, xmlWriter)
2763
2764    def decompileAllCharStrings(self):
2765        # Make sure that all the Private Dicts have been instantiated.
2766        for i, charString in enumerate(self.CharStrings.values()):
2767            try:
2768                charString.decompile()
2769            except:
2770                log.error("Error in charstring %s", i)
2771                raise
2772
2773    def recalcFontBBox(self):
2774        fontBBox = None
2775        for charString in self.CharStrings.values():
2776            bounds = charString.calcBounds(self.CharStrings)
2777            if bounds is not None:
2778                if fontBBox is not None:
2779                    fontBBox = unionRect(fontBBox, bounds)
2780                else:
2781                    fontBBox = bounds
2782
2783        if fontBBox is None:
2784            self.FontBBox = self.defaults["FontBBox"][:]
2785        else:
2786            self.FontBBox = list(intRect(fontBBox))
2787
2788
2789class FontDict(BaseDict):
2790    #
2791    # Since fonttools used to pass a lot of fields that are not relevant in the FDArray
2792    # FontDict, there are 'ttx' files in the wild that contain all these. These got in
2793    # the ttx files because fonttools writes explicit values for all the TopDict default
2794    # values. These are not actually illegal in the context of an FDArray FontDict - you
2795    # can legally, per spec, put any arbitrary key/value pair in a FontDict - but are
2796    # useless since current major company CFF interpreters ignore anything but the set
2797    # listed in this file. So, we just silently skip them. An exception is Weight: this
2798    # is not used by any interpreter, but some foundries have asked that this be
2799    # supported in FDArray FontDicts just to preserve information about the design when
2800    # the font is being inspected.
2801    #
2802    # On top of that, there are fonts out there that contain such useless FontDict values.
2803    #
2804    # By subclassing TopDict, we *allow* all key/values from TopDict, both when reading
2805    # from binary or when reading from XML, but by overriding `order` with a limited
2806    # list of names, we ensure that only the useful names ever get exported to XML and
2807    # ever get compiled into the binary font.
2808    #
2809    # We override compilerClass so we can warn about "useless" key/value pairs, either
2810    # from the original binary font or from TTX input.
2811    #
2812    # See:
2813    # - https://github.com/fonttools/fonttools/issues/740
2814    # - https://github.com/fonttools/fonttools/issues/601
2815    # - https://github.com/adobe-type-tools/afdko/issues/137
2816    #
2817    defaults = {}
2818    converters = buildConverters(topDictOperators)
2819    compilerClass = FontDictCompiler
2820    orderCFF = ["FontName", "FontMatrix", "Weight", "Private"]
2821    orderCFF2 = ["Private"]
2822    decompilerClass = TopDictDecompiler
2823
2824    def __init__(
2825        self,
2826        strings=None,
2827        file=None,
2828        offset=None,
2829        GlobalSubrs=None,
2830        isCFF2=None,
2831        vstore=None,
2832    ):
2833        super(FontDict, self).__init__(strings, file, offset, isCFF2=isCFF2)
2834        self.vstore = vstore
2835        self.setCFF2(isCFF2)
2836
2837    def setCFF2(self, isCFF2):
2838        # isCFF2 may be None.
2839        if isCFF2:
2840            self.order = self.orderCFF2
2841            self._isCFF2 = True
2842        else:
2843            self.order = self.orderCFF
2844            self._isCFF2 = False
2845
2846
2847class PrivateDict(BaseDict):
2848    defaults = buildDefaults(privateDictOperators)
2849    converters = buildConverters(privateDictOperators)
2850    order = buildOrder(privateDictOperators)
2851    decompilerClass = PrivateDictDecompiler
2852    compilerClass = PrivateDictCompiler
2853
2854    def __init__(self, strings=None, file=None, offset=None, isCFF2=None, vstore=None):
2855        super(PrivateDict, self).__init__(strings, file, offset, isCFF2=isCFF2)
2856        self.vstore = vstore
2857        if isCFF2:
2858            self.defaults = buildDefaults(privateDictOperators2)
2859            self.order = buildOrder(privateDictOperators2)
2860            # Provide dummy values. This avoids needing to provide
2861            # an isCFF2 state in a lot of places.
2862            self.nominalWidthX = self.defaultWidthX = None
2863        else:
2864            self.defaults = buildDefaults(privateDictOperators)
2865            self.order = buildOrder(privateDictOperators)
2866
2867    @property
2868    def in_cff2(self):
2869        return self._isCFF2
2870
2871    def getNumRegions(self, vi=None):  # called from misc/psCharStrings.py
2872        # if getNumRegions is being called, we can assume that VarStore exists.
2873        if vi is None:
2874            if hasattr(self, "vsindex"):
2875                vi = self.vsindex
2876            else:
2877                vi = 0
2878        numRegions = self.vstore.getNumRegions(vi)
2879        return numRegions
2880
2881
2882class IndexedStrings(object):
2883    """SID -> string mapping."""
2884
2885    def __init__(self, file=None):
2886        if file is None:
2887            strings = []
2888        else:
2889            strings = [tostr(s, encoding="latin1") for s in Index(file, isCFF2=False)]
2890        self.strings = strings
2891
2892    def getCompiler(self):
2893        return IndexedStringsCompiler(self, None, self, isCFF2=False)
2894
2895    def __len__(self):
2896        return len(self.strings)
2897
2898    def __getitem__(self, SID):
2899        if SID < cffStandardStringCount:
2900            return cffStandardStrings[SID]
2901        else:
2902            return self.strings[SID - cffStandardStringCount]
2903
2904    def getSID(self, s):
2905        if not hasattr(self, "stringMapping"):
2906            self.buildStringMapping()
2907        s = tostr(s, encoding="latin1")
2908        if s in cffStandardStringMapping:
2909            SID = cffStandardStringMapping[s]
2910        elif s in self.stringMapping:
2911            SID = self.stringMapping[s]
2912        else:
2913            SID = len(self.strings) + cffStandardStringCount
2914            self.strings.append(s)
2915            self.stringMapping[s] = SID
2916        return SID
2917
2918    def getStrings(self):
2919        return self.strings
2920
2921    def buildStringMapping(self):
2922        self.stringMapping = {}
2923        for index in range(len(self.strings)):
2924            self.stringMapping[self.strings[index]] = index + cffStandardStringCount
2925
2926
2927# The 391 Standard Strings as used in the CFF format.
2928# from Adobe Technical None #5176, version 1.0, 18 March 1998
2929
2930cffStandardStrings = [
2931    ".notdef",
2932    "space",
2933    "exclam",
2934    "quotedbl",
2935    "numbersign",
2936    "dollar",
2937    "percent",
2938    "ampersand",
2939    "quoteright",
2940    "parenleft",
2941    "parenright",
2942    "asterisk",
2943    "plus",
2944    "comma",
2945    "hyphen",
2946    "period",
2947    "slash",
2948    "zero",
2949    "one",
2950    "two",
2951    "three",
2952    "four",
2953    "five",
2954    "six",
2955    "seven",
2956    "eight",
2957    "nine",
2958    "colon",
2959    "semicolon",
2960    "less",
2961    "equal",
2962    "greater",
2963    "question",
2964    "at",
2965    "A",
2966    "B",
2967    "C",
2968    "D",
2969    "E",
2970    "F",
2971    "G",
2972    "H",
2973    "I",
2974    "J",
2975    "K",
2976    "L",
2977    "M",
2978    "N",
2979    "O",
2980    "P",
2981    "Q",
2982    "R",
2983    "S",
2984    "T",
2985    "U",
2986    "V",
2987    "W",
2988    "X",
2989    "Y",
2990    "Z",
2991    "bracketleft",
2992    "backslash",
2993    "bracketright",
2994    "asciicircum",
2995    "underscore",
2996    "quoteleft",
2997    "a",
2998    "b",
2999    "c",
3000    "d",
3001    "e",
3002    "f",
3003    "g",
3004    "h",
3005    "i",
3006    "j",
3007    "k",
3008    "l",
3009    "m",
3010    "n",
3011    "o",
3012    "p",
3013    "q",
3014    "r",
3015    "s",
3016    "t",
3017    "u",
3018    "v",
3019    "w",
3020    "x",
3021    "y",
3022    "z",
3023    "braceleft",
3024    "bar",
3025    "braceright",
3026    "asciitilde",
3027    "exclamdown",
3028    "cent",
3029    "sterling",
3030    "fraction",
3031    "yen",
3032    "florin",
3033    "section",
3034    "currency",
3035    "quotesingle",
3036    "quotedblleft",
3037    "guillemotleft",
3038    "guilsinglleft",
3039    "guilsinglright",
3040    "fi",
3041    "fl",
3042    "endash",
3043    "dagger",
3044    "daggerdbl",
3045    "periodcentered",
3046    "paragraph",
3047    "bullet",
3048    "quotesinglbase",
3049    "quotedblbase",
3050    "quotedblright",
3051    "guillemotright",
3052    "ellipsis",
3053    "perthousand",
3054    "questiondown",
3055    "grave",
3056    "acute",
3057    "circumflex",
3058    "tilde",
3059    "macron",
3060    "breve",
3061    "dotaccent",
3062    "dieresis",
3063    "ring",
3064    "cedilla",
3065    "hungarumlaut",
3066    "ogonek",
3067    "caron",
3068    "emdash",
3069    "AE",
3070    "ordfeminine",
3071    "Lslash",
3072    "Oslash",
3073    "OE",
3074    "ordmasculine",
3075    "ae",
3076    "dotlessi",
3077    "lslash",
3078    "oslash",
3079    "oe",
3080    "germandbls",
3081    "onesuperior",
3082    "logicalnot",
3083    "mu",
3084    "trademark",
3085    "Eth",
3086    "onehalf",
3087    "plusminus",
3088    "Thorn",
3089    "onequarter",
3090    "divide",
3091    "brokenbar",
3092    "degree",
3093    "thorn",
3094    "threequarters",
3095    "twosuperior",
3096    "registered",
3097    "minus",
3098    "eth",
3099    "multiply",
3100    "threesuperior",
3101    "copyright",
3102    "Aacute",
3103    "Acircumflex",
3104    "Adieresis",
3105    "Agrave",
3106    "Aring",
3107    "Atilde",
3108    "Ccedilla",
3109    "Eacute",
3110    "Ecircumflex",
3111    "Edieresis",
3112    "Egrave",
3113    "Iacute",
3114    "Icircumflex",
3115    "Idieresis",
3116    "Igrave",
3117    "Ntilde",
3118    "Oacute",
3119    "Ocircumflex",
3120    "Odieresis",
3121    "Ograve",
3122    "Otilde",
3123    "Scaron",
3124    "Uacute",
3125    "Ucircumflex",
3126    "Udieresis",
3127    "Ugrave",
3128    "Yacute",
3129    "Ydieresis",
3130    "Zcaron",
3131    "aacute",
3132    "acircumflex",
3133    "adieresis",
3134    "agrave",
3135    "aring",
3136    "atilde",
3137    "ccedilla",
3138    "eacute",
3139    "ecircumflex",
3140    "edieresis",
3141    "egrave",
3142    "iacute",
3143    "icircumflex",
3144    "idieresis",
3145    "igrave",
3146    "ntilde",
3147    "oacute",
3148    "ocircumflex",
3149    "odieresis",
3150    "ograve",
3151    "otilde",
3152    "scaron",
3153    "uacute",
3154    "ucircumflex",
3155    "udieresis",
3156    "ugrave",
3157    "yacute",
3158    "ydieresis",
3159    "zcaron",
3160    "exclamsmall",
3161    "Hungarumlautsmall",
3162    "dollaroldstyle",
3163    "dollarsuperior",
3164    "ampersandsmall",
3165    "Acutesmall",
3166    "parenleftsuperior",
3167    "parenrightsuperior",
3168    "twodotenleader",
3169    "onedotenleader",
3170    "zerooldstyle",
3171    "oneoldstyle",
3172    "twooldstyle",
3173    "threeoldstyle",
3174    "fouroldstyle",
3175    "fiveoldstyle",
3176    "sixoldstyle",
3177    "sevenoldstyle",
3178    "eightoldstyle",
3179    "nineoldstyle",
3180    "commasuperior",
3181    "threequartersemdash",
3182    "periodsuperior",
3183    "questionsmall",
3184    "asuperior",
3185    "bsuperior",
3186    "centsuperior",
3187    "dsuperior",
3188    "esuperior",
3189    "isuperior",
3190    "lsuperior",
3191    "msuperior",
3192    "nsuperior",
3193    "osuperior",
3194    "rsuperior",
3195    "ssuperior",
3196    "tsuperior",
3197    "ff",
3198    "ffi",
3199    "ffl",
3200    "parenleftinferior",
3201    "parenrightinferior",
3202    "Circumflexsmall",
3203    "hyphensuperior",
3204    "Gravesmall",
3205    "Asmall",
3206    "Bsmall",
3207    "Csmall",
3208    "Dsmall",
3209    "Esmall",
3210    "Fsmall",
3211    "Gsmall",
3212    "Hsmall",
3213    "Ismall",
3214    "Jsmall",
3215    "Ksmall",
3216    "Lsmall",
3217    "Msmall",
3218    "Nsmall",
3219    "Osmall",
3220    "Psmall",
3221    "Qsmall",
3222    "Rsmall",
3223    "Ssmall",
3224    "Tsmall",
3225    "Usmall",
3226    "Vsmall",
3227    "Wsmall",
3228    "Xsmall",
3229    "Ysmall",
3230    "Zsmall",
3231    "colonmonetary",
3232    "onefitted",
3233    "rupiah",
3234    "Tildesmall",
3235    "exclamdownsmall",
3236    "centoldstyle",
3237    "Lslashsmall",
3238    "Scaronsmall",
3239    "Zcaronsmall",
3240    "Dieresissmall",
3241    "Brevesmall",
3242    "Caronsmall",
3243    "Dotaccentsmall",
3244    "Macronsmall",
3245    "figuredash",
3246    "hypheninferior",
3247    "Ogoneksmall",
3248    "Ringsmall",
3249    "Cedillasmall",
3250    "questiondownsmall",
3251    "oneeighth",
3252    "threeeighths",
3253    "fiveeighths",
3254    "seveneighths",
3255    "onethird",
3256    "twothirds",
3257    "zerosuperior",
3258    "foursuperior",
3259    "fivesuperior",
3260    "sixsuperior",
3261    "sevensuperior",
3262    "eightsuperior",
3263    "ninesuperior",
3264    "zeroinferior",
3265    "oneinferior",
3266    "twoinferior",
3267    "threeinferior",
3268    "fourinferior",
3269    "fiveinferior",
3270    "sixinferior",
3271    "seveninferior",
3272    "eightinferior",
3273    "nineinferior",
3274    "centinferior",
3275    "dollarinferior",
3276    "periodinferior",
3277    "commainferior",
3278    "Agravesmall",
3279    "Aacutesmall",
3280    "Acircumflexsmall",
3281    "Atildesmall",
3282    "Adieresissmall",
3283    "Aringsmall",
3284    "AEsmall",
3285    "Ccedillasmall",
3286    "Egravesmall",
3287    "Eacutesmall",
3288    "Ecircumflexsmall",
3289    "Edieresissmall",
3290    "Igravesmall",
3291    "Iacutesmall",
3292    "Icircumflexsmall",
3293    "Idieresissmall",
3294    "Ethsmall",
3295    "Ntildesmall",
3296    "Ogravesmall",
3297    "Oacutesmall",
3298    "Ocircumflexsmall",
3299    "Otildesmall",
3300    "Odieresissmall",
3301    "OEsmall",
3302    "Oslashsmall",
3303    "Ugravesmall",
3304    "Uacutesmall",
3305    "Ucircumflexsmall",
3306    "Udieresissmall",
3307    "Yacutesmall",
3308    "Thornsmall",
3309    "Ydieresissmall",
3310    "001.000",
3311    "001.001",
3312    "001.002",
3313    "001.003",
3314    "Black",
3315    "Bold",
3316    "Book",
3317    "Light",
3318    "Medium",
3319    "Regular",
3320    "Roman",
3321    "Semibold",
3322]
3323
3324cffStandardStringCount = 391
3325assert len(cffStandardStrings) == cffStandardStringCount
3326# build reverse mapping
3327cffStandardStringMapping = {}
3328for _i in range(cffStandardStringCount):
3329    cffStandardStringMapping[cffStandardStrings[_i]] = _i
3330
3331cffISOAdobeStrings = [
3332    ".notdef",
3333    "space",
3334    "exclam",
3335    "quotedbl",
3336    "numbersign",
3337    "dollar",
3338    "percent",
3339    "ampersand",
3340    "quoteright",
3341    "parenleft",
3342    "parenright",
3343    "asterisk",
3344    "plus",
3345    "comma",
3346    "hyphen",
3347    "period",
3348    "slash",
3349    "zero",
3350    "one",
3351    "two",
3352    "three",
3353    "four",
3354    "five",
3355    "six",
3356    "seven",
3357    "eight",
3358    "nine",
3359    "colon",
3360    "semicolon",
3361    "less",
3362    "equal",
3363    "greater",
3364    "question",
3365    "at",
3366    "A",
3367    "B",
3368    "C",
3369    "D",
3370    "E",
3371    "F",
3372    "G",
3373    "H",
3374    "I",
3375    "J",
3376    "K",
3377    "L",
3378    "M",
3379    "N",
3380    "O",
3381    "P",
3382    "Q",
3383    "R",
3384    "S",
3385    "T",
3386    "U",
3387    "V",
3388    "W",
3389    "X",
3390    "Y",
3391    "Z",
3392    "bracketleft",
3393    "backslash",
3394    "bracketright",
3395    "asciicircum",
3396    "underscore",
3397    "quoteleft",
3398    "a",
3399    "b",
3400    "c",
3401    "d",
3402    "e",
3403    "f",
3404    "g",
3405    "h",
3406    "i",
3407    "j",
3408    "k",
3409    "l",
3410    "m",
3411    "n",
3412    "o",
3413    "p",
3414    "q",
3415    "r",
3416    "s",
3417    "t",
3418    "u",
3419    "v",
3420    "w",
3421    "x",
3422    "y",
3423    "z",
3424    "braceleft",
3425    "bar",
3426    "braceright",
3427    "asciitilde",
3428    "exclamdown",
3429    "cent",
3430    "sterling",
3431    "fraction",
3432    "yen",
3433    "florin",
3434    "section",
3435    "currency",
3436    "quotesingle",
3437    "quotedblleft",
3438    "guillemotleft",
3439    "guilsinglleft",
3440    "guilsinglright",
3441    "fi",
3442    "fl",
3443    "endash",
3444    "dagger",
3445    "daggerdbl",
3446    "periodcentered",
3447    "paragraph",
3448    "bullet",
3449    "quotesinglbase",
3450    "quotedblbase",
3451    "quotedblright",
3452    "guillemotright",
3453    "ellipsis",
3454    "perthousand",
3455    "questiondown",
3456    "grave",
3457    "acute",
3458    "circumflex",
3459    "tilde",
3460    "macron",
3461    "breve",
3462    "dotaccent",
3463    "dieresis",
3464    "ring",
3465    "cedilla",
3466    "hungarumlaut",
3467    "ogonek",
3468    "caron",
3469    "emdash",
3470    "AE",
3471    "ordfeminine",
3472    "Lslash",
3473    "Oslash",
3474    "OE",
3475    "ordmasculine",
3476    "ae",
3477    "dotlessi",
3478    "lslash",
3479    "oslash",
3480    "oe",
3481    "germandbls",
3482    "onesuperior",
3483    "logicalnot",
3484    "mu",
3485    "trademark",
3486    "Eth",
3487    "onehalf",
3488    "plusminus",
3489    "Thorn",
3490    "onequarter",
3491    "divide",
3492    "brokenbar",
3493    "degree",
3494    "thorn",
3495    "threequarters",
3496    "twosuperior",
3497    "registered",
3498    "minus",
3499    "eth",
3500    "multiply",
3501    "threesuperior",
3502    "copyright",
3503    "Aacute",
3504    "Acircumflex",
3505    "Adieresis",
3506    "Agrave",
3507    "Aring",
3508    "Atilde",
3509    "Ccedilla",
3510    "Eacute",
3511    "Ecircumflex",
3512    "Edieresis",
3513    "Egrave",
3514    "Iacute",
3515    "Icircumflex",
3516    "Idieresis",
3517    "Igrave",
3518    "Ntilde",
3519    "Oacute",
3520    "Ocircumflex",
3521    "Odieresis",
3522    "Ograve",
3523    "Otilde",
3524    "Scaron",
3525    "Uacute",
3526    "Ucircumflex",
3527    "Udieresis",
3528    "Ugrave",
3529    "Yacute",
3530    "Ydieresis",
3531    "Zcaron",
3532    "aacute",
3533    "acircumflex",
3534    "adieresis",
3535    "agrave",
3536    "aring",
3537    "atilde",
3538    "ccedilla",
3539    "eacute",
3540    "ecircumflex",
3541    "edieresis",
3542    "egrave",
3543    "iacute",
3544    "icircumflex",
3545    "idieresis",
3546    "igrave",
3547    "ntilde",
3548    "oacute",
3549    "ocircumflex",
3550    "odieresis",
3551    "ograve",
3552    "otilde",
3553    "scaron",
3554    "uacute",
3555    "ucircumflex",
3556    "udieresis",
3557    "ugrave",
3558    "yacute",
3559    "ydieresis",
3560    "zcaron",
3561]
3562
3563cffISOAdobeStringCount = 229
3564assert len(cffISOAdobeStrings) == cffISOAdobeStringCount
3565
3566cffIExpertStrings = [
3567    ".notdef",
3568    "space",
3569    "exclamsmall",
3570    "Hungarumlautsmall",
3571    "dollaroldstyle",
3572    "dollarsuperior",
3573    "ampersandsmall",
3574    "Acutesmall",
3575    "parenleftsuperior",
3576    "parenrightsuperior",
3577    "twodotenleader",
3578    "onedotenleader",
3579    "comma",
3580    "hyphen",
3581    "period",
3582    "fraction",
3583    "zerooldstyle",
3584    "oneoldstyle",
3585    "twooldstyle",
3586    "threeoldstyle",
3587    "fouroldstyle",
3588    "fiveoldstyle",
3589    "sixoldstyle",
3590    "sevenoldstyle",
3591    "eightoldstyle",
3592    "nineoldstyle",
3593    "colon",
3594    "semicolon",
3595    "commasuperior",
3596    "threequartersemdash",
3597    "periodsuperior",
3598    "questionsmall",
3599    "asuperior",
3600    "bsuperior",
3601    "centsuperior",
3602    "dsuperior",
3603    "esuperior",
3604    "isuperior",
3605    "lsuperior",
3606    "msuperior",
3607    "nsuperior",
3608    "osuperior",
3609    "rsuperior",
3610    "ssuperior",
3611    "tsuperior",
3612    "ff",
3613    "fi",
3614    "fl",
3615    "ffi",
3616    "ffl",
3617    "parenleftinferior",
3618    "parenrightinferior",
3619    "Circumflexsmall",
3620    "hyphensuperior",
3621    "Gravesmall",
3622    "Asmall",
3623    "Bsmall",
3624    "Csmall",
3625    "Dsmall",
3626    "Esmall",
3627    "Fsmall",
3628    "Gsmall",
3629    "Hsmall",
3630    "Ismall",
3631    "Jsmall",
3632    "Ksmall",
3633    "Lsmall",
3634    "Msmall",
3635    "Nsmall",
3636    "Osmall",
3637    "Psmall",
3638    "Qsmall",
3639    "Rsmall",
3640    "Ssmall",
3641    "Tsmall",
3642    "Usmall",
3643    "Vsmall",
3644    "Wsmall",
3645    "Xsmall",
3646    "Ysmall",
3647    "Zsmall",
3648    "colonmonetary",
3649    "onefitted",
3650    "rupiah",
3651    "Tildesmall",
3652    "exclamdownsmall",
3653    "centoldstyle",
3654    "Lslashsmall",
3655    "Scaronsmall",
3656    "Zcaronsmall",
3657    "Dieresissmall",
3658    "Brevesmall",
3659    "Caronsmall",
3660    "Dotaccentsmall",
3661    "Macronsmall",
3662    "figuredash",
3663    "hypheninferior",
3664    "Ogoneksmall",
3665    "Ringsmall",
3666    "Cedillasmall",
3667    "onequarter",
3668    "onehalf",
3669    "threequarters",
3670    "questiondownsmall",
3671    "oneeighth",
3672    "threeeighths",
3673    "fiveeighths",
3674    "seveneighths",
3675    "onethird",
3676    "twothirds",
3677    "zerosuperior",
3678    "onesuperior",
3679    "twosuperior",
3680    "threesuperior",
3681    "foursuperior",
3682    "fivesuperior",
3683    "sixsuperior",
3684    "sevensuperior",
3685    "eightsuperior",
3686    "ninesuperior",
3687    "zeroinferior",
3688    "oneinferior",
3689    "twoinferior",
3690    "threeinferior",
3691    "fourinferior",
3692    "fiveinferior",
3693    "sixinferior",
3694    "seveninferior",
3695    "eightinferior",
3696    "nineinferior",
3697    "centinferior",
3698    "dollarinferior",
3699    "periodinferior",
3700    "commainferior",
3701    "Agravesmall",
3702    "Aacutesmall",
3703    "Acircumflexsmall",
3704    "Atildesmall",
3705    "Adieresissmall",
3706    "Aringsmall",
3707    "AEsmall",
3708    "Ccedillasmall",
3709    "Egravesmall",
3710    "Eacutesmall",
3711    "Ecircumflexsmall",
3712    "Edieresissmall",
3713    "Igravesmall",
3714    "Iacutesmall",
3715    "Icircumflexsmall",
3716    "Idieresissmall",
3717    "Ethsmall",
3718    "Ntildesmall",
3719    "Ogravesmall",
3720    "Oacutesmall",
3721    "Ocircumflexsmall",
3722    "Otildesmall",
3723    "Odieresissmall",
3724    "OEsmall",
3725    "Oslashsmall",
3726    "Ugravesmall",
3727    "Uacutesmall",
3728    "Ucircumflexsmall",
3729    "Udieresissmall",
3730    "Yacutesmall",
3731    "Thornsmall",
3732    "Ydieresissmall",
3733]
3734
3735cffExpertStringCount = 166
3736assert len(cffIExpertStrings) == cffExpertStringCount
3737
3738cffExpertSubsetStrings = [
3739    ".notdef",
3740    "space",
3741    "dollaroldstyle",
3742    "dollarsuperior",
3743    "parenleftsuperior",
3744    "parenrightsuperior",
3745    "twodotenleader",
3746    "onedotenleader",
3747    "comma",
3748    "hyphen",
3749    "period",
3750    "fraction",
3751    "zerooldstyle",
3752    "oneoldstyle",
3753    "twooldstyle",
3754    "threeoldstyle",
3755    "fouroldstyle",
3756    "fiveoldstyle",
3757    "sixoldstyle",
3758    "sevenoldstyle",
3759    "eightoldstyle",
3760    "nineoldstyle",
3761    "colon",
3762    "semicolon",
3763    "commasuperior",
3764    "threequartersemdash",
3765    "periodsuperior",
3766    "asuperior",
3767    "bsuperior",
3768    "centsuperior",
3769    "dsuperior",
3770    "esuperior",
3771    "isuperior",
3772    "lsuperior",
3773    "msuperior",
3774    "nsuperior",
3775    "osuperior",
3776    "rsuperior",
3777    "ssuperior",
3778    "tsuperior",
3779    "ff",
3780    "fi",
3781    "fl",
3782    "ffi",
3783    "ffl",
3784    "parenleftinferior",
3785    "parenrightinferior",
3786    "hyphensuperior",
3787    "colonmonetary",
3788    "onefitted",
3789    "rupiah",
3790    "centoldstyle",
3791    "figuredash",
3792    "hypheninferior",
3793    "onequarter",
3794    "onehalf",
3795    "threequarters",
3796    "oneeighth",
3797    "threeeighths",
3798    "fiveeighths",
3799    "seveneighths",
3800    "onethird",
3801    "twothirds",
3802    "zerosuperior",
3803    "onesuperior",
3804    "twosuperior",
3805    "threesuperior",
3806    "foursuperior",
3807    "fivesuperior",
3808    "sixsuperior",
3809    "sevensuperior",
3810    "eightsuperior",
3811    "ninesuperior",
3812    "zeroinferior",
3813    "oneinferior",
3814    "twoinferior",
3815    "threeinferior",
3816    "fourinferior",
3817    "fiveinferior",
3818    "sixinferior",
3819    "seveninferior",
3820    "eightinferior",
3821    "nineinferior",
3822    "centinferior",
3823    "dollarinferior",
3824    "periodinferior",
3825    "commainferior",
3826]
3827
3828cffExpertSubsetStringCount = 87
3829assert len(cffExpertSubsetStrings) == cffExpertSubsetStringCount
3830