|
|
|
|
|
|
|
|
|
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 (e.g. ``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. |
|
""" |
|
|
|
|
|
|
|
fonts = self._openFonts(fontfiles) |
|
glyphOrders = [list(font.getGlyphOrder()) for font in fonts] |
|
computeMegaGlyphOrder(self, glyphOrders) |
|
|
|
|
|
sfntVersion = fonts[0].sfntVersion |
|
|
|
|
|
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) |
|
|
|
|
|
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): |
|
|
|
|
|
|
|
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: |
|
|
|
|
|
font["OS/2"].recalcAvgCharWidth(font) |
|
|
|
|
|
__all__ = ["Options", "Merger", "main"] |
|
|
|
|
|
@timer("make one with everything (TOTAL TIME)") |
|
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()) |
|
|