Spaces:
Running
Running
from fontTools.config import Config | |
from fontTools.misc import xmlWriter | |
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 ( | |
_TTGlyph, | |
_TTGlyphSetCFF, | |
_TTGlyphSetGlyf, | |
_TTGlyphSetVARC, | |
) | |
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter | |
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 | |
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 length 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 | |
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, recalcBounds=True | |
): | |
"""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) | |
glyphSet = None | |
if ("CFF " in self or "CFF2" in self) and (preferCFF or "glyf" not in self): | |
glyphSet = _TTGlyphSetCFF(self, location) | |
elif "glyf" in self: | |
glyphSet = _TTGlyphSetGlyf(self, location, recalcBounds=recalcBounds) | |
else: | |
raise TTLibError("Font contains no outlines") | |
if "VARC" in self: | |
glyphSet = _TTGlyphSetVARC(self, location, glyphSet) | |
return glyphSet | |
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 | |
if "fvar" not in self: | |
raise TTLibError("Not a variable font") | |
axes = self["fvar"].getAxes() | |
location = normalizeLocation(location, axes) | |
if "avar" in self: | |
location = self["avar"].renormalizeLocation(location, self) | |
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) | |
def reorderGlyphs(self, new_glyph_order): | |
from .reorderGlyphs import reorderGlyphs | |
reorderGlyphs(self, new_glyph_order) | |
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. | |
""" | |
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 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) | |
# Registry for custom table packer/unpacker classes. Keys are table | |
# tags, values are (moduleName, className) tuples. | |
# See registerCustomTableClass() and getCustomTableClass() | |
_customTableRegistry = {} | |
def registerCustomTableClass(tag, moduleName, className=None): | |
"""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 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] | |
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) | |
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 | |
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) | |
def newTable(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:] | |
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 | |
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) | |
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) | |
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))) | |
# 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", | |
] | |
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 | |
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() | |
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) | |
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 | |