-
Notifications
You must be signed in to change notification settings - Fork 448
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2473 from fonttools/merge-noto
Merger improvements
- Loading branch information
Showing
10 changed files
with
1,469 additions
and
1,305 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
# 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 (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. | ||
""" | ||
# | ||
# 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 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: | ||
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) | ||
|
||
|
||
__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, ignore_unknown=['output-file']) | ||
outfile = 'merged.ttf' | ||
fontfiles = [] | ||
for g in args: | ||
if g.startswith('--output-file='): | ||
outfile = g[14:] | ||
continue | ||
fontfiles.append(g) | ||
|
||
if len(args) < 1: | ||
print("usage: pyftmerge font...", 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) | ||
with timer("compile and save font"): | ||
font.save(outfile) | ||
|
||
|
||
if __name__ == "__main__": | ||
sys.exit(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import sys | ||
from fontTools.merge import main | ||
|
||
|
||
if __name__ == '__main__': | ||
sys.exit(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
# Copyright 2013 Google, Inc. All Rights Reserved. | ||
# | ||
# Google Author(s): Behdad Esfahbod, Roozbeh Pournader | ||
|
||
from fontTools.ttLib.tables.DefaultTable import DefaultTable | ||
import logging | ||
|
||
|
||
log = logging.getLogger("fontTools.merge") | ||
|
||
|
||
def add_method(*clazzes, **kwargs): | ||
"""Returns a decorator function that adds a new method to one or | ||
more classes.""" | ||
allowDefault = kwargs.get('allowDefaultTable', False) | ||
def wrapper(method): | ||
done = [] | ||
for clazz in clazzes: | ||
if clazz in done: continue # Support multiple names of a clazz | ||
done.append(clazz) | ||
assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.' | ||
assert method.__name__ not in clazz.__dict__, \ | ||
"Oops, class '%s' has method '%s'." % (clazz.__name__, method.__name__) | ||
setattr(clazz, method.__name__, method) | ||
return None | ||
return wrapper | ||
|
||
def mergeObjects(lst): | ||
lst = [item for item in lst if item is not NotImplemented] | ||
if not lst: | ||
return NotImplemented | ||
lst = [item for item in lst if item is not None] | ||
if not lst: | ||
return None | ||
|
||
clazz = lst[0].__class__ | ||
assert all(type(item) == clazz for item in lst), lst | ||
|
||
logic = clazz.mergeMap | ||
returnTable = clazz() | ||
returnDict = {} | ||
|
||
allKeys = set.union(set(), *(vars(table).keys() for table in lst)) | ||
for key in allKeys: | ||
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, clazz.__name__)) | ||
if mergeLogic is NotImplemented: | ||
continue | ||
value = mergeLogic(getattr(table, key, NotImplemented) for table in lst) | ||
if value is not NotImplemented: | ||
returnDict[key] = value | ||
|
||
returnTable.__dict__ = returnDict | ||
|
||
return returnTable | ||
|
||
@add_method(DefaultTable, allowDefaultTable=True) | ||
def merge(self, m, tables): | ||
if not hasattr(self, 'mergeMap'): | ||
log.info("Don't know how to merge '%s'.", self.tableTag) | ||
return NotImplemented | ||
|
||
logic = self.mergeMap | ||
|
||
if isinstance(logic, dict): | ||
return m.mergeObjects(self, self.mergeMap, tables) | ||
else: | ||
return logic(tables) | ||
|
||
|
Oops, something went wrong.