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