Move TTFont into fontTools.ttLib.ttFont
Users can still import TTFont from fontTools.ttLib. It was for cleaning up only.
This commit is contained in:
parent
76ba3b423f
commit
49b6004040
@ -43,977 +43,18 @@ Dumping 'prep' table...
|
||||
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.loggingTools import deprecateArgument, deprecateFunction
|
||||
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
|
||||
import os
|
||||
import sys
|
||||
from fontTools.misc.loggingTools import deprecateFunction
|
||||
import logging
|
||||
import itertools
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class TTLibError(Exception): pass
|
||||
|
||||
|
||||
class TTFont(object):
|
||||
|
||||
"""The main font object. It 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.
|
||||
"""
|
||||
|
||||
def __init__(self, file=None, res_name_or_index=None,
|
||||
sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False,
|
||||
verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False,
|
||||
recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
|
||||
_tableCache=None):
|
||||
|
||||
"""The constructor can be called with a few different arguments.
|
||||
When reading a font from disk, 'file' should be either a pathname
|
||||
pointing to a file, or a readable file object.
|
||||
|
||||
It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt
|
||||
resource name or an sfnt resource index number or zero. The latter
|
||||
case will cause TTLib to autodetect whether the file is a flat file
|
||||
or a suitcase. (If it's a suitcase, only the first 'sfnt' resource
|
||||
will be read!)
|
||||
|
||||
The 'checkChecksums' argument is used to specify how sfnt
|
||||
checksums are treated upon reading a file from disk:
|
||||
0: don't check (default)
|
||||
1: check, print warnings if a wrong checksum is found
|
||||
2: check, raise an exception if a wrong checksum is found.
|
||||
|
||||
The TTFont constructor can also be called without a 'file'
|
||||
argument: this is the way to create a new empty font.
|
||||
In this case you can optionally supply the 'sfntVersion' argument,
|
||||
and a 'flavor' which can be None, 'woff', or 'woff2'.
|
||||
|
||||
If the recalcBBoxes argument is false, a number of things will *not*
|
||||
be recalculated upon save/compile:
|
||||
1) 'glyf' glyph bounding boxes
|
||||
2) 'CFF ' font bounding box
|
||||
3) 'head' font bounding box
|
||||
4) 'hhea' min/max values
|
||||
5) 'vhea' min/max values
|
||||
(1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-).
|
||||
Additionally, upon importing an TTX file, this option cause glyphs
|
||||
to be compiled right away. This should reduce memory consumption
|
||||
greatly, and therefore should have some impact on the time needed
|
||||
to parse/compile large fonts.
|
||||
|
||||
If the recalcTimestamp argument is false, the modified timestamp in the
|
||||
'head' table will *not* be recalculated upon save/compile.
|
||||
|
||||
If the allowVID argument is set to true, then virtual GID's are
|
||||
supported. Asking for a glyph ID with a glyph name or GID that is not in
|
||||
the font will return a virtual GID. This is valid for GSUB and cmap
|
||||
tables. For SING glyphlets, the cmap table is used to specify Unicode
|
||||
values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested
|
||||
and does not exist in the font, or the glyphname has the form glyphN
|
||||
and does not exist in the font, then N is used as the virtual GID.
|
||||
Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new
|
||||
virtual GIDs, the next is one less than the previous.
|
||||
|
||||
If ignoreDecompileErrors is set to True, exceptions raised in
|
||||
individual tables during decompilation will be ignored, falling
|
||||
back to the DefaultTable implementation, which simply keeps the
|
||||
binary data.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# Permit the user to reference glyphs that are not int the font.
|
||||
self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value.
|
||||
self.reverseVIDDict = {}
|
||||
self.VIDDict = {}
|
||||
self.allowVID = allowVID
|
||||
self.ignoreDecompileErrors = ignoreDecompileErrors
|
||||
|
||||
if not file:
|
||||
self.sfntVersion = sfntVersion
|
||||
self.flavor = flavor
|
||||
self.flavorData = None
|
||||
return
|
||||
if not hasattr(file, "read"):
|
||||
# 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")
|
||||
|
||||
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
|
||||
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 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. Similarly to the constructor,
|
||||
the 'file' argument can be either a pathname or a writable
|
||||
file object.
|
||||
"""
|
||||
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")
|
||||
file = open(file, "wb")
|
||||
|
||||
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
|
||||
tmp = BytesIO()
|
||||
writer = SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData)
|
||||
|
||||
done = []
|
||||
for tag in tags:
|
||||
self._writeTable(tag, writer, done)
|
||||
|
||||
writer.close()
|
||||
|
||||
if (reorderTables is None or writer.reordersTables() or
|
||||
(reorderTables is False and self.reader is None)):
|
||||
# don't reorder tables and save as is
|
||||
file.write(tmp.getvalue())
|
||||
tmp.close()
|
||||
else:
|
||||
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()
|
||||
tmp.seek(0)
|
||||
tmp2 = BytesIO()
|
||||
reorderFontTables(tmp, tmp2, tableOrder)
|
||||
file.write(tmp2.getvalue())
|
||||
tmp.close()
|
||||
tmp2.close()
|
||||
|
||||
def saveXML(self, fileOrPath, progress=None, quiet=None,
|
||||
tables=None, skipTables=None, splitTables=False, disassembleInstructions=True,
|
||||
bitmapGlyphDataFormat='raw', newlinestr=None):
|
||||
"""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.
|
||||
"""
|
||||
from fontTools import version
|
||||
from fontTools.misc import xmlWriter
|
||||
|
||||
# only write the MAJOR.MINOR version in the 'ttLibVersion' attribute of
|
||||
# TTX files' root element (without PATCH or .dev suffixes)
|
||||
version = ".".join(version.split('.')[:2])
|
||||
|
||||
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 progress:
|
||||
progress.set(0, numTables)
|
||||
idlefunc = getattr(progress, "idle", None)
|
||||
else:
|
||||
idlefunc = None
|
||||
|
||||
writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc,
|
||||
newlinestr=newlinestr)
|
||||
writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
|
||||
ttLibVersion=version)
|
||||
writer.newline()
|
||||
|
||||
if not splitTables:
|
||||
writer.newline()
|
||||
else:
|
||||
# 'fileOrPath' must now be a path
|
||||
path, ext = os.path.splitext(fileOrPath)
|
||||
fileNameTemplate = path + ".%s" + ext
|
||||
|
||||
for i in range(numTables):
|
||||
if progress:
|
||||
progress.set(i)
|
||||
tag = tables[i]
|
||||
if splitTables:
|
||||
tablePath = fileNameTemplate % tagToIdentifier(tag)
|
||||
tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc,
|
||||
newlinestr=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, progress)
|
||||
if splitTables:
|
||||
tableWriter.endtag("ttFont")
|
||||
tableWriter.newline()
|
||||
tableWriter.close()
|
||||
if progress:
|
||||
progress.set((i + 1))
|
||||
writer.endtag("ttFont")
|
||||
writer.newline()
|
||||
# close if 'fileOrPath' is a path; leave it open if it's a file.
|
||||
# The special string "-" means standard output so leave that open too
|
||||
if not hasattr(fileOrPath, "write") and fileOrPath != "-":
|
||||
writer.close()
|
||||
|
||||
def _tableToXML(self, writer, tag, progress, quiet=None):
|
||||
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
|
||||
if progress:
|
||||
progress.setLabel(report)
|
||||
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 in ("glyf", "CFF "):
|
||||
table.toXML(writer, self, progress)
|
||||
else:
|
||||
table.toXML(writer, self)
|
||||
writer.endtag(xmlTag)
|
||||
writer.newline()
|
||||
writer.newline()
|
||||
|
||||
def importXML(self, fileOrPath, progress=None, 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, progress)
|
||||
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):
|
||||
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):
|
||||
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 __len__(self):
|
||||
return len(list(self.keys()))
|
||||
|
||||
def __getitem__(self, tag):
|
||||
tag = Tag(tag)
|
||||
try:
|
||||
return self.tables[tag]
|
||||
except KeyError:
|
||||
if tag == "GlyphOrder":
|
||||
table = GlyphOrder(tag)
|
||||
self.tables[tag] = table
|
||||
return table
|
||||
if self.reader is not None:
|
||||
import traceback
|
||||
log.debug("Reading '%s' table from disk", tag)
|
||||
data = self.reader[tag]
|
||||
if self.tableCache is not None:
|
||||
table = self.tableCache.get((Tag(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:
|
||||
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(tag), data)] = table
|
||||
return table
|
||||
else:
|
||||
raise KeyError("'%s' table not found" % tag)
|
||||
|
||||
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):
|
||||
try:
|
||||
return self[tag]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def setGlyphOrder(self, glyphOrder):
|
||||
self.glyphOrder = glyphOrder
|
||||
|
||||
def getGlyphOrder(self):
|
||||
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.
|
||||
reversecmap = self['cmap'].buildReversed()
|
||||
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
|
||||
|
||||
# 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, requireReal=False):
|
||||
try:
|
||||
return self.getGlyphOrder()[glyphID]
|
||||
except IndexError:
|
||||
if requireReal or not self.allowVID:
|
||||
# XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in
|
||||
# the cmap table than there are glyphs. I don't think it's legal...
|
||||
return "glyph%.5d" % glyphID
|
||||
else:
|
||||
# user intends virtual GID support
|
||||
try:
|
||||
glyphName = self.VIDDict[glyphID]
|
||||
except KeyError:
|
||||
glyphName ="glyph%.5d" % glyphID
|
||||
self.last_vid = min(glyphID, self.last_vid )
|
||||
self.reverseVIDDict[glyphName] = glyphID
|
||||
self.VIDDict[glyphID] = glyphName
|
||||
return glyphName
|
||||
|
||||
def getGlyphID(self, glyphName, requireReal=False):
|
||||
if not hasattr(self, "_reverseGlyphOrderDict"):
|
||||
self._buildReverseGlyphOrderDict()
|
||||
glyphOrder = self.getGlyphOrder()
|
||||
d = self._reverseGlyphOrderDict
|
||||
if glyphName not in d:
|
||||
if glyphName in glyphOrder:
|
||||
self._buildReverseGlyphOrderDict()
|
||||
return self.getGlyphID(glyphName)
|
||||
else:
|
||||
if requireReal:
|
||||
raise KeyError(glyphName)
|
||||
elif not self.allowVID:
|
||||
# Handle glyphXXX only
|
||||
if glyphName[:5] == "glyph":
|
||||
try:
|
||||
return int(glyphName[5:])
|
||||
except (NameError, ValueError):
|
||||
raise KeyError(glyphName)
|
||||
else:
|
||||
# user intends virtual GID support
|
||||
try:
|
||||
glyphID = self.reverseVIDDict[glyphName]
|
||||
except KeyError:
|
||||
# if name is in glyphXXX format, use the specified name.
|
||||
if glyphName[:5] == "glyph":
|
||||
try:
|
||||
glyphID = int(glyphName[5:])
|
||||
except (NameError, ValueError):
|
||||
glyphID = None
|
||||
if glyphID is None:
|
||||
glyphID = self.last_vid -1
|
||||
self.last_vid = glyphID
|
||||
self.reverseVIDDict[glyphName] = glyphID
|
||||
self.VIDDict[glyphID] = glyphName
|
||||
return glyphID
|
||||
|
||||
glyphID = d[glyphName]
|
||||
if glyphName != glyphOrder[glyphID]:
|
||||
self._buildReverseGlyphOrderDict()
|
||||
return self.getGlyphID(glyphName)
|
||||
return glyphID
|
||||
|
||||
def getReverseGlyphMap(self, rebuild=False):
|
||||
if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
|
||||
self._buildReverseGlyphOrderDict()
|
||||
return self._reverseGlyphOrderDict
|
||||
|
||||
def _buildReverseGlyphOrderDict(self):
|
||||
self._reverseGlyphOrderDict = d = {}
|
||||
glyphOrder = self.getGlyphOrder()
|
||||
for glyphID in range(len(glyphOrder)):
|
||||
d[glyphOrder[glyphID]] = glyphID
|
||||
|
||||
def _writeTable(self, tag, writer, done):
|
||||
"""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)
|
||||
else:
|
||||
done.append(masterTable)
|
||||
tabledata = self.getTableData(tag)
|
||||
log.debug("writing '%s' table to disk", tag)
|
||||
writer[tag] = tabledata
|
||||
done.append(tag)
|
||||
|
||||
def getTableData(self, tag):
|
||||
"""Returns raw table data, whether compiled or directly read from disk.
|
||||
"""
|
||||
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):
|
||||
"""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.
|
||||
"""
|
||||
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 "
|
||||
glyphs = _TTGlyphSet(self,
|
||||
list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF)
|
||||
|
||||
if glyphs is None and "glyf" in self:
|
||||
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))):
|
||||
"""Return 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:
|
||||
(3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0)
|
||||
This can be customized via the cmapPreferences argument.
|
||||
"""
|
||||
return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
|
||||
|
||||
|
||||
class _TTGlyphSet(object):
|
||||
|
||||
"""Generic dict-like GlyphSet class that pulls metrics from hmtx and
|
||||
glyph shape from TrueType or CFF.
|
||||
"""
|
||||
|
||||
def __init__(self, ttFont, glyphs, glyphType):
|
||||
self._glyphs = glyphs
|
||||
self._hmtx = ttFont['hmtx']
|
||||
self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
|
||||
self._glyphType = glyphType
|
||||
|
||||
def keys(self):
|
||||
return list(self._glyphs.keys())
|
||||
|
||||
def has_key(self, glyphName):
|
||||
return glyphName in self._glyphs
|
||||
|
||||
__contains__ = has_key
|
||||
|
||||
def __getitem__(self, glyphName):
|
||||
horizontalMetrics = self._hmtx[glyphName]
|
||||
verticalMetrics = self._vmtx[glyphName] if self._vmtx else None
|
||||
return self._glyphType(
|
||||
self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics)
|
||||
|
||||
def get(self, glyphName, default=None):
|
||||
try:
|
||||
return self[glyphName]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
class _TTGlyph(object):
|
||||
|
||||
"""Wrapper for a TrueType glyph that supports the Pen protocol, meaning
|
||||
that it has a .draw() method that takes a pen object as its only
|
||||
argument. Additionally there are 'width' and 'lsb' attributes, read from
|
||||
the 'hmtx' table.
|
||||
|
||||
If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
|
||||
attributes.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
|
||||
self._glyphset = glyphset
|
||||
self._glyph = glyph
|
||||
self.width, self.lsb = horizontalMetrics
|
||||
if verticalMetrics:
|
||||
self.height, self.tsb = verticalMetrics
|
||||
else:
|
||||
self.height, self.tsb = None, None
|
||||
|
||||
def draw(self, pen):
|
||||
"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
|
||||
how that works.
|
||||
"""
|
||||
self._glyph.draw(pen)
|
||||
|
||||
class _TTGlyphCFF(_TTGlyph):
|
||||
pass
|
||||
|
||||
class _TTGlyphGlyf(_TTGlyph):
|
||||
|
||||
def draw(self, pen):
|
||||
"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
|
||||
how that works.
|
||||
"""
|
||||
glyfTable = self._glyphset._glyphs
|
||||
glyph = self._glyph
|
||||
offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
|
||||
glyph.draw(pen, glyfTable, offset)
|
||||
|
||||
|
||||
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 = []
|
||||
ttFont.setGlyphOrder(self.glyphOrder)
|
||||
if name == "GlyphID":
|
||||
self.glyphOrder.append(attrs["name"])
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def getTableClass(tag):
|
||||
"""Fetch the packer/unpacker class for a table.
|
||||
Return None when no class is found.
|
||||
"""
|
||||
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:
|
||||
'glyf' -> '_g_l_y_f'
|
||||
'cvt ' -> '_c_v_t'
|
||||
'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)))
|
||||
|
||||
|
||||
@deprecateFunction("use logging instead", category=DeprecationWarning)
|
||||
def debugmsg(msg):
|
||||
import time
|
||||
print(msg + time.strftime(" (%H:%M:%S)", time.localtime(time.time())))
|
||||
|
||||
|
||||
# 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.
|
||||
"""
|
||||
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
|
||||
|
||||
from fontTools.ttLib.ttFont import *
|
||||
from fontTools.ttLib.ttCollection import TTCollection
|
||||
|
@ -15,6 +15,7 @@ a table's length chages you need to rewrite the whole file anyway.
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc import sstruct
|
||||
from fontTools.ttLib import TTLibError
|
||||
import struct
|
||||
from collections import OrderedDict
|
||||
import logging
|
||||
@ -54,33 +55,28 @@ class SFNTReader(object):
|
||||
if self.sfntVersion == b"ttcf":
|
||||
header = readTTCHeader(self.file)
|
||||
if not 0 <= fontNumber < header.numFonts:
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("specify a font number between 0 and %d (inclusive)" % (header.numFonts - 1))
|
||||
raise TTLibError("specify a font number between 0 and %d (inclusive)" % (header.numFonts - 1))
|
||||
self.file.seek(header.offsetTable[fontNumber])
|
||||
data = self.file.read(sfntDirectorySize)
|
||||
if len(data) != sfntDirectorySize:
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("Not a Font Collection (not enough data)")
|
||||
raise TTLibError("Not a Font Collection (not enough data)")
|
||||
sstruct.unpack(sfntDirectoryFormat, data, self)
|
||||
elif self.sfntVersion == b"wOFF":
|
||||
self.flavor = "woff"
|
||||
self.DirectoryEntry = WOFFDirectoryEntry
|
||||
data = self.file.read(woffDirectorySize)
|
||||
if len(data) != woffDirectorySize:
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("Not a WOFF font (not enough data)")
|
||||
raise TTLibError("Not a WOFF font (not enough data)")
|
||||
sstruct.unpack(woffDirectoryFormat, data, self)
|
||||
else:
|
||||
data = self.file.read(sfntDirectorySize)
|
||||
if len(data) != sfntDirectorySize:
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("Not a TrueType or OpenType font (not enough data)")
|
||||
raise TTLibError("Not a TrueType or OpenType font (not enough data)")
|
||||
sstruct.unpack(sfntDirectoryFormat, data, self)
|
||||
self.sfntVersion = Tag(self.sfntVersion)
|
||||
|
||||
if self.sfntVersion not in ("\x00\x01\x00\x00", "OTTO", "true"):
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
|
||||
raise TTLibError("Not a TrueType or OpenType font (bad sfntVersion)")
|
||||
tables = {}
|
||||
for i in range(self.numTables):
|
||||
entry = self.DirectoryEntry()
|
||||
@ -221,8 +217,7 @@ class SFNTWriter(object):
|
||||
def __setitem__(self, tag, data):
|
||||
"""Write raw table data to disk."""
|
||||
if tag in self.tables:
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("cannot rewrite '%s' table" % tag)
|
||||
raise TTLibError("cannot rewrite '%s' table" % tag)
|
||||
|
||||
entry = self.DirectoryEntry()
|
||||
entry.tag = tag
|
||||
@ -255,8 +250,7 @@ class SFNTWriter(object):
|
||||
"""
|
||||
tables = sorted(self.tables.items())
|
||||
if len(tables) != self.numTables:
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables)))
|
||||
raise TTLibError("wrong number of tables; expected %d, found %d" % (self.numTables, len(tables)))
|
||||
|
||||
if self.flavor == "woff":
|
||||
self.signature = b"wOFF"
|
||||
@ -565,8 +559,7 @@ def readTTCHeader(file):
|
||||
self = SimpleNamespace()
|
||||
data = file.read(ttcHeaderSize)
|
||||
if len(data) != ttcHeaderSize:
|
||||
from fontTools import ttLib
|
||||
raise ttLib.TTLibError("Not a Font Collection (not enough data)")
|
||||
raise TTLibError("Not a Font Collection (not enough data)")
|
||||
sstruct.unpack(ttcHeaderFormat, data, self)
|
||||
assert self.Version == 0x00010000 or self.Version == 0x00020000, "unrecognized TTC version 0x%08x" % self.Version
|
||||
self.offsetTable = struct.unpack(">%dL" % self.numFonts, file.read(self.numFonts * 4))
|
||||
|
@ -1,5 +1,6 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.ttLib.ttFont import TTFont
|
||||
from fontTools.ttLib.sfnt import readTTCHeader
|
||||
import logging
|
||||
|
||||
@ -36,7 +37,6 @@ class TTCollection(object):
|
||||
if sfntVersion != b"ttcf":
|
||||
raise TTLibError("Not a Font Collection")
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
header = readTTCHeader(file)
|
||||
for i in range(header.numFonts):
|
||||
font = TTFont(file, fontNumber=i, _tableCache=tableCache, **kwargs)
|
||||
|
964
Lib/fontTools/ttLib/ttFont.py
Normal file
964
Lib/fontTools/ttLib/ttFont.py
Normal file
@ -0,0 +1,964 @@
|
||||
from __future__ import print_function, division, absolute_import
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.loggingTools import deprecateArgument
|
||||
from fontTools.ttLib.sfnt import SFNTReader, SFNTWriter
|
||||
import os
|
||||
import logging
|
||||
import itertools
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class TTFont(object):
|
||||
|
||||
"""The main font object. It 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.
|
||||
"""
|
||||
|
||||
def __init__(self, file=None, res_name_or_index=None,
|
||||
sfntVersion="\000\001\000\000", flavor=None, checkChecksums=False,
|
||||
verbose=None, recalcBBoxes=True, allowVID=False, ignoreDecompileErrors=False,
|
||||
recalcTimestamp=True, fontNumber=-1, lazy=None, quiet=None,
|
||||
_tableCache=None):
|
||||
|
||||
"""The constructor can be called with a few different arguments.
|
||||
When reading a font from disk, 'file' should be either a pathname
|
||||
pointing to a file, or a readable file object.
|
||||
|
||||
It we're running on a Macintosh, 'res_name_or_index' maybe an sfnt
|
||||
resource name or an sfnt resource index number or zero. The latter
|
||||
case will cause TTLib to autodetect whether the file is a flat file
|
||||
or a suitcase. (If it's a suitcase, only the first 'sfnt' resource
|
||||
will be read!)
|
||||
|
||||
The 'checkChecksums' argument is used to specify how sfnt
|
||||
checksums are treated upon reading a file from disk:
|
||||
0: don't check (default)
|
||||
1: check, print warnings if a wrong checksum is found
|
||||
2: check, raise an exception if a wrong checksum is found.
|
||||
|
||||
The TTFont constructor can also be called without a 'file'
|
||||
argument: this is the way to create a new empty font.
|
||||
In this case you can optionally supply the 'sfntVersion' argument,
|
||||
and a 'flavor' which can be None, 'woff', or 'woff2'.
|
||||
|
||||
If the recalcBBoxes argument is false, a number of things will *not*
|
||||
be recalculated upon save/compile:
|
||||
1) 'glyf' glyph bounding boxes
|
||||
2) 'CFF ' font bounding box
|
||||
3) 'head' font bounding box
|
||||
4) 'hhea' min/max values
|
||||
5) 'vhea' min/max values
|
||||
(1) is needed for certain kinds of CJK fonts (ask Werner Lemberg ;-).
|
||||
Additionally, upon importing an TTX file, this option cause glyphs
|
||||
to be compiled right away. This should reduce memory consumption
|
||||
greatly, and therefore should have some impact on the time needed
|
||||
to parse/compile large fonts.
|
||||
|
||||
If the recalcTimestamp argument is false, the modified timestamp in the
|
||||
'head' table will *not* be recalculated upon save/compile.
|
||||
|
||||
If the allowVID argument is set to true, then virtual GID's are
|
||||
supported. Asking for a glyph ID with a glyph name or GID that is not in
|
||||
the font will return a virtual GID. This is valid for GSUB and cmap
|
||||
tables. For SING glyphlets, the cmap table is used to specify Unicode
|
||||
values for virtual GI's used in GSUB/GPOS rules. If the gid N is requested
|
||||
and does not exist in the font, or the glyphname has the form glyphN
|
||||
and does not exist in the font, then N is used as the virtual GID.
|
||||
Else, the first virtual GID is assigned as 0x1000 -1; for subsequent new
|
||||
virtual GIDs, the next is one less than the previous.
|
||||
|
||||
If ignoreDecompileErrors is set to True, exceptions raised in
|
||||
individual tables during decompilation will be ignored, falling
|
||||
back to the DefaultTable implementation, which simply keeps the
|
||||
binary data.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# Permit the user to reference glyphs that are not int the font.
|
||||
self.last_vid = 0xFFFE # Can't make it be 0xFFFF, as the world is full unsigned short integer counters that get incremented after the last seen GID value.
|
||||
self.reverseVIDDict = {}
|
||||
self.VIDDict = {}
|
||||
self.allowVID = allowVID
|
||||
self.ignoreDecompileErrors = ignoreDecompileErrors
|
||||
|
||||
if not file:
|
||||
self.sfntVersion = sfntVersion
|
||||
self.flavor = flavor
|
||||
self.flavorData = None
|
||||
return
|
||||
if not hasattr(file, "read"):
|
||||
# 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")
|
||||
|
||||
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
|
||||
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 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. Similarly to the constructor,
|
||||
the 'file' argument can be either a pathname or a writable
|
||||
file object.
|
||||
"""
|
||||
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")
|
||||
file = open(file, "wb")
|
||||
|
||||
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
|
||||
tmp = BytesIO()
|
||||
writer = SFNTWriter(tmp, numTables, self.sfntVersion, self.flavor, self.flavorData)
|
||||
|
||||
done = []
|
||||
for tag in tags:
|
||||
self._writeTable(tag, writer, done)
|
||||
|
||||
writer.close()
|
||||
|
||||
if (reorderTables is None or writer.reordersTables() or
|
||||
(reorderTables is False and self.reader is None)):
|
||||
# don't reorder tables and save as is
|
||||
file.write(tmp.getvalue())
|
||||
tmp.close()
|
||||
else:
|
||||
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()
|
||||
tmp.seek(0)
|
||||
tmp2 = BytesIO()
|
||||
reorderFontTables(tmp, tmp2, tableOrder)
|
||||
file.write(tmp2.getvalue())
|
||||
tmp.close()
|
||||
tmp2.close()
|
||||
|
||||
def saveXML(self, fileOrPath, progress=None, quiet=None,
|
||||
tables=None, skipTables=None, splitTables=False, disassembleInstructions=True,
|
||||
bitmapGlyphDataFormat='raw', newlinestr=None):
|
||||
"""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.
|
||||
"""
|
||||
from fontTools import version
|
||||
from fontTools.misc import xmlWriter
|
||||
|
||||
# only write the MAJOR.MINOR version in the 'ttLibVersion' attribute of
|
||||
# TTX files' root element (without PATCH or .dev suffixes)
|
||||
version = ".".join(version.split('.')[:2])
|
||||
|
||||
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 progress:
|
||||
progress.set(0, numTables)
|
||||
idlefunc = getattr(progress, "idle", None)
|
||||
else:
|
||||
idlefunc = None
|
||||
|
||||
writer = xmlWriter.XMLWriter(fileOrPath, idlefunc=idlefunc,
|
||||
newlinestr=newlinestr)
|
||||
writer.begintag("ttFont", sfntVersion=repr(tostr(self.sfntVersion))[1:-1],
|
||||
ttLibVersion=version)
|
||||
writer.newline()
|
||||
|
||||
if not splitTables:
|
||||
writer.newline()
|
||||
else:
|
||||
# 'fileOrPath' must now be a path
|
||||
path, ext = os.path.splitext(fileOrPath)
|
||||
fileNameTemplate = path + ".%s" + ext
|
||||
|
||||
for i in range(numTables):
|
||||
if progress:
|
||||
progress.set(i)
|
||||
tag = tables[i]
|
||||
if splitTables:
|
||||
tablePath = fileNameTemplate % tagToIdentifier(tag)
|
||||
tableWriter = xmlWriter.XMLWriter(tablePath, idlefunc=idlefunc,
|
||||
newlinestr=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, progress)
|
||||
if splitTables:
|
||||
tableWriter.endtag("ttFont")
|
||||
tableWriter.newline()
|
||||
tableWriter.close()
|
||||
if progress:
|
||||
progress.set((i + 1))
|
||||
writer.endtag("ttFont")
|
||||
writer.newline()
|
||||
# close if 'fileOrPath' is a path; leave it open if it's a file.
|
||||
# The special string "-" means standard output so leave that open too
|
||||
if not hasattr(fileOrPath, "write") and fileOrPath != "-":
|
||||
writer.close()
|
||||
|
||||
def _tableToXML(self, writer, tag, progress, quiet=None):
|
||||
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
|
||||
if progress:
|
||||
progress.setLabel(report)
|
||||
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 in ("glyf", "CFF "):
|
||||
table.toXML(writer, self, progress)
|
||||
else:
|
||||
table.toXML(writer, self)
|
||||
writer.endtag(xmlTag)
|
||||
writer.newline()
|
||||
writer.newline()
|
||||
|
||||
def importXML(self, fileOrPath, progress=None, 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, progress)
|
||||
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):
|
||||
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):
|
||||
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 __len__(self):
|
||||
return len(list(self.keys()))
|
||||
|
||||
def __getitem__(self, tag):
|
||||
tag = Tag(tag)
|
||||
try:
|
||||
return self.tables[tag]
|
||||
except KeyError:
|
||||
if tag == "GlyphOrder":
|
||||
table = GlyphOrder(tag)
|
||||
self.tables[tag] = table
|
||||
return table
|
||||
if self.reader is not None:
|
||||
import traceback
|
||||
log.debug("Reading '%s' table from disk", tag)
|
||||
data = self.reader[tag]
|
||||
if self.tableCache is not None:
|
||||
table = self.tableCache.get((Tag(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:
|
||||
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(tag), data)] = table
|
||||
return table
|
||||
else:
|
||||
raise KeyError("'%s' table not found" % tag)
|
||||
|
||||
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):
|
||||
try:
|
||||
return self[tag]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def setGlyphOrder(self, glyphOrder):
|
||||
self.glyphOrder = glyphOrder
|
||||
|
||||
def getGlyphOrder(self):
|
||||
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.
|
||||
reversecmap = self['cmap'].buildReversed()
|
||||
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
|
||||
|
||||
# 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, requireReal=False):
|
||||
try:
|
||||
return self.getGlyphOrder()[glyphID]
|
||||
except IndexError:
|
||||
if requireReal or not self.allowVID:
|
||||
# XXX The ??.W8.otf font that ships with OSX uses higher glyphIDs in
|
||||
# the cmap table than there are glyphs. I don't think it's legal...
|
||||
return "glyph%.5d" % glyphID
|
||||
else:
|
||||
# user intends virtual GID support
|
||||
try:
|
||||
glyphName = self.VIDDict[glyphID]
|
||||
except KeyError:
|
||||
glyphName ="glyph%.5d" % glyphID
|
||||
self.last_vid = min(glyphID, self.last_vid )
|
||||
self.reverseVIDDict[glyphName] = glyphID
|
||||
self.VIDDict[glyphID] = glyphName
|
||||
return glyphName
|
||||
|
||||
def getGlyphID(self, glyphName, requireReal=False):
|
||||
if not hasattr(self, "_reverseGlyphOrderDict"):
|
||||
self._buildReverseGlyphOrderDict()
|
||||
glyphOrder = self.getGlyphOrder()
|
||||
d = self._reverseGlyphOrderDict
|
||||
if glyphName not in d:
|
||||
if glyphName in glyphOrder:
|
||||
self._buildReverseGlyphOrderDict()
|
||||
return self.getGlyphID(glyphName)
|
||||
else:
|
||||
if requireReal:
|
||||
raise KeyError(glyphName)
|
||||
elif not self.allowVID:
|
||||
# Handle glyphXXX only
|
||||
if glyphName[:5] == "glyph":
|
||||
try:
|
||||
return int(glyphName[5:])
|
||||
except (NameError, ValueError):
|
||||
raise KeyError(glyphName)
|
||||
else:
|
||||
# user intends virtual GID support
|
||||
try:
|
||||
glyphID = self.reverseVIDDict[glyphName]
|
||||
except KeyError:
|
||||
# if name is in glyphXXX format, use the specified name.
|
||||
if glyphName[:5] == "glyph":
|
||||
try:
|
||||
glyphID = int(glyphName[5:])
|
||||
except (NameError, ValueError):
|
||||
glyphID = None
|
||||
if glyphID is None:
|
||||
glyphID = self.last_vid -1
|
||||
self.last_vid = glyphID
|
||||
self.reverseVIDDict[glyphName] = glyphID
|
||||
self.VIDDict[glyphID] = glyphName
|
||||
return glyphID
|
||||
|
||||
glyphID = d[glyphName]
|
||||
if glyphName != glyphOrder[glyphID]:
|
||||
self._buildReverseGlyphOrderDict()
|
||||
return self.getGlyphID(glyphName)
|
||||
return glyphID
|
||||
|
||||
def getReverseGlyphMap(self, rebuild=False):
|
||||
if rebuild or not hasattr(self, "_reverseGlyphOrderDict"):
|
||||
self._buildReverseGlyphOrderDict()
|
||||
return self._reverseGlyphOrderDict
|
||||
|
||||
def _buildReverseGlyphOrderDict(self):
|
||||
self._reverseGlyphOrderDict = d = {}
|
||||
glyphOrder = self.getGlyphOrder()
|
||||
for glyphID in range(len(glyphOrder)):
|
||||
d[glyphOrder[glyphID]] = glyphID
|
||||
|
||||
def _writeTable(self, tag, writer, done):
|
||||
"""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)
|
||||
else:
|
||||
done.append(masterTable)
|
||||
tabledata = self.getTableData(tag)
|
||||
log.debug("writing '%s' table to disk", tag)
|
||||
writer[tag] = tabledata
|
||||
done.append(tag)
|
||||
|
||||
def getTableData(self, tag):
|
||||
"""Returns raw table data, whether compiled or directly read from disk.
|
||||
"""
|
||||
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):
|
||||
"""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.
|
||||
"""
|
||||
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 "
|
||||
glyphs = _TTGlyphSet(self,
|
||||
list(self[table_tag].cff.values())[0].CharStrings, _TTGlyphCFF)
|
||||
|
||||
if glyphs is None and "glyf" in self:
|
||||
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))):
|
||||
"""Return 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:
|
||||
(3, 10), (0, 6), (0, 4), (3, 1), (0, 3), (0, 2), (0, 1), (0, 0)
|
||||
This can be customized via the cmapPreferences argument.
|
||||
"""
|
||||
return self["cmap"].getBestCmap(cmapPreferences=cmapPreferences)
|
||||
|
||||
|
||||
class _TTGlyphSet(object):
|
||||
|
||||
"""Generic dict-like GlyphSet class that pulls metrics from hmtx and
|
||||
glyph shape from TrueType or CFF.
|
||||
"""
|
||||
|
||||
def __init__(self, ttFont, glyphs, glyphType):
|
||||
self._glyphs = glyphs
|
||||
self._hmtx = ttFont['hmtx']
|
||||
self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
|
||||
self._glyphType = glyphType
|
||||
|
||||
def keys(self):
|
||||
return list(self._glyphs.keys())
|
||||
|
||||
def has_key(self, glyphName):
|
||||
return glyphName in self._glyphs
|
||||
|
||||
__contains__ = has_key
|
||||
|
||||
def __getitem__(self, glyphName):
|
||||
horizontalMetrics = self._hmtx[glyphName]
|
||||
verticalMetrics = self._vmtx[glyphName] if self._vmtx else None
|
||||
return self._glyphType(
|
||||
self, self._glyphs[glyphName], horizontalMetrics, verticalMetrics)
|
||||
|
||||
def get(self, glyphName, default=None):
|
||||
try:
|
||||
return self[glyphName]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
class _TTGlyph(object):
|
||||
|
||||
"""Wrapper for a TrueType glyph that supports the Pen protocol, meaning
|
||||
that it has a .draw() method that takes a pen object as its only
|
||||
argument. Additionally there are 'width' and 'lsb' attributes, read from
|
||||
the 'hmtx' table.
|
||||
|
||||
If the font contains a 'vmtx' table, there will also be 'height' and 'tsb'
|
||||
attributes.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
|
||||
self._glyphset = glyphset
|
||||
self._glyph = glyph
|
||||
self.width, self.lsb = horizontalMetrics
|
||||
if verticalMetrics:
|
||||
self.height, self.tsb = verticalMetrics
|
||||
else:
|
||||
self.height, self.tsb = None, None
|
||||
|
||||
def draw(self, pen):
|
||||
"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
|
||||
how that works.
|
||||
"""
|
||||
self._glyph.draw(pen)
|
||||
|
||||
class _TTGlyphCFF(_TTGlyph):
|
||||
pass
|
||||
|
||||
class _TTGlyphGlyf(_TTGlyph):
|
||||
|
||||
def draw(self, pen):
|
||||
"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
|
||||
how that works.
|
||||
"""
|
||||
glyfTable = self._glyphset._glyphs
|
||||
glyph = self._glyph
|
||||
offset = self.lsb - glyph.xMin if hasattr(glyph, "xMin") else 0
|
||||
glyph.draw(pen, glyfTable, offset)
|
||||
|
||||
|
||||
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 = []
|
||||
ttFont.setGlyphOrder(self.glyphOrder)
|
||||
if name == "GlyphID":
|
||||
self.glyphOrder.append(attrs["name"])
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def getTableClass(tag):
|
||||
"""Fetch the packer/unpacker class for a table.
|
||||
Return None when no class is found.
|
||||
"""
|
||||
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:
|
||||
'glyf' -> '_g_l_y_f'
|
||||
'cvt ' -> '_c_v_t'
|
||||
'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.
|
||||
"""
|
||||
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
|
Loading…
x
Reference in New Issue
Block a user