Spaces:
Sleeping
Sleeping
# Copyright 2013 Google, Inc. All Rights Reserved. | |
# | |
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader | |
from fontTools import ttLib | |
import fontTools.merge.base | |
from fontTools.merge.cmap import ( | |
computeMegaGlyphOrder, | |
computeMegaCmap, | |
renameCFFCharStrings, | |
) | |
from fontTools.merge.layout import layoutPreMerge, layoutPostMerge | |
from fontTools.merge.options import Options | |
import fontTools.merge.tables | |
from fontTools.misc.loggingTools import Timer | |
from functools import reduce | |
import sys | |
import logging | |
log = logging.getLogger("fontTools.merge") | |
timer = Timer(logger=logging.getLogger(__name__ + ".timer"), level=logging.INFO) | |
class Merger(object): | |
"""Font merger. | |
This class merges multiple files into a single OpenType font, taking into | |
account complexities such as OpenType layout (``GSUB``/``GPOS``) tables and | |
cross-font metrics (for example ``hhea.ascent`` is set to the maximum value | |
across all the fonts). | |
If multiple glyphs map to the same Unicode value, and the glyphs are considered | |
sufficiently different (that is, they differ in any of paths, widths, or | |
height), then subsequent glyphs are renamed and a lookup in the ``locl`` | |
feature will be created to disambiguate them. For example, if the arguments | |
are an Arabic font and a Latin font and both contain a set of parentheses, | |
the Latin glyphs will be renamed to ``parenleft.1`` and ``parenright.1``, | |
and a lookup will be inserted into the to ``locl`` feature (creating it if | |
necessary) under the ``latn`` script to substitute ``parenleft`` with | |
``parenleft.1`` etc. | |
Restrictions: | |
- All fonts must have the same units per em. | |
- If duplicate glyph disambiguation takes place as described above then the | |
fonts must have a ``GSUB`` table. | |
Attributes: | |
options: Currently unused. | |
""" | |
def __init__(self, options=None): | |
if not options: | |
options = Options() | |
self.options = options | |
def _openFonts(self, fontfiles): | |
fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles] | |
for font, fontfile in zip(fonts, fontfiles): | |
font._merger__fontfile = fontfile | |
font._merger__name = font["name"].getDebugName(4) | |
return fonts | |
def merge(self, fontfiles): | |
"""Merges fonts together. | |
Args: | |
fontfiles: A list of file names to be merged | |
Returns: | |
A :class:`fontTools.ttLib.TTFont` object. Call the ``save`` method on | |
this to write it out to an OTF file. | |
""" | |
# | |
# Settle on a mega glyph order. | |
# | |
fonts = self._openFonts(fontfiles) | |
glyphOrders = [list(font.getGlyphOrder()) for font in fonts] | |
computeMegaGlyphOrder(self, glyphOrders) | |
# Take first input file sfntVersion | |
sfntVersion = fonts[0].sfntVersion | |
# Reload fonts and set new glyph names on them. | |
fonts = self._openFonts(fontfiles) | |
for font, glyphOrder in zip(fonts, glyphOrders): | |
font.setGlyphOrder(glyphOrder) | |
if "CFF " in font: | |
renameCFFCharStrings(self, glyphOrder, font["CFF "]) | |
cmaps = [font["cmap"] for font in fonts] | |
self.duplicateGlyphsPerFont = [{} for _ in fonts] | |
computeMegaCmap(self, cmaps) | |
mega = ttLib.TTFont(sfntVersion=sfntVersion) | |
mega.setGlyphOrder(self.glyphOrder) | |
for font in fonts: | |
self._preMerge(font) | |
self.fonts = fonts | |
allTags = reduce(set.union, (list(font.keys()) for font in fonts), set()) | |
allTags.remove("GlyphOrder") | |
for tag in sorted(allTags): | |
if tag in self.options.drop_tables: | |
continue | |
with timer("merge '%s'" % tag): | |
tables = [font.get(tag, NotImplemented) for font in fonts] | |
log.info("Merging '%s'.", tag) | |
clazz = ttLib.getTableClass(tag) | |
table = clazz(tag).merge(self, tables) | |
# XXX Clean this up and use: table = mergeObjects(tables) | |
if table is not NotImplemented and table is not False: | |
mega[tag] = table | |
log.info("Merged '%s'.", tag) | |
else: | |
log.info("Dropped '%s'.", tag) | |
del self.duplicateGlyphsPerFont | |
del self.fonts | |
self._postMerge(mega) | |
return mega | |
def mergeObjects(self, returnTable, logic, tables): | |
# Right now we don't use self at all. Will use in the future | |
# for options and logging. | |
allKeys = set.union( | |
set(), | |
*(vars(table).keys() for table in tables if table is not NotImplemented), | |
) | |
for key in allKeys: | |
log.info(" %s", key) | |
try: | |
mergeLogic = logic[key] | |
except KeyError: | |
try: | |
mergeLogic = logic["*"] | |
except KeyError: | |
raise Exception( | |
"Don't know how to merge key %s of class %s" | |
% (key, returnTable.__class__.__name__) | |
) | |
if mergeLogic is NotImplemented: | |
continue | |
value = mergeLogic(getattr(table, key, NotImplemented) for table in tables) | |
if value is not NotImplemented: | |
setattr(returnTable, key, value) | |
return returnTable | |
def _preMerge(self, font): | |
layoutPreMerge(font) | |
def _postMerge(self, font): | |
layoutPostMerge(font) | |
if "OS/2" in font: | |
# https://github.com/fonttools/fonttools/issues/2538 | |
# TODO: Add an option to disable this? | |
font["OS/2"].recalcAvgCharWidth(font) | |
__all__ = ["Options", "Merger", "main"] | |
def main(args=None): | |
"""Merge multiple fonts into one""" | |
from fontTools import configLogger | |
if args is None: | |
args = sys.argv[1:] | |
options = Options() | |
args = options.parse_opts(args) | |
fontfiles = [] | |
if options.input_file: | |
with open(options.input_file) as inputfile: | |
fontfiles = [ | |
line.strip() | |
for line in inputfile.readlines() | |
if not line.lstrip().startswith("#") | |
] | |
for g in args: | |
fontfiles.append(g) | |
if len(fontfiles) < 1: | |
print( | |
"usage: pyftmerge [font1 ... fontN] [--input-file=filelist.txt] [--output-file=merged.ttf] [--import-file=tables.ttx]", | |
file=sys.stderr, | |
) | |
print( | |
" [--drop-tables=tags] [--verbose] [--timing]", | |
file=sys.stderr, | |
) | |
print("", file=sys.stderr) | |
print(" font1 ... fontN Files to merge.", file=sys.stderr) | |
print( | |
" --input-file=<filename> Read files to merge from a text file, each path new line. # Comment lines allowed.", | |
file=sys.stderr, | |
) | |
print( | |
" --output-file=<filename> Specify output file name (default: merged.ttf).", | |
file=sys.stderr, | |
) | |
print( | |
" --import-file=<filename> TTX file to import after merging. This can be used to set metadata.", | |
file=sys.stderr, | |
) | |
print( | |
" --drop-tables=<table tags> Comma separated list of table tags to skip, case sensitive.", | |
file=sys.stderr, | |
) | |
print( | |
" --verbose Output progress information.", | |
file=sys.stderr, | |
) | |
print(" --timing Output progress timing.", file=sys.stderr) | |
return 1 | |
configLogger(level=logging.INFO if options.verbose else logging.WARNING) | |
if options.timing: | |
timer.logger.setLevel(logging.DEBUG) | |
else: | |
timer.logger.disabled = True | |
merger = Merger(options=options) | |
font = merger.merge(fontfiles) | |
if options.import_file: | |
font.importXML(options.import_file) | |
with timer("compile and save font"): | |
font.save(options.output_file) | |
if __name__ == "__main__": | |
sys.exit(main()) | |