xref: /aosp_15_r20/external/fonttools/Lib/fontTools/ttLib/ttFont.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
1from fontTools.config import Config
2from fontTools.misc import xmlWriter
3from fontTools.misc.configTools import AbstractConfig
4from fontTools.misc.textTools import Tag, byteord, tostr
5from fontTools.misc.loggingTools import deprecateArgument
6from fontTools.ttLib import TTLibError
7from fontTools.ttLib.ttGlyphSet import _TTGlyph, _TTGlyphSetCFF, _TTGlyphSetGlyf
8from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
9from io import BytesIO, StringIO, UnsupportedOperation
10import os
11import logging
12import traceback
13
14log = logging.getLogger(__name__)
15
16
17class TTFont(object):
18    """Represents a TrueType font.
19
20    The object manages file input and output, and offers a convenient way of
21    accessing tables. Tables will be only decompiled when necessary, ie. when
22    they're actually accessed. This means that simple operations can be extremely fast.
23
24    Example usage::
25
26            >> from fontTools import ttLib
27            >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file
28            >> tt['maxp'].numGlyphs
29            242
30            >> tt['OS/2'].achVendID
31            'B&H\000'
32            >> tt['head'].unitsPerEm
33            2048
34
35    For details of the objects returned when accessing each table, see :ref:`tables`.
36    To add a table to the font, use the :py:func:`newTable` function::
37
38            >> os2 = newTable("OS/2")
39            >> os2.version = 4
40            >> # set other attributes
41            >> font["OS/2"] = os2
42
43    TrueType fonts can also be serialized to and from XML format (see also the
44    :ref:`ttx` binary)::
45
46            >> tt.saveXML("afont.ttx")
47            Dumping 'LTSH' table...
48            Dumping 'OS/2' table...
49            [...]
50
51            >> tt2 = ttLib.TTFont() # Create a new font object
52            >> tt2.importXML("afont.ttx")
53            >> tt2['maxp'].numGlyphs
54            242
55
56    The TTFont object may be used as a context manager; this will cause the file
57    reader to be closed after the context ``with`` block is exited::
58
59            with TTFont(filename) as f:
60                    # Do stuff
61
62    Args:
63            file: When reading a font from disk, either a pathname pointing to a file,
64                    or a readable file object.
65            res_name_or_index: If running on a Macintosh, either a sfnt resource name or
66                    an sfnt resource index number. If the index number is zero, TTLib will
67                    autodetect whether the file is a flat file or a suitcase. (If it is a suitcase,
68                    only the first 'sfnt' resource will be read.)
69            sfntVersion (str): When constructing a font object from scratch, sets the four-byte
70                    sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create
71                    an OpenType file, use ``OTTO``.
72            flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2
73                    file.
74            checkChecksums (int): How checksum data should be treated. Default is 0
75                    (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to
76                    raise an exception if any wrong checksums are found.
77            recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``,
78                    ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save.
79                    Also compiles the glyphs on importing, which saves memory consumption and
80                    time.
81            ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation
82                    will be ignored, and the binary data will be returned for those tables instead.
83            recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in
84                    the ``head`` table on save.
85            fontNumber (int): The index of the font in a TrueType Collection file.
86            lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon
87                    access only. If it is set to False, many data structures are loaded immediately.
88                    The default is ``lazy=None`` which is somewhere in between.
89    """
90
91    def __init__(
92        self,
93        file=None,
94        res_name_or_index=None,
95        sfntVersion="\000\001\000\000",
96        flavor=None,
97        checkChecksums=0,
98        verbose=None,
99        recalcBBoxes=True,
100        allowVID=NotImplemented,
101        ignoreDecompileErrors=False,
102        recalcTimestamp=True,
103        fontNumber=-1,
104        lazy=None,
105        quiet=None,
106        _tableCache=None,
107        cfg={},
108    ):
109        for name in ("verbose", "quiet"):
110            val = locals().get(name)
111            if val is not None:
112                deprecateArgument(name, "configure logging instead")
113            setattr(self, name, val)
114
115        self.lazy = lazy
116        self.recalcBBoxes = recalcBBoxes
117        self.recalcTimestamp = recalcTimestamp
118        self.tables = {}
119        self.reader = None
120        self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg)
121        self.ignoreDecompileErrors = ignoreDecompileErrors
122
123        if not file:
124            self.sfntVersion = sfntVersion
125            self.flavor = flavor
126            self.flavorData = None
127            return
128        seekable = True
129        if not hasattr(file, "read"):
130            closeStream = True
131            # assume file is a string
132            if res_name_or_index is not None:
133                # see if it contains 'sfnt' resources in the resource or data fork
134                from . import macUtils
135
136                if res_name_or_index == 0:
137                    if macUtils.getSFNTResIndices(file):
138                        # get the first available sfnt font.
139                        file = macUtils.SFNTResourceReader(file, 1)
140                    else:
141                        file = open(file, "rb")
142                else:
143                    file = macUtils.SFNTResourceReader(file, res_name_or_index)
144            else:
145                file = open(file, "rb")
146        else:
147            # assume "file" is a readable file object
148            closeStream = False
149            # SFNTReader wants the input file to be seekable.
150            # SpooledTemporaryFile has no seekable() on < 3.11, but still can seek:
151            # https://github.com/fonttools/fonttools/issues/3052
152            if hasattr(file, "seekable"):
153                seekable = file.seekable()
154            elif hasattr(file, "seek"):
155                try:
156                    file.seek(0)
157                except UnsupportedOperation:
158                    seekable = False
159
160        if not self.lazy:
161            # read input file in memory and wrap a stream around it to allow overwriting
162            if seekable:
163                file.seek(0)
164            tmp = BytesIO(file.read())
165            if hasattr(file, "name"):
166                # save reference to input file name
167                tmp.name = file.name
168            if closeStream:
169                file.close()
170            file = tmp
171        elif not seekable:
172            raise TTLibError("Input file must be seekable when lazy=True")
173        self._tableCache = _tableCache
174        self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber)
175        self.sfntVersion = self.reader.sfntVersion
176        self.flavor = self.reader.flavor
177        self.flavorData = self.reader.flavorData
178
179    def __enter__(self):
180        return self
181
182    def __exit__(self, type, value, traceback):
183        self.close()
184
185    def close(self):
186        """If we still have a reader object, close it."""
187        if self.reader is not None:
188            self.reader.close()
189
190    def save(self, file, reorderTables=True):
191        """Save the font to disk.
192
193        Args:
194                file: Similarly to the constructor, can be either a pathname or a writable
195                        file object.
196                reorderTables (Option[bool]): If true (the default), reorder the tables,
197                        sorting them by tag (recommended by the OpenType specification). If
198                        false, retain the original font order. If None, reorder by table
199                        dependency (fastest).
200        """
201        if not hasattr(file, "write"):
202            if self.lazy and self.reader.file.name == file:
203                raise TTLibError("Can't overwrite TTFont when 'lazy' attribute is True")
204            createStream = True
205        else:
206            # assume "file" is a writable file object
207            createStream = False
208
209        tmp = BytesIO()
210
211        writer_reordersTables = self._save(tmp)
212
213        if not (
214            reorderTables is None
215            or writer_reordersTables
216            or (reorderTables is False and self.reader is None)
217        ):
218            if reorderTables is False:
219                # sort tables using the original font's order
220                tableOrder = list(self.reader.keys())
221            else:
222                # use the recommended order from the OpenType specification
223                tableOrder = None
224            tmp.flush()
225            tmp2 = BytesIO()
226            reorderFontTables(tmp, tmp2, tableOrder)
227            tmp.close()
228            tmp = tmp2
229
230        if createStream:
231            # "file" is a path
232            with open(file, "wb") as file:
233                file.write(tmp.getvalue())
234        else:
235            file.write(tmp.getvalue())
236
237        tmp.close()
238
239    def _save(self, file, tableCache=None):
240        """Internal function, to be shared by save() and TTCollection.save()"""
241
242        if self.recalcTimestamp and "head" in self:
243            self[
244                "head"
245            ]  # make sure 'head' is loaded so the recalculation is actually done
246
247        tags = list(self.keys())
248        if "GlyphOrder" in tags:
249            tags.remove("GlyphOrder")
250        numTables = len(tags)
251        # write to a temporary stream to allow saving to unseekable streams
252        writer = SFNTWriter(
253            file, numTables, self.sfntVersion, self.flavor, self.flavorData
254        )
255
256        done = []
257        for tag in tags:
258            self._writeTable(tag, writer, done, tableCache)
259
260        writer.close()
261
262        return writer.reordersTables()
263
264    def saveXML(self, fileOrPath, newlinestr="\n", **kwargs):
265        """Export the font as TTX (an XML-based text file), or as a series of text
266        files when splitTables is true. In the latter case, the 'fileOrPath'
267        argument should be a path to a directory.
268        The 'tables' argument must either be false (dump all tables) or a
269        list of tables to dump. The 'skipTables' argument may be a list of tables
270        to skip, but only when the 'tables' argument is false.
271        """
272
273        writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
274        self._saveXML(writer, **kwargs)
275        writer.close()
276
277    def _saveXML(
278        self,
279        writer,
280        writeVersion=True,
281        quiet=None,
282        tables=None,
283        skipTables=None,
284        splitTables=False,
285        splitGlyphs=False,
286        disassembleInstructions=True,
287        bitmapGlyphDataFormat="raw",
288    ):
289        if quiet is not None:
290            deprecateArgument("quiet", "configure logging instead")
291
292        self.disassembleInstructions = disassembleInstructions
293        self.bitmapGlyphDataFormat = bitmapGlyphDataFormat
294        if not tables:
295            tables = list(self.keys())
296            if "GlyphOrder" not in tables:
297                tables = ["GlyphOrder"] + tables
298            if skipTables:
299                for tag in skipTables:
300                    if tag in tables:
301                        tables.remove(tag)
302        numTables = len(tables)
303
304        if writeVersion:
305            from fontTools import version
306
307            version = ".".join(version.split(".")[:2])
308            writer.begintag(
309                "ttFont",
310                sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
311                ttLibVersion=version,
312            )
313        else:
314            writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1])
315        writer.newline()
316
317        # always splitTables if splitGlyphs is enabled
318        splitTables = splitTables or splitGlyphs
319
320        if not splitTables:
321            writer.newline()
322        else:
323            path, ext = os.path.splitext(writer.filename)
324
325        for i in range(numTables):
326            tag = tables[i]
327            if splitTables:
328                tablePath = path + "." + tagToIdentifier(tag) + ext
329                tableWriter = xmlWriter.XMLWriter(
330                    tablePath, newlinestr=writer.newlinestr
331                )
332                tableWriter.begintag("ttFont", ttLibVersion=version)
333                tableWriter.newline()
334                tableWriter.newline()
335                writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath))
336                writer.newline()
337            else:
338                tableWriter = writer
339            self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs)
340            if splitTables:
341                tableWriter.endtag("ttFont")
342                tableWriter.newline()
343                tableWriter.close()
344        writer.endtag("ttFont")
345        writer.newline()
346
347    def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False):
348        if quiet is not None:
349            deprecateArgument("quiet", "configure logging instead")
350        if tag in self:
351            table = self[tag]
352            report = "Dumping '%s' table..." % tag
353        else:
354            report = "No '%s' table found." % tag
355        log.info(report)
356        if tag not in self:
357            return
358        xmlTag = tagToXML(tag)
359        attrs = dict()
360        if hasattr(table, "ERROR"):
361            attrs["ERROR"] = "decompilation error"
362        from .tables.DefaultTable import DefaultTable
363
364        if table.__class__ == DefaultTable:
365            attrs["raw"] = True
366        writer.begintag(xmlTag, **attrs)
367        writer.newline()
368        if tag == "glyf":
369            table.toXML(writer, self, splitGlyphs=splitGlyphs)
370        else:
371            table.toXML(writer, self)
372        writer.endtag(xmlTag)
373        writer.newline()
374        writer.newline()
375
376    def importXML(self, fileOrPath, quiet=None):
377        """Import a TTX file (an XML-based text format), so as to recreate
378        a font object.
379        """
380        if quiet is not None:
381            deprecateArgument("quiet", "configure logging instead")
382
383        if "maxp" in self and "post" in self:
384            # Make sure the glyph order is loaded, as it otherwise gets
385            # lost if the XML doesn't contain the glyph order, yet does
386            # contain the table which was originally used to extract the
387            # glyph names from (ie. 'post', 'cmap' or 'CFF ').
388            self.getGlyphOrder()
389
390        from fontTools.misc import xmlReader
391
392        reader = xmlReader.XMLReader(fileOrPath, self)
393        reader.read()
394
395    def isLoaded(self, tag):
396        """Return true if the table identified by ``tag`` has been
397        decompiled and loaded into memory."""
398        return tag in self.tables
399
400    def has_key(self, tag):
401        """Test if the table identified by ``tag`` is present in the font.
402
403        As well as this method, ``tag in font`` can also be used to determine the
404        presence of the table."""
405        if self.isLoaded(tag):
406            return True
407        elif self.reader and tag in self.reader:
408            return True
409        elif tag == "GlyphOrder":
410            return True
411        else:
412            return False
413
414    __contains__ = has_key
415
416    def keys(self):
417        """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table."""
418        keys = list(self.tables.keys())
419        if self.reader:
420            for key in list(self.reader.keys()):
421                if key not in keys:
422                    keys.append(key)
423
424        if "GlyphOrder" in keys:
425            keys.remove("GlyphOrder")
426        keys = sortedTagList(keys)
427        return ["GlyphOrder"] + keys
428
429    def ensureDecompiled(self, recurse=None):
430        """Decompile all the tables, even if a TTFont was opened in 'lazy' mode."""
431        for tag in self.keys():
432            table = self[tag]
433            if recurse is None:
434                recurse = self.lazy is not False
435            if recurse and hasattr(table, "ensureDecompiled"):
436                table.ensureDecompiled(recurse=recurse)
437        self.lazy = False
438
439    def __len__(self):
440        return len(list(self.keys()))
441
442    def __getitem__(self, tag):
443        tag = Tag(tag)
444        table = self.tables.get(tag)
445        if table is None:
446            if tag == "GlyphOrder":
447                table = GlyphOrder(tag)
448                self.tables[tag] = table
449            elif self.reader is not None:
450                table = self._readTable(tag)
451            else:
452                raise KeyError("'%s' table not found" % tag)
453        return table
454
455    def _readTable(self, tag):
456        log.debug("Reading '%s' table from disk", tag)
457        data = self.reader[tag]
458        if self._tableCache is not None:
459            table = self._tableCache.get((tag, data))
460            if table is not None:
461                return table
462        tableClass = getTableClass(tag)
463        table = tableClass(tag)
464        self.tables[tag] = table
465        log.debug("Decompiling '%s' table", tag)
466        try:
467            table.decompile(data, self)
468        except Exception:
469            if not self.ignoreDecompileErrors:
470                raise
471            # fall back to DefaultTable, retaining the binary table data
472            log.exception(
473                "An exception occurred during the decompilation of the '%s' table", tag
474            )
475            from .tables.DefaultTable import DefaultTable
476
477            file = StringIO()
478            traceback.print_exc(file=file)
479            table = DefaultTable(tag)
480            table.ERROR = file.getvalue()
481            self.tables[tag] = table
482            table.decompile(data, self)
483        if self._tableCache is not None:
484            self._tableCache[(tag, data)] = table
485        return table
486
487    def __setitem__(self, tag, table):
488        self.tables[Tag(tag)] = table
489
490    def __delitem__(self, tag):
491        if tag not in self:
492            raise KeyError("'%s' table not found" % tag)
493        if tag in self.tables:
494            del self.tables[tag]
495        if self.reader and tag in self.reader:
496            del self.reader[tag]
497
498    def get(self, tag, default=None):
499        """Returns the table if it exists or (optionally) a default if it doesn't."""
500        try:
501            return self[tag]
502        except KeyError:
503            return default
504
505    def setGlyphOrder(self, glyphOrder):
506        """Set the glyph order
507
508        Args:
509                glyphOrder ([str]): List of glyph names in order.
510        """
511        self.glyphOrder = glyphOrder
512        if hasattr(self, "_reverseGlyphOrderDict"):
513            del self._reverseGlyphOrderDict
514        if self.isLoaded("glyf"):
515            self["glyf"].setGlyphOrder(glyphOrder)
516
517    def getGlyphOrder(self):
518        """Returns a list of glyph names ordered by their position in the font."""
519        try:
520            return self.glyphOrder
521        except AttributeError:
522            pass
523        if "CFF " in self:
524            cff = self["CFF "]
525            self.glyphOrder = cff.getGlyphOrder()
526        elif "post" in self:
527            # TrueType font
528            glyphOrder = self["post"].getGlyphOrder()
529            if glyphOrder is None:
530                #
531                # No names found in the 'post' table.
532                # Try to create glyph names from the unicode cmap (if available)
533                # in combination with the Adobe Glyph List (AGL).
534                #
535                self._getGlyphNamesFromCmap()
536            elif len(glyphOrder) < self["maxp"].numGlyphs:
537                #
538                # Not enough names found in the 'post' table.
539                # Can happen when 'post' format 1 is improperly used on a font that
540                # has more than 258 glyphs (the lenght of 'standardGlyphOrder').
541                #
542                log.warning(
543                    "Not enough names found in the 'post' table, generating them from cmap instead"
544                )
545                self._getGlyphNamesFromCmap()
546            else:
547                self.glyphOrder = glyphOrder
548        else:
549            self._getGlyphNamesFromCmap()
550        return self.glyphOrder
551
552    def _getGlyphNamesFromCmap(self):
553        #
554        # This is rather convoluted, but then again, it's an interesting problem:
555        # - we need to use the unicode values found in the cmap table to
556        #   build glyph names (eg. because there is only a minimal post table,
557        #   or none at all).
558        # - but the cmap parser also needs glyph names to work with...
559        # So here's what we do:
560        # - make up glyph names based on glyphID
561        # - load a temporary cmap table based on those names
562        # - extract the unicode values, build the "real" glyph names
563        # - unload the temporary cmap table
564        #
565        if self.isLoaded("cmap"):
566            # Bootstrapping: we're getting called by the cmap parser
567            # itself. This means self.tables['cmap'] contains a partially
568            # loaded cmap, making it impossible to get at a unicode
569            # subtable here. We remove the partially loaded cmap and
570            # restore it later.
571            # This only happens if the cmap table is loaded before any
572            # other table that does f.getGlyphOrder()  or f.getGlyphName().
573            cmapLoading = self.tables["cmap"]
574            del self.tables["cmap"]
575        else:
576            cmapLoading = None
577        # Make up glyph names based on glyphID, which will be used by the
578        # temporary cmap and by the real cmap in case we don't find a unicode
579        # cmap.
580        numGlyphs = int(self["maxp"].numGlyphs)
581        glyphOrder = [None] * numGlyphs
582        glyphOrder[0] = ".notdef"
583        for i in range(1, numGlyphs):
584            glyphOrder[i] = "glyph%.5d" % i
585        # Set the glyph order, so the cmap parser has something
586        # to work with (so we don't get called recursively).
587        self.glyphOrder = glyphOrder
588
589        # Make up glyph names based on the reversed cmap table. Because some
590        # glyphs (eg. ligatures or alternates) may not be reachable via cmap,
591        # this naming table will usually not cover all glyphs in the font.
592        # If the font has no Unicode cmap table, reversecmap will be empty.
593        if "cmap" in self:
594            reversecmap = self["cmap"].buildReversed()
595        else:
596            reversecmap = {}
597        useCount = {}
598        for i in range(numGlyphs):
599            tempName = glyphOrder[i]
600            if tempName in reversecmap:
601                # If a font maps both U+0041 LATIN CAPITAL LETTER A and
602                # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph,
603                # we prefer naming the glyph as "A".
604                glyphName = self._makeGlyphName(min(reversecmap[tempName]))
605                numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1
606                if numUses > 1:
607                    glyphName = "%s.alt%d" % (glyphName, numUses - 1)
608                glyphOrder[i] = glyphName
609
610        if "cmap" in self:
611            # Delete the temporary cmap table from the cache, so it can
612            # be parsed again with the right names.
613            del self.tables["cmap"]
614            self.glyphOrder = glyphOrder
615            if cmapLoading:
616                # restore partially loaded cmap, so it can continue loading
617                # using the proper names.
618                self.tables["cmap"] = cmapLoading
619
620    @staticmethod
621    def _makeGlyphName(codepoint):
622        from fontTools import agl  # Adobe Glyph List
623
624        if codepoint in agl.UV2AGL:
625            return agl.UV2AGL[codepoint]
626        elif codepoint <= 0xFFFF:
627            return "uni%04X" % codepoint
628        else:
629            return "u%X" % codepoint
630
631    def getGlyphNames(self):
632        """Get a list of glyph names, sorted alphabetically."""
633        glyphNames = sorted(self.getGlyphOrder())
634        return glyphNames
635
636    def getGlyphNames2(self):
637        """Get a list of glyph names, sorted alphabetically,
638        but not case sensitive.
639        """
640        from fontTools.misc import textTools
641
642        return textTools.caselessSort(self.getGlyphOrder())
643
644    def getGlyphName(self, glyphID):
645        """Returns the name for the glyph with the given ID.
646
647        If no name is available, synthesises one with the form ``glyphXXXXX``` where
648        ```XXXXX`` is the zero-padded glyph ID.
649        """
650        try:
651            return self.getGlyphOrder()[glyphID]
652        except IndexError:
653            return "glyph%.5d" % glyphID
654
655    def getGlyphNameMany(self, lst):
656        """Converts a list of glyph IDs into a list of glyph names."""
657        glyphOrder = self.getGlyphOrder()
658        cnt = len(glyphOrder)
659        return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid for gid in lst]
660
661    def getGlyphID(self, glyphName):
662        """Returns the ID of the glyph with the given name."""
663        try:
664            return self.getReverseGlyphMap()[glyphName]
665        except KeyError:
666            if glyphName[:5] == "glyph":
667                try:
668                    return int(glyphName[5:])
669                except (NameError, ValueError):
670                    raise KeyError(glyphName)
671            raise
672
673    def getGlyphIDMany(self, lst):
674        """Converts a list of glyph names into a list of glyph IDs."""
675        d = self.getReverseGlyphMap()
676        try:
677            return [d[glyphName] for glyphName in lst]
678        except KeyError:
679            getGlyphID = self.getGlyphID
680            return [getGlyphID(glyphName) for glyphName in lst]
681
682    def getReverseGlyphMap(self, rebuild=False):
683        """Returns a mapping of glyph names to glyph IDs."""
684        if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
685            self._buildReverseGlyphOrderDict()
686        return self._reverseGlyphOrderDict
687
688    def _buildReverseGlyphOrderDict(self):
689        self._reverseGlyphOrderDict = d = {}
690        for glyphID, glyphName in enumerate(self.getGlyphOrder()):
691            d[glyphName] = glyphID
692        return d
693
694    def _writeTable(self, tag, writer, done, tableCache=None):
695        """Internal helper function for self.save(). Keeps track of
696        inter-table dependencies.
697        """
698        if tag in done:
699            return
700        tableClass = getTableClass(tag)
701        for masterTable in tableClass.dependencies:
702            if masterTable not in done:
703                if masterTable in self:
704                    self._writeTable(masterTable, writer, done, tableCache)
705                else:
706                    done.append(masterTable)
707        done.append(tag)
708        tabledata = self.getTableData(tag)
709        if tableCache is not None:
710            entry = tableCache.get((Tag(tag), tabledata))
711            if entry is not None:
712                log.debug("reusing '%s' table", tag)
713                writer.setEntry(tag, entry)
714                return
715        log.debug("Writing '%s' table to disk", tag)
716        writer[tag] = tabledata
717        if tableCache is not None:
718            tableCache[(Tag(tag), tabledata)] = writer[tag]
719
720    def getTableData(self, tag):
721        """Returns the binary representation of a table.
722
723        If the table is currently loaded and in memory, the data is compiled to
724        binary and returned; if it is not currently loaded, the binary data is
725        read from the font file and returned.
726        """
727        tag = Tag(tag)
728        if self.isLoaded(tag):
729            log.debug("Compiling '%s' table", tag)
730            return self.tables[tag].compile(self)
731        elif self.reader and tag in self.reader:
732            log.debug("Reading '%s' table from disk", tag)
733            return self.reader[tag]
734        else:
735            raise KeyError(tag)
736
737    def getGlyphSet(
738        self, preferCFF=True, location=None, normalized=False, recalcBounds=True
739    ):
740        """Return a generic GlyphSet, which is a dict-like object
741        mapping glyph names to glyph objects. The returned glyph objects
742        have a ``.draw()`` method that supports the Pen protocol, and will
743        have an attribute named 'width'.
744
745        If the font is CFF-based, the outlines will be taken from the ``CFF ``
746        or ``CFF2`` tables. Otherwise the outlines will be taken from the
747        ``glyf`` table.
748
749        If the font contains both a ``CFF ``/``CFF2`` and a ``glyf`` table, you
750        can use the ``preferCFF`` argument to specify which one should be taken.
751        If the font contains both a ``CFF `` and a ``CFF2`` table, the latter is
752        taken.
753
754        If the ``location`` parameter is set, it should be a dictionary mapping
755        four-letter variation tags to their float values, and the returned
756        glyph-set will represent an instance of a variable font at that
757        location.
758
759        If the ``normalized`` variable is set to True, that location is
760        interpreted as in the normalized (-1..+1) space, otherwise it is in the
761        font's defined axes space.
762        """
763        if location and "fvar" not in self:
764            location = None
765        if location and not normalized:
766            location = self.normalizeLocation(location)
767        if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self):
768            return _TTGlyphSetCFF(self, location)
769        elif "glyf" in self:
770            return _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds)
771        else:
772            raise TTLibError("Font contains no outlines")
773
774    def normalizeLocation(self, location):
775        """Normalize a ``location`` from the font's defined axes space (also
776        known as user space) into the normalized (-1..+1) space. It applies
777        ``avar`` mapping if the font contains an ``avar`` table.
778
779        The ``location`` parameter should be a dictionary mapping four-letter
780        variation tags to their float values.
781
782        Raises ``TTLibError`` if the font is not a variable font.
783        """
784        from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap
785
786        if "fvar" not in self:
787            raise TTLibError("Not a variable font")
788
789        axes = {
790            a.axisTag: (a.minValue, a.defaultValue, a.maxValue)
791            for a in self["fvar"].axes
792        }
793        location = normalizeLocation(location, axes)
794        if "avar" in self:
795            avar = self["avar"]
796            avarSegments = avar.segments
797            mappedLocation = {}
798            for axisTag, value in location.items():
799                avarMapping = avarSegments.get(axisTag, None)
800                if avarMapping is not None:
801                    value = piecewiseLinearMap(value, avarMapping)
802                mappedLocation[axisTag] = value
803            location = mappedLocation
804        return location
805
806    def getBestCmap(
807        self,
808        cmapPreferences=(
809            (3, 10),
810            (0, 6),
811            (0, 4),
812            (3, 1),
813            (0, 3),
814            (0, 2),
815            (0, 1),
816            (0, 0),
817        ),
818    ):
819        """Returns the 'best' Unicode cmap dictionary available in the font
820        or ``None``, if no Unicode cmap subtable is available.
821
822        By default it will search for the following (platformID, platEncID)
823        pairs in order::
824
825                        (3, 10), # Windows Unicode full repertoire
826                        (0, 6),  # Unicode full repertoire (format 13 subtable)
827                        (0, 4),  # Unicode 2.0 full repertoire
828                        (3, 1),  # Windows Unicode BMP
829                        (0, 3),  # Unicode 2.0 BMP
830                        (0, 2),  # Unicode ISO/IEC 10646
831                        (0, 1),  # Unicode 1.1
832                        (0, 0)   # Unicode 1.0
833
834        This particular order matches what HarfBuzz uses to choose what
835        subtable to use by default. This order prefers the largest-repertoire
836        subtable, and among those, prefers the Windows-platform over the
837        Unicode-platform as the former has wider support.
838
839        This order can be customized via the ``cmapPreferences`` argument.
840        """
841        return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
842
843
844class GlyphOrder(object):
845    """A pseudo table. The glyph order isn't in the font as a separate
846    table, but it's nice to present it as such in the TTX format.
847    """
848
849    def __init__(self, tag=None):
850        pass
851
852    def toXML(self, writer, ttFont):
853        glyphOrder = ttFont.getGlyphOrder()
854        writer.comment(
855            "The 'id' attribute is only for humans; " "it is ignored when parsed."
856        )
857        writer.newline()
858        for i in range(len(glyphOrder)):
859            glyphName = glyphOrder[i]
860            writer.simpletag("GlyphID", id=i, name=glyphName)
861            writer.newline()
862
863    def fromXML(self, name, attrs, content, ttFont):
864        if not hasattr(self, "glyphOrder"):
865            self.glyphOrder = []
866        if name == "GlyphID":
867            self.glyphOrder.append(attrs["name"])
868        ttFont.setGlyphOrder(self.glyphOrder)
869
870
871def getTableModule(tag):
872    """Fetch the packer/unpacker module for a table.
873    Return None when no module is found.
874    """
875    from . import tables
876
877    pyTag = tagToIdentifier(tag)
878    try:
879        __import__("fontTools.ttLib.tables." + pyTag)
880    except ImportError as err:
881        # If pyTag is found in the ImportError message,
882        # means table is not implemented.  If it's not
883        # there, then some other module is missing, don't
884        # suppress the error.
885        if str(err).find(pyTag) >= 0:
886            return None
887        else:
888            raise err
889    else:
890        return getattr(tables, pyTag)
891
892
893# Registry for custom table packer/unpacker classes. Keys are table
894# tags, values are (moduleName, className) tuples.
895# See registerCustomTableClass() and getCustomTableClass()
896_customTableRegistry = {}
897
898
899def registerCustomTableClass(tag, moduleName, className=None):
900    """Register a custom packer/unpacker class for a table.
901
902    The 'moduleName' must be an importable module. If no 'className'
903    is given, it is derived from the tag, for example it will be
904    ``table_C_U_S_T_`` for a 'CUST' tag.
905
906    The registered table class should be a subclass of
907    :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable`
908    """
909    if className is None:
910        className = "table_" + tagToIdentifier(tag)
911    _customTableRegistry[tag] = (moduleName, className)
912
913
914def unregisterCustomTableClass(tag):
915    """Unregister the custom packer/unpacker class for a table."""
916    del _customTableRegistry[tag]
917
918
919def getCustomTableClass(tag):
920    """Return the custom table class for tag, if one has been registered
921    with 'registerCustomTableClass()'. Else return None.
922    """
923    if tag not in _customTableRegistry:
924        return None
925    import importlib
926
927    moduleName, className = _customTableRegistry[tag]
928    module = importlib.import_module(moduleName)
929    return getattr(module, className)
930
931
932def getTableClass(tag):
933    """Fetch the packer/unpacker class for a table."""
934    tableClass = getCustomTableClass(tag)
935    if tableClass is not None:
936        return tableClass
937    module = getTableModule(tag)
938    if module is None:
939        from .tables.DefaultTable import DefaultTable
940
941        return DefaultTable
942    pyTag = tagToIdentifier(tag)
943    tableClass = getattr(module, "table_" + pyTag)
944    return tableClass
945
946
947def getClassTag(klass):
948    """Fetch the table tag for a class object."""
949    name = klass.__name__
950    assert name[:6] == "table_"
951    name = name[6:]  # Chop 'table_'
952    return identifierToTag(name)
953
954
955def newTable(tag):
956    """Return a new instance of a table."""
957    tableClass = getTableClass(tag)
958    return tableClass(tag)
959
960
961def _escapechar(c):
962    """Helper function for tagToIdentifier()"""
963    import re
964
965    if re.match("[a-z0-9]", c):
966        return "_" + c
967    elif re.match("[A-Z]", c):
968        return c + "_"
969    else:
970        return hex(byteord(c))[2:]
971
972
973def tagToIdentifier(tag):
974    """Convert a table tag to a valid (but UGLY) python identifier,
975    as well as a filename that's guaranteed to be unique even on a
976    caseless file system. Each character is mapped to two characters.
977    Lowercase letters get an underscore before the letter, uppercase
978    letters get an underscore after the letter. Trailing spaces are
979    trimmed. Illegal characters are escaped as two hex bytes. If the
980    result starts with a number (as the result of a hex escape), an
981    extra underscore is prepended. Examples::
982
983            >>> tagToIdentifier('glyf')
984            '_g_l_y_f'
985            >>> tagToIdentifier('cvt ')
986            '_c_v_t'
987            >>> tagToIdentifier('OS/2')
988            'O_S_2f_2'
989    """
990    import re
991
992    tag = Tag(tag)
993    if tag == "GlyphOrder":
994        return tag
995    assert len(tag) == 4, "tag should be 4 characters long"
996    while len(tag) > 1 and tag[-1] == " ":
997        tag = tag[:-1]
998    ident = ""
999    for c in tag:
1000        ident = ident + _escapechar(c)
1001    if re.match("[0-9]", ident):
1002        ident = "_" + ident
1003    return ident
1004
1005
1006def identifierToTag(ident):
1007    """the opposite of tagToIdentifier()"""
1008    if ident == "GlyphOrder":
1009        return ident
1010    if len(ident) % 2 and ident[0] == "_":
1011        ident = ident[1:]
1012    assert not (len(ident) % 2)
1013    tag = ""
1014    for i in range(0, len(ident), 2):
1015        if ident[i] == "_":
1016            tag = tag + ident[i + 1]
1017        elif ident[i + 1] == "_":
1018            tag = tag + ident[i]
1019        else:
1020            # assume hex
1021            tag = tag + chr(int(ident[i : i + 2], 16))
1022    # append trailing spaces
1023    tag = tag + (4 - len(tag)) * " "
1024    return Tag(tag)
1025
1026
1027def tagToXML(tag):
1028    """Similarly to tagToIdentifier(), this converts a TT tag
1029    to a valid XML element name. Since XML element names are
1030    case sensitive, this is a fairly simple/readable translation.
1031    """
1032    import re
1033
1034    tag = Tag(tag)
1035    if tag == "OS/2":
1036        return "OS_2"
1037    elif tag == "GlyphOrder":
1038        return tag
1039    if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag):
1040        return tag.strip()
1041    else:
1042        return tagToIdentifier(tag)
1043
1044
1045def xmlToTag(tag):
1046    """The opposite of tagToXML()"""
1047    if tag == "OS_2":
1048        return Tag("OS/2")
1049    if len(tag) == 8:
1050        return identifierToTag(tag)
1051    else:
1052        return Tag(tag + " " * (4 - len(tag)))
1053
1054
1055# Table order as recommended in the OpenType specification 1.4
1056TTFTableOrder = [
1057    "head",
1058    "hhea",
1059    "maxp",
1060    "OS/2",
1061    "hmtx",
1062    "LTSH",
1063    "VDMX",
1064    "hdmx",
1065    "cmap",
1066    "fpgm",
1067    "prep",
1068    "cvt ",
1069    "loca",
1070    "glyf",
1071    "kern",
1072    "name",
1073    "post",
1074    "gasp",
1075    "PCLT",
1076]
1077
1078OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "]
1079
1080
1081def sortedTagList(tagList, tableOrder=None):
1082    """Return a sorted copy of tagList, sorted according to the OpenType
1083    specification, or according to a custom tableOrder. If given and not
1084    None, tableOrder needs to be a list of tag names.
1085    """
1086    tagList = sorted(tagList)
1087    if tableOrder is None:
1088        if "DSIG" in tagList:
1089            # DSIG should be last (XXX spec reference?)
1090            tagList.remove("DSIG")
1091            tagList.append("DSIG")
1092        if "CFF " in tagList:
1093            tableOrder = OTFTableOrder
1094        else:
1095            tableOrder = TTFTableOrder
1096    orderedTables = []
1097    for tag in tableOrder:
1098        if tag in tagList:
1099            orderedTables.append(tag)
1100            tagList.remove(tag)
1101    orderedTables.extend(tagList)
1102    return orderedTables
1103
1104
1105def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False):
1106    """Rewrite a font file, ordering the tables as recommended by the
1107    OpenType specification 1.4.
1108    """
1109    inFile.seek(0)
1110    outFile.seek(0)
1111    reader = SFNTReader(inFile, checkChecksums=checkChecksums)
1112    writer = SFNTWriter(
1113        outFile,
1114        len(reader.tables),
1115        reader.sfntVersion,
1116        reader.flavor,
1117        reader.flavorData,
1118    )
1119    tables = list(reader.keys())
1120    for tag in sortedTagList(tables, tableOrder):
1121        writer[tag] = reader[tag]
1122    writer.close()
1123
1124
1125def maxPowerOfTwo(x):
1126    """Return the highest exponent of two, so that
1127    (2 ** exponent) <= x.  Return 0 if x is 0.
1128    """
1129    exponent = 0
1130    while x:
1131        x = x >> 1
1132        exponent = exponent + 1
1133    return max(exponent - 1, 0)
1134
1135
1136def getSearchRange(n, itemSize=16):
1137    """Calculate searchRange, entrySelector, rangeShift."""
1138    # itemSize defaults to 16, for backward compatibility
1139    # with upstream fonttools.
1140    exponent = maxPowerOfTwo(n)
1141    searchRange = (2**exponent) * itemSize
1142    entrySelector = exponent
1143    rangeShift = max(0, n * itemSize - searchRange)
1144    return searchRange, entrySelector, rangeShift
1145