diff options
Diffstat (limited to 'Lib/fontTools/ttLib/ttFont.py')
-rw-r--r-- | Lib/fontTools/ttLib/ttFont.py | 2002 |
1 files changed, 1063 insertions, 939 deletions
diff --git a/Lib/fontTools/ttLib/ttFont.py b/Lib/fontTools/ttLib/ttFont.py index 327d113f..6a9ca098 100644 --- a/Lib/fontTools/ttLib/ttFont.py +++ b/Lib/fontTools/ttLib/ttFont.py @@ -4,793 +4,890 @@ from fontTools.misc.configTools import AbstractConfig from fontTools.misc.textTools import Tag, byteord, tostr from fontTools.misc.loggingTools import deprecateArgument from fontTools.ttLib import TTLibError -from fontTools.ttLib.ttGlyphSet import ( - _TTGlyphSet, _TTGlyph, - _TTGlyphCFF, _TTGlyphGlyf, - _TTVarGlyphSet, -) +from fontTools.ttLib.ttGlyphSet import _TTGlyph, _TTGlyphSetCFF, _TTGlyphSetGlyf from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter -from io import BytesIO, StringIO +from io import BytesIO, StringIO, UnsupportedOperation import os import logging import traceback log = logging.getLogger(__name__) + class TTFont(object): - """Represents a TrueType font. - - The object manages file input and output, and offers a convenient way of - accessing tables. Tables will be only decompiled when necessary, ie. when - they're actually accessed. This means that simple operations can be extremely fast. - - Example usage:: - - >> from fontTools import ttLib - >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file - >> tt['maxp'].numGlyphs - 242 - >> tt['OS/2'].achVendID - 'B&H\000' - >> tt['head'].unitsPerEm - 2048 - - For details of the objects returned when accessing each table, see :ref:`tables`. - To add a table to the font, use the :py:func:`newTable` function:: - - >> os2 = newTable("OS/2") - >> os2.version = 4 - >> # set other attributes - >> font["OS/2"] = os2 - - TrueType fonts can also be serialized to and from XML format (see also the - :ref:`ttx` binary):: - - >> tt.saveXML("afont.ttx") - Dumping 'LTSH' table... - Dumping 'OS/2' table... - [...] - - >> tt2 = ttLib.TTFont() # Create a new font object - >> tt2.importXML("afont.ttx") - >> tt2['maxp'].numGlyphs - 242 - - The TTFont object may be used as a context manager; this will cause the file - reader to be closed after the context ``with`` block is exited:: - - with TTFont(filename) as f: - # Do stuff - - Args: - file: When reading a font from disk, either a pathname pointing to a file, - or a readable file object. - res_name_or_index: If running on a Macintosh, either a sfnt resource name or - an sfnt resource index number. If the index number is zero, TTLib will - autodetect whether the file is a flat file or a suitcase. (If it is a suitcase, - only the first 'sfnt' resource will be read.) - sfntVersion (str): When constructing a font object from scratch, sets the four-byte - sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create - an OpenType file, use ``OTTO``. - flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2 - file. - checkChecksums (int): How checksum data should be treated. Default is 0 - (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to - raise an exception if any wrong checksums are found. - recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``, - ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save. - Also compiles the glyphs on importing, which saves memory consumption and - time. - ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation - will be ignored, and the binary data will be returned for those tables instead. - recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in - the ``head`` table on save. - fontNumber (int): The index of the font in a TrueType Collection file. - lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon - access only. If it is set to False, many data structures are loaded immediately. - The default is ``lazy=None`` which is somewhere in between. - """ - - def __init__(self, file=None, res_name_or_index=None, - sfntVersion="\000\001\000\000", flavor=None, checkChecksums=0, - verbose=None, recalcBBoxes=True, allowVID=NotImplemented, ignoreDecompileErrors=False, - recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None, - _tableCache=None, cfg={}): - for name in ("verbose", "quiet"): - val = locals().get(name) - if val is not None: - deprecateArgument(name, "configure logging instead") - setattr(self, name, val) - - self.lazy = lazy - self.recalcBBoxes = recalcBBoxes - self.recalcTimestamp = recalcTimestamp - self.tables = {} - self.reader = None - self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg) - self.ignoreDecompileErrors = ignoreDecompileErrors - - if not file: - self.sfntVersion = sfntVersion - self.flavor = flavor - self.flavorData = None - return - if not hasattr(file, "read"): - closeStream = True - # assume file is a string - if res_name_or_index is not None: - # see if it contains 'sfnt' resources in the resource or data fork - from . import macUtils - if res_name_or_index == 0: - if macUtils.getSFNTResIndices(file): - # get the first available sfnt font. - file = macUtils.SFNTResourceReader(file, 1) - else: - file = open(file, "rb") - else: - file = macUtils.SFNTResourceReader(file, res_name_or_index) - else: - file = open(file, "rb") - else: - # assume "file" is a readable file object - closeStream = False - file.seek(0) - - if not self.lazy: - # read input file in memory and wrap a stream around it to allow overwriting - file.seek(0) - tmp = BytesIO(file.read()) - if hasattr(file, 'name'): - # save reference to input file name - tmp.name = file.name - if closeStream: - file.close() - file = tmp - self._tableCache = _tableCache - self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) - self.sfntVersion = self.reader.sfntVersion - self.flavor = self.reader.flavor - self.flavorData = self.reader.flavorData - - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - - def close(self): - """If we still have a reader object, close it.""" - if self.reader is not None: - self.reader.close() - - def save(self, file, reorderTables=True): - """Save the font to disk. - - Args: - file: Similarly to the constructor, can be either a pathname or a writable - file object. - reorderTables (Option[bool]): If true (the default), reorder the tables, - sorting them by tag (recommended by the OpenType specification). If - false, retain the original font order. If None, reorder by table - dependency (fastest). - """ - if not hasattr(file, "write"): - if self.lazy and self.reader.file.name == file: - raise TTLibError( - "Can't overwrite TTFont when 'lazy' attribute is True") - createStream = True - else: - # assume "file" is a writable file object - createStream = False - - tmp = BytesIO() - - writer_reordersTables = self._save(tmp) - - if not (reorderTables is None or writer_reordersTables or - (reorderTables is False and self.reader is None)): - if reorderTables is False: - # sort tables using the original font's order - tableOrder = list(self.reader.keys()) - else: - # use the recommended order from the OpenType specification - tableOrder = None - tmp.flush() - tmp2 = BytesIO() - reorderFontTables(tmp, tmp2, tableOrder) - tmp.close() - tmp = tmp2 - - if createStream: - # "file" is a path - with open(file, "wb") as file: - file.write(tmp.getvalue()) - else: - file.write(tmp.getvalue()) - - tmp.close() - - def _save(self, file, tableCache=None): - """Internal function, to be shared by save() and TTCollection.save()""" - - if self.recalcTimestamp and 'head' in self: - self['head'] # make sure 'head' is loaded so the recalculation is actually done - - tags = list(self.keys()) - if "GlyphOrder" in tags: - tags.remove("GlyphOrder") - numTables = len(tags) - # write to a temporary stream to allow saving to unseekable streams - writer = SFNTWriter(file, numTables, self.sfntVersion, self.flavor, self.flavorData) - - done = [] - for tag in tags: - self._writeTable(tag, writer, done, tableCache) - - writer.close() - - return writer.reordersTables() - - def saveXML(self, fileOrPath, newlinestr="\n", **kwargs): - """Export the font as TTX (an XML-based text file), or as a series of text - files when splitTables is true. In the latter case, the 'fileOrPath' - argument should be a path to a directory. - The 'tables' argument must either be false (dump all tables) or a - list of tables to dump. The 'skipTables' argument may be a list of tables - to skip, but only when the 'tables' argument is false. - """ - - writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) - self._saveXML(writer, **kwargs) - writer.close() - - def _saveXML(self, writer, - writeVersion=True, - quiet=None, tables=None, skipTables=None, splitTables=False, - splitGlyphs=False, disassembleInstructions=True, - bitmapGlyphDataFormat='raw'): - - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - - self.disassembleInstructions = disassembleInstructions - self.bitmapGlyphDataFormat = bitmapGlyphDataFormat - if not tables: - tables = list(self.keys()) - if "GlyphOrder" not in tables: - tables = ["GlyphOrder"] + tables - if skipTables: - for tag in skipTables: - if tag in tables: - tables.remove(tag) - numTables = len(tables) - - if writeVersion: - from fontTools import version - version = ".".join(version.split('.')[:2]) - writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1], - ttLibVersion=version) - else: - writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) - writer.newline() - - # always splitTables if splitGlyphs is enabled - splitTables = splitTables or splitGlyphs - - if not splitTables: - writer.newline() - else: - path, ext = os.path.splitext(writer.filename) - fileNameTemplate = path + ".%s" + ext - - for i in range(numTables): - tag = tables[i] - if splitTables: - tablePath = fileNameTemplate % tagToIdentifier(tag) - tableWriter = xmlWriter.XMLWriter(tablePath, - newlinestr=writer.newlinestr) - tableWriter.begintag("ttFont", ttLibVersion=version) - tableWriter.newline() - tableWriter.newline() - writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) - writer.newline() - else: - tableWriter = writer - self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) - if splitTables: - tableWriter.endtag("ttFont") - tableWriter.newline() - tableWriter.close() - writer.endtag("ttFont") - writer.newline() - - def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - if tag in self: - table = self[tag] - report = "Dumping '%s' table..." % tag - else: - report = "No '%s' table found." % tag - log.info(report) - if tag not in self: - return - xmlTag = tagToXML(tag) - attrs = dict() - if hasattr(table, "ERROR"): - attrs['ERROR'] = "decompilation error" - from .tables.DefaultTable import DefaultTable - if table.__class__ == DefaultTable: - attrs['raw'] = True - writer.begintag(xmlTag, **attrs) - writer.newline() - if tag == "glyf": - table.toXML(writer, self, splitGlyphs=splitGlyphs) - else: - table.toXML(writer, self) - writer.endtag(xmlTag) - writer.newline() - writer.newline() - - def importXML(self, fileOrPath, quiet=None): - """Import a TTX file (an XML-based text format), so as to recreate - a font object. - """ - if quiet is not None: - deprecateArgument("quiet", "configure logging instead") - - if "maxp" in self and "post" in self: - # Make sure the glyph order is loaded, as it otherwise gets - # lost if the XML doesn't contain the glyph order, yet does - # contain the table which was originally used to extract the - # glyph names from (ie. 'post', 'cmap' or 'CFF '). - self.getGlyphOrder() - - from fontTools.misc import xmlReader - - reader = xmlReader.XMLReader(fileOrPath, self) - reader.read() - - def isLoaded(self, tag): - """Return true if the table identified by ``tag`` has been - decompiled and loaded into memory.""" - return tag in self.tables - - def has_key(self, tag): - """Test if the table identified by ``tag`` is present in the font. - - As well as this method, ``tag in font`` can also be used to determine the - presence of the table.""" - if self.isLoaded(tag): - return True - elif self.reader and tag in self.reader: - return True - elif tag == "GlyphOrder": - return True - else: - return False - - __contains__ = has_key - - def keys(self): - """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table.""" - keys = list(self.tables.keys()) - if self.reader: - for key in list(self.reader.keys()): - if key not in keys: - keys.append(key) - - if "GlyphOrder" in keys: - keys.remove("GlyphOrder") - keys = sortedTagList(keys) - return ["GlyphOrder"] + keys - - def ensureDecompiled(self, recurse=None): - """Decompile all the tables, even if a TTFont was opened in 'lazy' mode.""" - for tag in self.keys(): - table = self[tag] - if recurse is None: - recurse = self.lazy is not False - if recurse and hasattr(table, "ensureDecompiled"): - table.ensureDecompiled(recurse=recurse) - self.lazy = False - - def __len__(self): - return len(list(self.keys())) - - def __getitem__(self, tag): - tag = Tag(tag) - table = self.tables.get(tag) - if table is None: - if tag == "GlyphOrder": - table = GlyphOrder(tag) - self.tables[tag] = table - elif self.reader is not None: - table = self._readTable(tag) - else: - raise KeyError("'%s' table not found" % tag) - return table - - def _readTable(self, tag): - log.debug("Reading '%s' table from disk", tag) - data = self.reader[tag] - if self._tableCache is not None: - table = self._tableCache.get((tag, data)) - if table is not None: - return table - tableClass = getTableClass(tag) - table = tableClass(tag) - self.tables[tag] = table - log.debug("Decompiling '%s' table", tag) - try: - table.decompile(data, self) - except Exception: - if not self.ignoreDecompileErrors: - raise - # fall back to DefaultTable, retaining the binary table data - log.exception( - "An exception occurred during the decompilation of the '%s' table", tag) - from .tables.DefaultTable import DefaultTable - file = StringIO() - traceback.print_exc(file=file) - table = DefaultTable(tag) - table.ERROR = file.getvalue() - self.tables[tag] = table - table.decompile(data, self) - if self._tableCache is not None: - self._tableCache[(tag, data)] = table - return table - - def __setitem__(self, tag, table): - self.tables[Tag(tag)] = table - - def __delitem__(self, tag): - if tag not in self: - raise KeyError("'%s' table not found" % tag) - if tag in self.tables: - del self.tables[tag] - if self.reader and tag in self.reader: - del self.reader[tag] - - def get(self, tag, default=None): - """Returns the table if it exists or (optionally) a default if it doesn't.""" - try: - return self[tag] - except KeyError: - return default - - def setGlyphOrder(self, glyphOrder): - """Set the glyph order - - Args: - glyphOrder ([str]): List of glyph names in order. - """ - self.glyphOrder = glyphOrder - if hasattr(self, '_reverseGlyphOrderDict'): - del self._reverseGlyphOrderDict - if self.isLoaded("glyf"): - self["glyf"].setGlyphOrder(glyphOrder) - - def getGlyphOrder(self): - """Returns a list of glyph names ordered by their position in the font.""" - try: - return self.glyphOrder - except AttributeError: - pass - if 'CFF ' in self: - cff = self['CFF '] - self.glyphOrder = cff.getGlyphOrder() - elif 'post' in self: - # TrueType font - glyphOrder = self['post'].getGlyphOrder() - if glyphOrder is None: - # - # No names found in the 'post' table. - # Try to create glyph names from the unicode cmap (if available) - # in combination with the Adobe Glyph List (AGL). - # - self._getGlyphNamesFromCmap() - else: - self.glyphOrder = glyphOrder - else: - self._getGlyphNamesFromCmap() - return self.glyphOrder - - def _getGlyphNamesFromCmap(self): - # - # This is rather convoluted, but then again, it's an interesting problem: - # - we need to use the unicode values found in the cmap table to - # build glyph names (eg. because there is only a minimal post table, - # or none at all). - # - but the cmap parser also needs glyph names to work with... - # So here's what we do: - # - make up glyph names based on glyphID - # - load a temporary cmap table based on those names - # - extract the unicode values, build the "real" glyph names - # - unload the temporary cmap table - # - if self.isLoaded("cmap"): - # Bootstrapping: we're getting called by the cmap parser - # itself. This means self.tables['cmap'] contains a partially - # loaded cmap, making it impossible to get at a unicode - # subtable here. We remove the partially loaded cmap and - # restore it later. - # This only happens if the cmap table is loaded before any - # other table that does f.getGlyphOrder() or f.getGlyphName(). - cmapLoading = self.tables['cmap'] - del self.tables['cmap'] - else: - cmapLoading = None - # Make up glyph names based on glyphID, which will be used by the - # temporary cmap and by the real cmap in case we don't find a unicode - # cmap. - numGlyphs = int(self['maxp'].numGlyphs) - glyphOrder = [None] * numGlyphs - glyphOrder[0] = ".notdef" - for i in range(1, numGlyphs): - glyphOrder[i] = "glyph%.5d" % i - # Set the glyph order, so the cmap parser has something - # to work with (so we don't get called recursively). - self.glyphOrder = glyphOrder - - # Make up glyph names based on the reversed cmap table. Because some - # glyphs (eg. ligatures or alternates) may not be reachable via cmap, - # this naming table will usually not cover all glyphs in the font. - # If the font has no Unicode cmap table, reversecmap will be empty. - if 'cmap' in self: - reversecmap = self['cmap'].buildReversed() - else: - reversecmap = {} - useCount = {} - for i in range(numGlyphs): - tempName = glyphOrder[i] - if tempName in reversecmap: - # If a font maps both U+0041 LATIN CAPITAL LETTER A and - # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, - # we prefer naming the glyph as "A". - glyphName = self._makeGlyphName(min(reversecmap[tempName])) - numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 - if numUses > 1: - glyphName = "%s.alt%d" % (glyphName, numUses - 1) - glyphOrder[i] = glyphName - - if 'cmap' in self: - # Delete the temporary cmap table from the cache, so it can - # be parsed again with the right names. - del self.tables['cmap'] - self.glyphOrder = glyphOrder - if cmapLoading: - # restore partially loaded cmap, so it can continue loading - # using the proper names. - self.tables['cmap'] = cmapLoading - - @staticmethod - def _makeGlyphName(codepoint): - from fontTools import agl # Adobe Glyph List - if codepoint in agl.UV2AGL: - return agl.UV2AGL[codepoint] - elif codepoint <= 0xFFFF: - return "uni%04X" % codepoint - else: - return "u%X" % codepoint - - def getGlyphNames(self): - """Get a list of glyph names, sorted alphabetically.""" - glyphNames = sorted(self.getGlyphOrder()) - return glyphNames - - def getGlyphNames2(self): - """Get a list of glyph names, sorted alphabetically, - but not case sensitive. - """ - from fontTools.misc import textTools - return textTools.caselessSort(self.getGlyphOrder()) - - def getGlyphName(self, glyphID): - """Returns the name for the glyph with the given ID. - - If no name is available, synthesises one with the form ``glyphXXXXX``` where - ```XXXXX`` is the zero-padded glyph ID. - """ - try: - return self.getGlyphOrder()[glyphID] - except IndexError: - return "glyph%.5d" % glyphID - - def getGlyphNameMany(self, lst): - """Converts a list of glyph IDs into a list of glyph names.""" - glyphOrder = self.getGlyphOrder(); - cnt = len(glyphOrder) - return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid - for gid in lst] - - def getGlyphID(self, glyphName): - """Returns the ID of the glyph with the given name.""" - try: - return self.getReverseGlyphMap()[glyphName] - except KeyError: - if glyphName[:5] == "glyph": - try: - return int(glyphName[5:]) - except (NameError, ValueError): - raise KeyError(glyphName) - - def getGlyphIDMany(self, lst): - """Converts a list of glyph names into a list of glyph IDs.""" - d = self.getReverseGlyphMap() - try: - return [d[glyphName] for glyphName in lst] - except KeyError: - getGlyphID = self.getGlyphID - return [getGlyphID(glyphName) for glyphName in lst] - - def getReverseGlyphMap(self, rebuild=False): - """Returns a mapping of glyph names to glyph IDs.""" - if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): - self._buildReverseGlyphOrderDict() - return self._reverseGlyphOrderDict - - def _buildReverseGlyphOrderDict(self): - self._reverseGlyphOrderDict = d = {} - for glyphID,glyphName in enumerate(self.getGlyphOrder()): - d[glyphName] = glyphID - return d - - def _writeTable(self, tag, writer, done, tableCache=None): - """Internal helper function for self.save(). Keeps track of - inter-table dependencies. - """ - if tag in done: - return - tableClass = getTableClass(tag) - for masterTable in tableClass.dependencies: - if masterTable not in done: - if masterTable in self: - self._writeTable(masterTable, writer, done, tableCache) - else: - done.append(masterTable) - done.append(tag) - tabledata = self.getTableData(tag) - if tableCache is not None: - entry = tableCache.get((Tag(tag), tabledata)) - if entry is not None: - log.debug("reusing '%s' table", tag) - writer.setEntry(tag, entry) - return - log.debug("Writing '%s' table to disk", tag) - writer[tag] = tabledata - if tableCache is not None: - tableCache[(Tag(tag), tabledata)] = writer[tag] - - def getTableData(self, tag): - """Returns the binary representation of a table. - - If the table is currently loaded and in memory, the data is compiled to - binary and returned; if it is not currently loaded, the binary data is - read from the font file and returned. - """ - tag = Tag(tag) - if self.isLoaded(tag): - log.debug("Compiling '%s' table", tag) - return self.tables[tag].compile(self) - elif self.reader and tag in self.reader: - log.debug("Reading '%s' table from disk", tag) - return self.reader[tag] - else: - raise KeyError(tag) - - def getGlyphSet(self, preferCFF=True, location=None, normalized=False): - """Return a generic GlyphSet, which is a dict-like object - mapping glyph names to glyph objects. The returned glyph objects - have a .draw() method that supports the Pen protocol, and will - have an attribute named 'width'. - - If the font is CFF-based, the outlines will be taken from the 'CFF ' or - 'CFF2' tables. Otherwise the outlines will be taken from the 'glyf' table. - If the font contains both a 'CFF '/'CFF2' and a 'glyf' table, you can use - the 'preferCFF' argument to specify which one should be taken. If the - font contains both a 'CFF ' and a 'CFF2' table, the latter is taken. - - If the 'location' parameter is set, it should be a dictionary mapping - four-letter variation tags to their float values, and the returned - glyph-set will represent an instance of a variable font at that location. - If the 'normalized' variable is set to True, that location is interpretted - as in the normalized (-1..+1) space, otherwise it is in the font's defined - axes space. - """ - glyphs = None - if (preferCFF and any(tb in self for tb in ["CFF ", "CFF2"]) or - ("glyf" not in self and any(tb in self for tb in ["CFF ", "CFF2"]))): - table_tag = "CFF2" if "CFF2" in self else "CFF " - if location: - raise NotImplementedError # TODO - glyphs = _TTGlyphSet(self, - list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF) - - if glyphs is None and "glyf" in self: - if location and 'gvar' in self: - glyphs = _TTVarGlyphSet(self, location=location, normalized=normalized) - else: - glyphs = _TTGlyphSet(self, self["glyf"], _TTGlyphGlyf) - - if glyphs is None: - raise TTLibError("Font contains no outlines") - - return glyphs - - def getBestCmap(self, cmapPreferences=((3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0))): - """Returns the 'best' Unicode cmap dictionary available in the font - or ``None``, if no Unicode cmap subtable is available. - - By default it will search for the following (platformID, platEncID) - pairs in order:: - - (3, 10), # Windows Unicode full repertoire - (0, 6), # Unicode full repertoire (format 13 subtable) - (0, 4), # Unicode 2.0 full repertoire - (3, 1), # Windows Unicode BMP - (0, 3), # Unicode 2.0 BMP - (0, 2), # Unicode ISO/IEC 10646 - (0, 1), # Unicode 1.1 - (0, 0) # Unicode 1.0 - - This particular order matches what HarfBuzz uses to choose what - subtable to use by default. This order prefers the largest-repertoire - subtable, and among those, prefers the Windows-platform over the - Unicode-platform as the former has wider support. - - This order can be customized via the ``cmapPreferences`` argument. - """ - return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) + """Represents a TrueType font. + + The object manages file input and output, and offers a convenient way of + accessing tables. Tables will be only decompiled when necessary, ie. when + they're actually accessed. This means that simple operations can be extremely fast. + + Example usage:: + + >> from fontTools import ttLib + >> tt = ttLib.TTFont("afont.ttf") # Load an existing font file + >> tt['maxp'].numGlyphs + 242 + >> tt['OS/2'].achVendID + 'B&H\000' + >> tt['head'].unitsPerEm + 2048 + + For details of the objects returned when accessing each table, see :ref:`tables`. + To add a table to the font, use the :py:func:`newTable` function:: + + >> os2 = newTable("OS/2") + >> os2.version = 4 + >> # set other attributes + >> font["OS/2"] = os2 + + TrueType fonts can also be serialized to and from XML format (see also the + :ref:`ttx` binary):: + + >> tt.saveXML("afont.ttx") + Dumping 'LTSH' table... + Dumping 'OS/2' table... + [...] + + >> tt2 = ttLib.TTFont() # Create a new font object + >> tt2.importXML("afont.ttx") + >> tt2['maxp'].numGlyphs + 242 + + The TTFont object may be used as a context manager; this will cause the file + reader to be closed after the context ``with`` block is exited:: + + with TTFont(filename) as f: + # Do stuff + + Args: + file: When reading a font from disk, either a pathname pointing to a file, + or a readable file object. + res_name_or_index: If running on a Macintosh, either a sfnt resource name or + an sfnt resource index number. If the index number is zero, TTLib will + autodetect whether the file is a flat file or a suitcase. (If it is a suitcase, + only the first 'sfnt' resource will be read.) + sfntVersion (str): When constructing a font object from scratch, sets the four-byte + sfnt magic number to be used. Defaults to ``\0\1\0\0`` (TrueType). To create + an OpenType file, use ``OTTO``. + flavor (str): Set this to ``woff`` when creating a WOFF file or ``woff2`` for a WOFF2 + file. + checkChecksums (int): How checksum data should be treated. Default is 0 + (no checking). Set to 1 to check and warn on wrong checksums; set to 2 to + raise an exception if any wrong checksums are found. + recalcBBoxes (bool): If true (the default), recalculates ``glyf``, ``CFF ``, + ``head`` bounding box values and ``hhea``/``vhea`` min/max values on save. + Also compiles the glyphs on importing, which saves memory consumption and + time. + ignoreDecompileErrors (bool): If true, exceptions raised during table decompilation + will be ignored, and the binary data will be returned for those tables instead. + recalcTimestamp (bool): If true (the default), sets the ``modified`` timestamp in + the ``head`` table on save. + fontNumber (int): The index of the font in a TrueType Collection file. + lazy (bool): If lazy is set to True, many data structures are loaded lazily, upon + access only. If it is set to False, many data structures are loaded immediately. + The default is ``lazy=None`` which is somewhere in between. + """ + + def __init__( + self, + file=None, + res_name_or_index=None, + sfntVersion="\000\001\000\000", + flavor=None, + checkChecksums=0, + verbose=None, + recalcBBoxes=True, + allowVID=NotImplemented, + ignoreDecompileErrors=False, + recalcTimestamp=True, + fontNumber=-1, + lazy=None, + quiet=None, + _tableCache=None, + cfg={}, + ): + for name in ("verbose", "quiet"): + val = locals().get(name) + if val is not None: + deprecateArgument(name, "configure logging instead") + setattr(self, name, val) + + self.lazy = lazy + self.recalcBBoxes = recalcBBoxes + self.recalcTimestamp = recalcTimestamp + self.tables = {} + self.reader = None + self.cfg = cfg.copy() if isinstance(cfg, AbstractConfig) else Config(cfg) + self.ignoreDecompileErrors = ignoreDecompileErrors + + if not file: + self.sfntVersion = sfntVersion + self.flavor = flavor + self.flavorData = None + return + seekable = True + if not hasattr(file, "read"): + closeStream = True + # assume file is a string + if res_name_or_index is not None: + # see if it contains 'sfnt' resources in the resource or data fork + from . import macUtils + + if res_name_or_index == 0: + if macUtils.getSFNTResIndices(file): + # get the first available sfnt font. + file = macUtils.SFNTResourceReader(file, 1) + else: + file = open(file, "rb") + else: + file = macUtils.SFNTResourceReader(file, res_name_or_index) + else: + file = open(file, "rb") + else: + # assume "file" is a readable file object + closeStream = False + # SFNTReader wants the input file to be seekable. + # SpooledTemporaryFile has no seekable() on < 3.11, but still can seek: + # https://github.com/fonttools/fonttools/issues/3052 + if hasattr(file, "seekable"): + seekable = file.seekable() + elif hasattr(file, "seek"): + try: + file.seek(0) + except UnsupportedOperation: + seekable = False + + if not self.lazy: + # read input file in memory and wrap a stream around it to allow overwriting + if seekable: + file.seek(0) + tmp = BytesIO(file.read()) + if hasattr(file, "name"): + # save reference to input file name + tmp.name = file.name + if closeStream: + file.close() + file = tmp + elif not seekable: + raise TTLibError("Input file must be seekable when lazy=True") + self._tableCache = _tableCache + self.reader = SFNTReader(file, checkChecksums, fontNumber=fontNumber) + self.sfntVersion = self.reader.sfntVersion + self.flavor = self.reader.flavor + self.flavorData = self.reader.flavorData + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + def close(self): + """If we still have a reader object, close it.""" + if self.reader is not None: + self.reader.close() + + def save(self, file, reorderTables=True): + """Save the font to disk. + + Args: + file: Similarly to the constructor, can be either a pathname or a writable + file object. + reorderTables (Option[bool]): If true (the default), reorder the tables, + sorting them by tag (recommended by the OpenType specification). If + false, retain the original font order. If None, reorder by table + dependency (fastest). + """ + if not hasattr(file, "write"): + if self.lazy and self.reader.file.name == file: + raise TTLibError("Can't overwrite TTFont when 'lazy' attribute is True") + createStream = True + else: + # assume "file" is a writable file object + createStream = False + + tmp = BytesIO() + + writer_reordersTables = self._save(tmp) + + if not ( + reorderTables is None + or writer_reordersTables + or (reorderTables is False and self.reader is None) + ): + if reorderTables is False: + # sort tables using the original font's order + tableOrder = list(self.reader.keys()) + else: + # use the recommended order from the OpenType specification + tableOrder = None + tmp.flush() + tmp2 = BytesIO() + reorderFontTables(tmp, tmp2, tableOrder) + tmp.close() + tmp = tmp2 + + if createStream: + # "file" is a path + with open(file, "wb") as file: + file.write(tmp.getvalue()) + else: + file.write(tmp.getvalue()) + + tmp.close() + + def _save(self, file, tableCache=None): + """Internal function, to be shared by save() and TTCollection.save()""" + + if self.recalcTimestamp and "head" in self: + self[ + "head" + ] # make sure 'head' is loaded so the recalculation is actually done + + tags = list(self.keys()) + if "GlyphOrder" in tags: + tags.remove("GlyphOrder") + numTables = len(tags) + # write to a temporary stream to allow saving to unseekable streams + writer = SFNTWriter( + file, numTables, self.sfntVersion, self.flavor, self.flavorData + ) + + done = [] + for tag in tags: + self._writeTable(tag, writer, done, tableCache) + + writer.close() + + return writer.reordersTables() + + def saveXML(self, fileOrPath, newlinestr="\n", **kwargs): + """Export the font as TTX (an XML-based text file), or as a series of text + files when splitTables is true. In the latter case, the 'fileOrPath' + argument should be a path to a directory. + The 'tables' argument must either be false (dump all tables) or a + list of tables to dump. The 'skipTables' argument may be a list of tables + to skip, but only when the 'tables' argument is false. + """ + + writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr) + self._saveXML(writer, **kwargs) + writer.close() + + def _saveXML( + self, + writer, + writeVersion=True, + quiet=None, + tables=None, + skipTables=None, + splitTables=False, + splitGlyphs=False, + disassembleInstructions=True, + bitmapGlyphDataFormat="raw", + ): + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + self.disassembleInstructions = disassembleInstructions + self.bitmapGlyphDataFormat = bitmapGlyphDataFormat + if not tables: + tables = list(self.keys()) + if "GlyphOrder" not in tables: + tables = ["GlyphOrder"] + tables + if skipTables: + for tag in skipTables: + if tag in tables: + tables.remove(tag) + numTables = len(tables) + + if writeVersion: + from fontTools import version + + version = ".".join(version.split(".")[:2]) + writer.begintag( + "ttFont", + sfntVersion=repr(tostr(self.sfntVersion))[1:-1], + ttLibVersion=version, + ) + else: + writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1]) + writer.newline() + + # always splitTables if splitGlyphs is enabled + splitTables = splitTables or splitGlyphs + + if not splitTables: + writer.newline() + else: + path, ext = os.path.splitext(writer.filename) + + for i in range(numTables): + tag = tables[i] + if splitTables: + tablePath = path + "." + tagToIdentifier(tag) + ext + tableWriter = xmlWriter.XMLWriter( + tablePath, newlinestr=writer.newlinestr + ) + tableWriter.begintag("ttFont", ttLibVersion=version) + tableWriter.newline() + tableWriter.newline() + writer.simpletag(tagToXML(tag), src=os.path.basename(tablePath)) + writer.newline() + else: + tableWriter = writer + self._tableToXML(tableWriter, tag, splitGlyphs=splitGlyphs) + if splitTables: + tableWriter.endtag("ttFont") + tableWriter.newline() + tableWriter.close() + writer.endtag("ttFont") + writer.newline() + + def _tableToXML(self, writer, tag, quiet=None, splitGlyphs=False): + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + if tag in self: + table = self[tag] + report = "Dumping '%s' table..." % tag + else: + report = "No '%s' table found." % tag + log.info(report) + if tag not in self: + return + xmlTag = tagToXML(tag) + attrs = dict() + if hasattr(table, "ERROR"): + attrs["ERROR"] = "decompilation error" + from .tables.DefaultTable import DefaultTable + + if table.__class__ == DefaultTable: + attrs["raw"] = True + writer.begintag(xmlTag, **attrs) + writer.newline() + if tag == "glyf": + table.toXML(writer, self, splitGlyphs=splitGlyphs) + else: + table.toXML(writer, self) + writer.endtag(xmlTag) + writer.newline() + writer.newline() + + def importXML(self, fileOrPath, quiet=None): + """Import a TTX file (an XML-based text format), so as to recreate + a font object. + """ + if quiet is not None: + deprecateArgument("quiet", "configure logging instead") + + if "maxp" in self and "post" in self: + # Make sure the glyph order is loaded, as it otherwise gets + # lost if the XML doesn't contain the glyph order, yet does + # contain the table which was originally used to extract the + # glyph names from (ie. 'post', 'cmap' or 'CFF '). + self.getGlyphOrder() + + from fontTools.misc import xmlReader + + reader = xmlReader.XMLReader(fileOrPath, self) + reader.read() + + def isLoaded(self, tag): + """Return true if the table identified by ``tag`` has been + decompiled and loaded into memory.""" + return tag in self.tables + + def has_key(self, tag): + """Test if the table identified by ``tag`` is present in the font. + + As well as this method, ``tag in font`` can also be used to determine the + presence of the table.""" + if self.isLoaded(tag): + return True + elif self.reader and tag in self.reader: + return True + elif tag == "GlyphOrder": + return True + else: + return False + + __contains__ = has_key + + def keys(self): + """Returns the list of tables in the font, along with the ``GlyphOrder`` pseudo-table.""" + keys = list(self.tables.keys()) + if self.reader: + for key in list(self.reader.keys()): + if key not in keys: + keys.append(key) + + if "GlyphOrder" in keys: + keys.remove("GlyphOrder") + keys = sortedTagList(keys) + return ["GlyphOrder"] + keys + + def ensureDecompiled(self, recurse=None): + """Decompile all the tables, even if a TTFont was opened in 'lazy' mode.""" + for tag in self.keys(): + table = self[tag] + if recurse is None: + recurse = self.lazy is not False + if recurse and hasattr(table, "ensureDecompiled"): + table.ensureDecompiled(recurse=recurse) + self.lazy = False + + def __len__(self): + return len(list(self.keys())) + + def __getitem__(self, tag): + tag = Tag(tag) + table = self.tables.get(tag) + if table is None: + if tag == "GlyphOrder": + table = GlyphOrder(tag) + self.tables[tag] = table + elif self.reader is not None: + table = self._readTable(tag) + else: + raise KeyError("'%s' table not found" % tag) + return table + + def _readTable(self, tag): + log.debug("Reading '%s' table from disk", tag) + data = self.reader[tag] + if self._tableCache is not None: + table = self._tableCache.get((tag, data)) + if table is not None: + return table + tableClass = getTableClass(tag) + table = tableClass(tag) + self.tables[tag] = table + log.debug("Decompiling '%s' table", tag) + try: + table.decompile(data, self) + except Exception: + if not self.ignoreDecompileErrors: + raise + # fall back to DefaultTable, retaining the binary table data + log.exception( + "An exception occurred during the decompilation of the '%s' table", tag + ) + from .tables.DefaultTable import DefaultTable + + file = StringIO() + traceback.print_exc(file=file) + table = DefaultTable(tag) + table.ERROR = file.getvalue() + self.tables[tag] = table + table.decompile(data, self) + if self._tableCache is not None: + self._tableCache[(tag, data)] = table + return table + + def __setitem__(self, tag, table): + self.tables[Tag(tag)] = table + + def __delitem__(self, tag): + if tag not in self: + raise KeyError("'%s' table not found" % tag) + if tag in self.tables: + del self.tables[tag] + if self.reader and tag in self.reader: + del self.reader[tag] + + def get(self, tag, default=None): + """Returns the table if it exists or (optionally) a default if it doesn't.""" + try: + return self[tag] + except KeyError: + return default + + def setGlyphOrder(self, glyphOrder): + """Set the glyph order + + Args: + glyphOrder ([str]): List of glyph names in order. + """ + self.glyphOrder = glyphOrder + if hasattr(self, "_reverseGlyphOrderDict"): + del self._reverseGlyphOrderDict + if self.isLoaded("glyf"): + self["glyf"].setGlyphOrder(glyphOrder) + + def getGlyphOrder(self): + """Returns a list of glyph names ordered by their position in the font.""" + try: + return self.glyphOrder + except AttributeError: + pass + if "CFF " in self: + cff = self["CFF "] + self.glyphOrder = cff.getGlyphOrder() + elif "post" in self: + # TrueType font + glyphOrder = self["post"].getGlyphOrder() + if glyphOrder is None: + # + # No names found in the 'post' table. + # Try to create glyph names from the unicode cmap (if available) + # in combination with the Adobe Glyph List (AGL). + # + self._getGlyphNamesFromCmap() + elif len(glyphOrder) < self["maxp"].numGlyphs: + # + # Not enough names found in the 'post' table. + # Can happen when 'post' format 1 is improperly used on a font that + # has more than 258 glyphs (the lenght of 'standardGlyphOrder'). + # + log.warning( + "Not enough names found in the 'post' table, generating them from cmap instead" + ) + self._getGlyphNamesFromCmap() + else: + self.glyphOrder = glyphOrder + else: + self._getGlyphNamesFromCmap() + return self.glyphOrder + + def _getGlyphNamesFromCmap(self): + # + # This is rather convoluted, but then again, it's an interesting problem: + # - we need to use the unicode values found in the cmap table to + # build glyph names (eg. because there is only a minimal post table, + # or none at all). + # - but the cmap parser also needs glyph names to work with... + # So here's what we do: + # - make up glyph names based on glyphID + # - load a temporary cmap table based on those names + # - extract the unicode values, build the "real" glyph names + # - unload the temporary cmap table + # + if self.isLoaded("cmap"): + # Bootstrapping: we're getting called by the cmap parser + # itself. This means self.tables['cmap'] contains a partially + # loaded cmap, making it impossible to get at a unicode + # subtable here. We remove the partially loaded cmap and + # restore it later. + # This only happens if the cmap table is loaded before any + # other table that does f.getGlyphOrder() or f.getGlyphName(). + cmapLoading = self.tables["cmap"] + del self.tables["cmap"] + else: + cmapLoading = None + # Make up glyph names based on glyphID, which will be used by the + # temporary cmap and by the real cmap in case we don't find a unicode + # cmap. + numGlyphs = int(self["maxp"].numGlyphs) + glyphOrder = [None] * numGlyphs + glyphOrder[0] = ".notdef" + for i in range(1, numGlyphs): + glyphOrder[i] = "glyph%.5d" % i + # Set the glyph order, so the cmap parser has something + # to work with (so we don't get called recursively). + self.glyphOrder = glyphOrder + + # Make up glyph names based on the reversed cmap table. Because some + # glyphs (eg. ligatures or alternates) may not be reachable via cmap, + # this naming table will usually not cover all glyphs in the font. + # If the font has no Unicode cmap table, reversecmap will be empty. + if "cmap" in self: + reversecmap = self["cmap"].buildReversed() + else: + reversecmap = {} + useCount = {} + for i in range(numGlyphs): + tempName = glyphOrder[i] + if tempName in reversecmap: + # If a font maps both U+0041 LATIN CAPITAL LETTER A and + # U+0391 GREEK CAPITAL LETTER ALPHA to the same glyph, + # we prefer naming the glyph as "A". + glyphName = self._makeGlyphName(min(reversecmap[tempName])) + numUses = useCount[glyphName] = useCount.get(glyphName, 0) + 1 + if numUses > 1: + glyphName = "%s.alt%d" % (glyphName, numUses - 1) + glyphOrder[i] = glyphName + + if "cmap" in self: + # Delete the temporary cmap table from the cache, so it can + # be parsed again with the right names. + del self.tables["cmap"] + self.glyphOrder = glyphOrder + if cmapLoading: + # restore partially loaded cmap, so it can continue loading + # using the proper names. + self.tables["cmap"] = cmapLoading + + @staticmethod + def _makeGlyphName(codepoint): + from fontTools import agl # Adobe Glyph List + + if codepoint in agl.UV2AGL: + return agl.UV2AGL[codepoint] + elif codepoint <= 0xFFFF: + return "uni%04X" % codepoint + else: + return "u%X" % codepoint + + def getGlyphNames(self): + """Get a list of glyph names, sorted alphabetically.""" + glyphNames = sorted(self.getGlyphOrder()) + return glyphNames + + def getGlyphNames2(self): + """Get a list of glyph names, sorted alphabetically, + but not case sensitive. + """ + from fontTools.misc import textTools + + return textTools.caselessSort(self.getGlyphOrder()) + + def getGlyphName(self, glyphID): + """Returns the name for the glyph with the given ID. + + If no name is available, synthesises one with the form ``glyphXXXXX``` where + ```XXXXX`` is the zero-padded glyph ID. + """ + try: + return self.getGlyphOrder()[glyphID] + except IndexError: + return "glyph%.5d" % glyphID + + def getGlyphNameMany(self, lst): + """Converts a list of glyph IDs into a list of glyph names.""" + glyphOrder = self.getGlyphOrder() + cnt = len(glyphOrder) + return [glyphOrder[gid] if gid < cnt else "glyph%.5d" % gid for gid in lst] + + def getGlyphID(self, glyphName): + """Returns the ID of the glyph with the given name.""" + try: + return self.getReverseGlyphMap()[glyphName] + except KeyError: + if glyphName[:5] == "glyph": + try: + return int(glyphName[5:]) + except (NameError, ValueError): + raise KeyError(glyphName) + raise + + def getGlyphIDMany(self, lst): + """Converts a list of glyph names into a list of glyph IDs.""" + d = self.getReverseGlyphMap() + try: + return [d[glyphName] for glyphName in lst] + except KeyError: + getGlyphID = self.getGlyphID + return [getGlyphID(glyphName) for glyphName in lst] + + def getReverseGlyphMap(self, rebuild=False): + """Returns a mapping of glyph names to glyph IDs.""" + if rebuild or not hasattr(self, "_reverseGlyphOrderDict"): + self._buildReverseGlyphOrderDict() + return self._reverseGlyphOrderDict + + def _buildReverseGlyphOrderDict(self): + self._reverseGlyphOrderDict = d = {} + for glyphID, glyphName in enumerate(self.getGlyphOrder()): + d[glyphName] = glyphID + return d + + def _writeTable(self, tag, writer, done, tableCache=None): + """Internal helper function for self.save(). Keeps track of + inter-table dependencies. + """ + if tag in done: + return + tableClass = getTableClass(tag) + for masterTable in tableClass.dependencies: + if masterTable not in done: + if masterTable in self: + self._writeTable(masterTable, writer, done, tableCache) + else: + done.append(masterTable) + done.append(tag) + tabledata = self.getTableData(tag) + if tableCache is not None: + entry = tableCache.get((Tag(tag), tabledata)) + if entry is not None: + log.debug("reusing '%s' table", tag) + writer.setEntry(tag, entry) + return + log.debug("Writing '%s' table to disk", tag) + writer[tag] = tabledata + if tableCache is not None: + tableCache[(Tag(tag), tabledata)] = writer[tag] + + def getTableData(self, tag): + """Returns the binary representation of a table. + + If the table is currently loaded and in memory, the data is compiled to + binary and returned; if it is not currently loaded, the binary data is + read from the font file and returned. + """ + tag = Tag(tag) + if self.isLoaded(tag): + log.debug("Compiling '%s' table", tag) + return self.tables[tag].compile(self) + elif self.reader and tag in self.reader: + log.debug("Reading '%s' table from disk", tag) + return self.reader[tag] + else: + raise KeyError(tag) + + def getGlyphSet(self, preferCFF=True, location=None, normalized=False): + """Return a generic GlyphSet, which is a dict-like object + mapping glyph names to glyph objects. The returned glyph objects + have a ``.draw()`` method that supports the Pen protocol, and will + have an attribute named 'width'. + + If the font is CFF-based, the outlines will be taken from the ``CFF `` + or ``CFF2`` tables. Otherwise the outlines will be taken from the + ``glyf`` table. + + If the font contains both a ``CFF ``/``CFF2`` and a ``glyf`` table, you + can use the ``preferCFF`` argument to specify which one should be taken. + If the font contains both a ``CFF `` and a ``CFF2`` table, the latter is + taken. + + If the ``location`` parameter is set, it should be a dictionary mapping + four-letter variation tags to their float values, and the returned + glyph-set will represent an instance of a variable font at that + location. + + If the ``normalized`` variable is set to True, that location is + interpreted as in the normalized (-1..+1) space, otherwise it is in the + font's defined axes space. + """ + if location and "fvar" not in self: + location = None + if location and not normalized: + location = self.normalizeLocation(location) + if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self): + return _TTGlyphSetCFF(self, location) + elif "glyf" in self: + return _TTGlyphSetGlyf(self, location) + else: + raise TTLibError("Font contains no outlines") + + def normalizeLocation(self, location): + """Normalize a ``location`` from the font's defined axes space (also + known as user space) into the normalized (-1..+1) space. It applies + ``avar`` mapping if the font contains an ``avar`` table. + + The ``location`` parameter should be a dictionary mapping four-letter + variation tags to their float values. + + Raises ``TTLibError`` if the font is not a variable font. + """ + from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap + + if "fvar" not in self: + raise TTLibError("Not a variable font") + + axes = { + a.axisTag: (a.minValue, a.defaultValue, a.maxValue) + for a in self["fvar"].axes + } + location = normalizeLocation(location, axes) + if "avar" in self: + avar = self["avar"] + avarSegments = avar.segments + mappedLocation = {} + for axisTag, value in location.items(): + avarMapping = avarSegments.get(axisTag, None) + if avarMapping is not None: + value = piecewiseLinearMap(value, avarMapping) + mappedLocation[axisTag] = value + location = mappedLocation + return location + + def getBestCmap( + self, + cmapPreferences=( + (3, 10), + (0, 6), + (0, 4), + (3, 1), + (0, 3), + (0, 2), + (0, 1), + (0, 0), + ), + ): + """Returns the 'best' Unicode cmap dictionary available in the font + or ``None``, if no Unicode cmap subtable is available. + + By default it will search for the following (platformID, platEncID) + pairs in order:: + + (3, 10), # Windows Unicode full repertoire + (0, 6), # Unicode full repertoire (format 13 subtable) + (0, 4), # Unicode 2.0 full repertoire + (3, 1), # Windows Unicode BMP + (0, 3), # Unicode 2.0 BMP + (0, 2), # Unicode ISO/IEC 10646 + (0, 1), # Unicode 1.1 + (0, 0) # Unicode 1.0 + + This particular order matches what HarfBuzz uses to choose what + subtable to use by default. This order prefers the largest-repertoire + subtable, and among those, prefers the Windows-platform over the + Unicode-platform as the former has wider support. + + This order can be customized via the ``cmapPreferences`` argument. + """ + return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences) class GlyphOrder(object): - """A pseudo table. The glyph order isn't in the font as a separate - table, but it's nice to present it as such in the TTX format. - """ + """A pseudo table. The glyph order isn't in the font as a separate + table, but it's nice to present it as such in the TTX format. + """ - def __init__(self, tag=None): - pass + def __init__(self, tag=None): + pass - def toXML(self, writer, ttFont): - glyphOrder = ttFont.getGlyphOrder() - writer.comment("The 'id' attribute is only for humans; " - "it is ignored when parsed.") - writer.newline() - for i in range(len(glyphOrder)): - glyphName = glyphOrder[i] - writer.simpletag("GlyphID", id=i, name=glyphName) - writer.newline() + def toXML(self, writer, ttFont): + glyphOrder = ttFont.getGlyphOrder() + writer.comment( + "The 'id' attribute is only for humans; " "it is ignored when parsed." + ) + writer.newline() + for i in range(len(glyphOrder)): + glyphName = glyphOrder[i] + writer.simpletag("GlyphID", id=i, name=glyphName) + writer.newline() - def fromXML(self, name, attrs, content, ttFont): - if not hasattr(self, "glyphOrder"): - self.glyphOrder = [] - if name == "GlyphID": - self.glyphOrder.append(attrs["name"]) - ttFont.setGlyphOrder(self.glyphOrder) + def fromXML(self, name, attrs, content, ttFont): + if not hasattr(self, "glyphOrder"): + self.glyphOrder = [] + if name == "GlyphID": + self.glyphOrder.append(attrs["name"]) + ttFont.setGlyphOrder(self.glyphOrder) def getTableModule(tag): - """Fetch the packer/unpacker module for a table. - Return None when no module is found. - """ - from . import tables - pyTag = tagToIdentifier(tag) - try: - __import__("fontTools.ttLib.tables." + pyTag) - except ImportError as err: - # If pyTag is found in the ImportError message, - # means table is not implemented. If it's not - # there, then some other module is missing, don't - # suppress the error. - if str(err).find(pyTag) >= 0: - return None - else: - raise err - else: - return getattr(tables, pyTag) + """Fetch the packer/unpacker module for a table. + Return None when no module is found. + """ + from . import tables + + pyTag = tagToIdentifier(tag) + try: + __import__("fontTools.ttLib.tables." + pyTag) + except ImportError as err: + # If pyTag is found in the ImportError message, + # means table is not implemented. If it's not + # there, then some other module is missing, don't + # suppress the error. + if str(err).find(pyTag) >= 0: + return None + else: + raise err + else: + return getattr(tables, pyTag) # Registry for custom table packer/unpacker classes. Keys are table @@ -800,221 +897,248 @@ _customTableRegistry = {} def registerCustomTableClass(tag, moduleName, className=None): - """Register a custom packer/unpacker class for a table. + """Register a custom packer/unpacker class for a table. - The 'moduleName' must be an importable module. If no 'className' - is given, it is derived from the tag, for example it will be - ``table_C_U_S_T_`` for a 'CUST' tag. + The 'moduleName' must be an importable module. If no 'className' + is given, it is derived from the tag, for example it will be + ``table_C_U_S_T_`` for a 'CUST' tag. - The registered table class should be a subclass of - :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable` - """ - if className is None: - className = "table_" + tagToIdentifier(tag) - _customTableRegistry[tag] = (moduleName, className) + The registered table class should be a subclass of + :py:class:`fontTools.ttLib.tables.DefaultTable.DefaultTable` + """ + if className is None: + className = "table_" + tagToIdentifier(tag) + _customTableRegistry[tag] = (moduleName, className) def unregisterCustomTableClass(tag): - """Unregister the custom packer/unpacker class for a table.""" - del _customTableRegistry[tag] + """Unregister the custom packer/unpacker class for a table.""" + del _customTableRegistry[tag] def getCustomTableClass(tag): - """Return the custom table class for tag, if one has been registered - with 'registerCustomTableClass()'. Else return None. - """ - if tag not in _customTableRegistry: - return None - import importlib - moduleName, className = _customTableRegistry[tag] - module = importlib.import_module(moduleName) - return getattr(module, className) + """Return the custom table class for tag, if one has been registered + with 'registerCustomTableClass()'. Else return None. + """ + if tag not in _customTableRegistry: + return None + import importlib + + moduleName, className = _customTableRegistry[tag] + module = importlib.import_module(moduleName) + return getattr(module, className) def getTableClass(tag): - """Fetch the packer/unpacker class for a table.""" - tableClass = getCustomTableClass(tag) - if tableClass is not None: - return tableClass - module = getTableModule(tag) - if module is None: - from .tables.DefaultTable import DefaultTable - return DefaultTable - pyTag = tagToIdentifier(tag) - tableClass = getattr(module, "table_" + pyTag) - return tableClass + """Fetch the packer/unpacker class for a table.""" + tableClass = getCustomTableClass(tag) + if tableClass is not None: + return tableClass + module = getTableModule(tag) + if module is None: + from .tables.DefaultTable import DefaultTable + + return DefaultTable + pyTag = tagToIdentifier(tag) + tableClass = getattr(module, "table_" + pyTag) + return tableClass def getClassTag(klass): - """Fetch the table tag for a class object.""" - name = klass.__name__ - assert name[:6] == 'table_' - name = name[6:] # Chop 'table_' - return identifierToTag(name) + """Fetch the table tag for a class object.""" + name = klass.__name__ + assert name[:6] == "table_" + name = name[6:] # Chop 'table_' + return identifierToTag(name) def newTable(tag): - """Return a new instance of a table.""" - tableClass = getTableClass(tag) - return tableClass(tag) + """Return a new instance of a table.""" + tableClass = getTableClass(tag) + return tableClass(tag) def _escapechar(c): - """Helper function for tagToIdentifier()""" - import re - if re.match("[a-z0-9]", c): - return "_" + c - elif re.match("[A-Z]", c): - return c + "_" - else: - return hex(byteord(c))[2:] + """Helper function for tagToIdentifier()""" + import re + + if re.match("[a-z0-9]", c): + return "_" + c + elif re.match("[A-Z]", c): + return c + "_" + else: + return hex(byteord(c))[2:] def tagToIdentifier(tag): - """Convert a table tag to a valid (but UGLY) python identifier, - as well as a filename that's guaranteed to be unique even on a - caseless file system. Each character is mapped to two characters. - Lowercase letters get an underscore before the letter, uppercase - letters get an underscore after the letter. Trailing spaces are - trimmed. Illegal characters are escaped as two hex bytes. If the - result starts with a number (as the result of a hex escape), an - extra underscore is prepended. Examples:: - - >>> tagToIdentifier('glyf') - '_g_l_y_f' - >>> tagToIdentifier('cvt ') - '_c_v_t' - >>> tagToIdentifier('OS/2') - 'O_S_2f_2' - """ - import re - tag = Tag(tag) - if tag == "GlyphOrder": - return tag - assert len(tag) == 4, "tag should be 4 characters long" - while len(tag) > 1 and tag[-1] == ' ': - tag = tag[:-1] - ident = "" - for c in tag: - ident = ident + _escapechar(c) - if re.match("[0-9]", ident): - ident = "_" + ident - return ident + """Convert a table tag to a valid (but UGLY) python identifier, + as well as a filename that's guaranteed to be unique even on a + caseless file system. Each character is mapped to two characters. + Lowercase letters get an underscore before the letter, uppercase + letters get an underscore after the letter. Trailing spaces are + trimmed. Illegal characters are escaped as two hex bytes. If the + result starts with a number (as the result of a hex escape), an + extra underscore is prepended. Examples:: + + >>> tagToIdentifier('glyf') + '_g_l_y_f' + >>> tagToIdentifier('cvt ') + '_c_v_t' + >>> tagToIdentifier('OS/2') + 'O_S_2f_2' + """ + import re + + tag = Tag(tag) + if tag == "GlyphOrder": + return tag + assert len(tag) == 4, "tag should be 4 characters long" + while len(tag) > 1 and tag[-1] == " ": + tag = tag[:-1] + ident = "" + for c in tag: + ident = ident + _escapechar(c) + if re.match("[0-9]", ident): + ident = "_" + ident + return ident def identifierToTag(ident): - """the opposite of tagToIdentifier()""" - if ident == "GlyphOrder": - return ident - if len(ident) % 2 and ident[0] == "_": - ident = ident[1:] - assert not (len(ident) % 2) - tag = "" - for i in range(0, len(ident), 2): - if ident[i] == "_": - tag = tag + ident[i+1] - elif ident[i+1] == "_": - tag = tag + ident[i] - else: - # assume hex - tag = tag + chr(int(ident[i:i+2], 16)) - # append trailing spaces - tag = tag + (4 - len(tag)) * ' ' - return Tag(tag) + """the opposite of tagToIdentifier()""" + if ident == "GlyphOrder": + return ident + if len(ident) % 2 and ident[0] == "_": + ident = ident[1:] + assert not (len(ident) % 2) + tag = "" + for i in range(0, len(ident), 2): + if ident[i] == "_": + tag = tag + ident[i + 1] + elif ident[i + 1] == "_": + tag = tag + ident[i] + else: + # assume hex + tag = tag + chr(int(ident[i : i + 2], 16)) + # append trailing spaces + tag = tag + (4 - len(tag)) * " " + return Tag(tag) def tagToXML(tag): - """Similarly to tagToIdentifier(), this converts a TT tag - to a valid XML element name. Since XML element names are - case sensitive, this is a fairly simple/readable translation. - """ - import re - tag = Tag(tag) - if tag == "OS/2": - return "OS_2" - elif tag == "GlyphOrder": - return tag - if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): - return tag.strip() - else: - return tagToIdentifier(tag) + """Similarly to tagToIdentifier(), this converts a TT tag + to a valid XML element name. Since XML element names are + case sensitive, this is a fairly simple/readable translation. + """ + import re + + tag = Tag(tag) + if tag == "OS/2": + return "OS_2" + elif tag == "GlyphOrder": + return tag + if re.match("[A-Za-z_][A-Za-z_0-9]* *$", tag): + return tag.strip() + else: + return tagToIdentifier(tag) def xmlToTag(tag): - """The opposite of tagToXML()""" - if tag == "OS_2": - return Tag("OS/2") - if len(tag) == 8: - return identifierToTag(tag) - else: - return Tag(tag + " " * (4 - len(tag))) - + """The opposite of tagToXML()""" + if tag == "OS_2": + return Tag("OS/2") + if len(tag) == 8: + return identifierToTag(tag) + else: + return Tag(tag + " " * (4 - len(tag))) # Table order as recommended in the OpenType specification 1.4 -TTFTableOrder = ["head", "hhea", "maxp", "OS/2", "hmtx", "LTSH", "VDMX", - "hdmx", "cmap", "fpgm", "prep", "cvt ", "loca", "glyf", - "kern", "name", "post", "gasp", "PCLT"] +TTFTableOrder = [ + "head", + "hhea", + "maxp", + "OS/2", + "hmtx", + "LTSH", + "VDMX", + "hdmx", + "cmap", + "fpgm", + "prep", + "cvt ", + "loca", + "glyf", + "kern", + "name", + "post", + "gasp", + "PCLT", +] + +OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", "CFF "] -OTFTableOrder = ["head", "hhea", "maxp", "OS/2", "name", "cmap", "post", - "CFF "] def sortedTagList(tagList, tableOrder=None): - """Return a sorted copy of tagList, sorted according to the OpenType - specification, or according to a custom tableOrder. If given and not - None, tableOrder needs to be a list of tag names. - """ - tagList = sorted(tagList) - if tableOrder is None: - if "DSIG" in tagList: - # DSIG should be last (XXX spec reference?) - tagList.remove("DSIG") - tagList.append("DSIG") - if "CFF " in tagList: - tableOrder = OTFTableOrder - else: - tableOrder = TTFTableOrder - orderedTables = [] - for tag in tableOrder: - if tag in tagList: - orderedTables.append(tag) - tagList.remove(tag) - orderedTables.extend(tagList) - return orderedTables + """Return a sorted copy of tagList, sorted according to the OpenType + specification, or according to a custom tableOrder. If given and not + None, tableOrder needs to be a list of tag names. + """ + tagList = sorted(tagList) + if tableOrder is None: + if "DSIG" in tagList: + # DSIG should be last (XXX spec reference?) + tagList.remove("DSIG") + tagList.append("DSIG") + if "CFF " in tagList: + tableOrder = OTFTableOrder + else: + tableOrder = TTFTableOrder + orderedTables = [] + for tag in tableOrder: + if tag in tagList: + orderedTables.append(tag) + tagList.remove(tag) + orderedTables.extend(tagList) + return orderedTables def reorderFontTables(inFile, outFile, tableOrder=None, checkChecksums=False): - """Rewrite a font file, ordering the tables as recommended by the - OpenType specification 1.4. - """ - inFile.seek(0) - outFile.seek(0) - reader = SFNTReader(inFile, checkChecksums=checkChecksums) - writer = SFNTWriter(outFile, len(reader.tables), reader.sfntVersion, reader.flavor, reader.flavorData) - tables = list(reader.keys()) - for tag in sortedTagList(tables, tableOrder): - writer[tag] = reader[tag] - writer.close() + """Rewrite a font file, ordering the tables as recommended by the + OpenType specification 1.4. + """ + inFile.seek(0) + outFile.seek(0) + reader = SFNTReader(inFile, checkChecksums=checkChecksums) + writer = SFNTWriter( + outFile, + len(reader.tables), + reader.sfntVersion, + reader.flavor, + reader.flavorData, + ) + tables = list(reader.keys()) + for tag in sortedTagList(tables, tableOrder): + writer[tag] = reader[tag] + writer.close() def maxPowerOfTwo(x): - """Return the highest exponent of two, so that - (2 ** exponent) <= x. Return 0 if x is 0. - """ - exponent = 0 - while x: - x = x >> 1 - exponent = exponent + 1 - return max(exponent - 1, 0) + """Return the highest exponent of two, so that + (2 ** exponent) <= x. Return 0 if x is 0. + """ + exponent = 0 + while x: + x = x >> 1 + exponent = exponent + 1 + return max(exponent - 1, 0) def getSearchRange(n, itemSize=16): - """Calculate searchRange, entrySelector, rangeShift. - """ - # itemSize defaults to 16, for backward compatibility - # with upstream fonttools. - exponent = maxPowerOfTwo(n) - searchRange = (2 ** exponent) * itemSize - entrySelector = exponent - rangeShift = max(0, n * itemSize - searchRange) - return searchRange, entrySelector, rangeShift + """Calculate searchRange, entrySelector, rangeShift.""" + # itemSize defaults to 16, for backward compatibility + # with upstream fonttools. + exponent = maxPowerOfTwo(n) + searchRange = (2**exponent) * itemSize + entrySelector = exponent + rangeShift = max(0, n * itemSize - searchRange) + return searchRange, entrySelector, rangeShift |