Merge branch 'merge'
diff --git a/Lib/fontTools/merge.py b/Lib/fontTools/merge.py
new file mode 100644
index 0000000..b576b59
--- /dev/null
+++ b/Lib/fontTools/merge.py
@@ -0,0 +1,457 @@
+# Copyright 2013 Google, Inc. All Rights Reserved.
+#
+# Google Author(s): Behdad Esfahbod
+
+"""Font merger.
+"""
+
+import sys
+import time
+
+import fontTools
+from fontTools import misc, ttLib, cffLib
+from fontTools.ttLib.tables import otTables
+
+def _add_method(*clazzes):
+ """Returns a decorator function that adds a new method to one or
+ more classes."""
+ def wrapper(method):
+ for clazz in clazzes:
+ assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.'
+ assert not hasattr(clazz, method.func_name), \
+ "Oops, class '%s' has method '%s'." % (clazz.__name__,
+ method.func_name)
+ setattr(clazz, method.func_name, method)
+ return None
+ return wrapper
+
+
+@_add_method(ttLib.getTableClass('maxp'))
+def merge(self, m):
+ # TODO When we correctly merge hinting data, update these values:
+ # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
+ # TODO Assumes that all tables have format 1.0; safe assumption.
+ allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
+ for key in allKeys:
+ setattr(self, key, max(getattr(table, key) for table in m.tables))
+ return True
+
+@_add_method(ttLib.getTableClass('head'))
+def merge(self, m):
+ # TODO Check that unitsPerEm are the same.
+ # TODO Use bitwise ops for flags, macStyle, fontDirectionHint
+ minMembers = ['xMin', 'yMin']
+ # Negate some members
+ for key in minMembers:
+ for table in m.tables:
+ setattr(table, key, -getattr(table, key))
+ # Get max over members
+ allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
+ for key in allKeys:
+ setattr(self, key, max(getattr(table, key) for table in m.tables))
+ # Negate them back
+ for key in minMembers:
+ for table in m.tables:
+ setattr(table, key, -getattr(table, key))
+ setattr(self, key, -getattr(self, key))
+ return True
+
+@_add_method(ttLib.getTableClass('hhea'))
+def merge(self, m):
+ # TODO Check that ascent, descent, slope, etc are the same.
+ minMembers = ['descent', 'minLeftSideBearing', 'minRightSideBearing']
+ # Negate some members
+ for key in minMembers:
+ for table in m.tables:
+ setattr(table, key, -getattr(table, key))
+ # Get max over members
+ allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
+ for key in allKeys:
+ setattr(self, key, max(getattr(table, key) for table in m.tables))
+ # Negate them back
+ for key in minMembers:
+ for table in m.tables:
+ setattr(table, key, -getattr(table, key))
+ setattr(self, key, -getattr(self, key))
+ return True
+
+@_add_method(ttLib.getTableClass('OS/2'))
+def merge(self, m):
+ # TODO Check that weight/width/subscript/superscript/etc are the same.
+ # TODO Bitwise ops for UnicodeRange/CodePageRange.
+ # TODO Pretty much all fields generated here have bogus values.
+ # Get max over members
+ allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
+ for key in allKeys:
+ setattr(self, key, max(getattr(table, key) for table in m.tables))
+ return True
+
+@_add_method(ttLib.getTableClass('post'))
+def merge(self, m):
+ # TODO Check that italicAngle, underlinePosition, underlineThickness are the same.
+ minMembers = ['underlinePosition', 'minMemType42', 'minMemType1']
+ # Negate some members
+ for key in minMembers:
+ for table in m.tables:
+ setattr(table, key, -getattr(table, key))
+ # Get max over members
+ allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
+ if 'mapping' in allKeys:
+ allKeys.remove('mapping')
+ allKeys.remove('extraNames')
+ for key in allKeys:
+ setattr(self, key, max(getattr(table, key) for table in m.tables))
+ # Negate them back
+ for key in minMembers:
+ for table in m.tables:
+ setattr(table, key, -getattr(table, key))
+ setattr(self, key, -getattr(self, key))
+ self.mapping = {}
+ for table in m.tables:
+ if hasattr(table, 'mapping'):
+ self.mapping.update(table.mapping)
+ self.extraNames = []
+ return True
+
+@_add_method(ttLib.getTableClass('vmtx'),
+ ttLib.getTableClass('hmtx'))
+def merge(self, m):
+ self.metrics = {}
+ for table in m.tables:
+ self.metrics.update(table.metrics)
+ return True
+
+@_add_method(ttLib.getTableClass('loca'))
+def merge(self, m):
+ return True # Will be computed automatically
+
+@_add_method(ttLib.getTableClass('glyf'))
+def merge(self, m):
+ self.glyphs = {}
+ for table in m.tables:
+ for g in table.glyphs.values():
+ # Drop hints for now, since we don't remap
+ # functions / CVT values.
+ g.removeHinting()
+ # Expand composite glyphs to load their
+ # composite glyph names.
+ if g.isComposite():
+ g.expand(table)
+ self.glyphs.update(table.glyphs)
+ return True
+
+@_add_method(ttLib.getTableClass('prep'),
+ ttLib.getTableClass('fpgm'),
+ ttLib.getTableClass('cvt '))
+def merge(self, m):
+ return False # TODO We don't merge hinting data currently.
+
+@_add_method(ttLib.getTableClass('cmap'))
+def merge(self, m):
+ # TODO Handle format=14.
+ cmapTables = [t for table in m.tables for t in table.tables
+ if t.platformID == 3 and t.platEncID in [1, 10]]
+ # TODO Better handle format-4 and format-12 coexisting in same font.
+ # TODO Insert both a format-4 and format-12 if needed.
+ module = ttLib.getTableModule('cmap')
+ assert all(t.format in [4, 12] for t in cmapTables)
+ format = max(t.format for t in cmapTables)
+ cmapTable = module.cmap_classes[format](format)
+ cmapTable.cmap = {}
+ cmapTable.platformID = 3
+ cmapTable.platEncID = max(t.platEncID for t in cmapTables)
+ cmapTable.language = 0
+ for table in cmapTables:
+ # TODO handle duplicates.
+ cmapTable.cmap.update(table.cmap)
+ self.tableVersion = 0
+ self.tables = [cmapTable]
+ self.numSubTables = len(self.tables)
+ return True
+
+@_add_method(ttLib.getTableClass('GDEF'))
+def merge(self, m):
+ self.table = otTables.GDEF()
+ self.table.Version = 1.0 # TODO version 1.2...
+
+ if any(t.table.LigCaretList for t in m.tables):
+ glyphs = []
+ ligGlyphs = []
+ for table in m.tables:
+ if table.table.LigCaretList:
+ glyphs.extend(table.table.LigCaretList.Coverage.glyphs)
+ ligGlyphs.extend(table.table.LigCaretList.LigGlyph)
+ coverage = otTables.Coverage()
+ coverage.glyphs = glyphs
+ ligCaretList = otTables.LigCaretList()
+ ligCaretList.Coverage = coverage
+ ligCaretList.LigGlyph = ligGlyphs
+ ligCaretList.GlyphCount = len(ligGlyphs)
+ self.table.LigCaretList = ligCaretList
+ else:
+ self.table.LigCaretList = None
+
+ if any(t.table.MarkAttachClassDef for t in m.tables):
+ classDefs = {}
+ for table in m.tables:
+ if table.table.MarkAttachClassDef:
+ classDefs.update(table.table.MarkAttachClassDef.classDefs)
+ self.table.MarkAttachClassDef = otTables.MarkAttachClassDef()
+ self.table.MarkAttachClassDef.classDefs = classDefs
+ else:
+ self.table.MarkAttachClassDef = None
+
+ if any(t.table.GlyphClassDef for t in m.tables):
+ classDefs = {}
+ for table in m.tables:
+ if table.table.GlyphClassDef:
+ classDefs.update(table.table.GlyphClassDef.classDefs)
+ self.table.GlyphClassDef = otTables.GlyphClassDef()
+ self.table.GlyphClassDef.classDefs = classDefs
+ else:
+ self.table.GlyphClassDef = None
+
+ if any(t.table.AttachList for t in m.tables):
+ glyphs = []
+ attachPoints = []
+ for table in m.tables:
+ if table.table.AttachList:
+ glyphs.extend(table.table.AttachList.Coverage.glyphs)
+ attachPoints.extend(table.table.AttachList.AttachPoint)
+ coverage = otTables.Coverage()
+ coverage.glyphs = glyphs
+ attachList = otTables.AttachList()
+ attachList.Coverage = coverage
+ attachList.AttachPoint = attachPoints
+ attachList.GlyphCount = len(attachPoints)
+ self.table.AttachList = attachList
+ else:
+ self.table.AttachList = None
+
+ return True
+
+
+class Options(object):
+
+ class UnknownOptionError(Exception):
+ pass
+
+ _drop_tables_default = ['fpgm', 'prep', 'cvt ', 'gasp']
+ drop_tables = _drop_tables_default
+
+ def __init__(self, **kwargs):
+
+ self.set(**kwargs)
+
+ def set(self, **kwargs):
+ for k,v in kwargs.iteritems():
+ if not hasattr(self, k):
+ raise self.UnknownOptionError("Unknown option '%s'" % k)
+ setattr(self, k, v)
+
+ def parse_opts(self, argv, ignore_unknown=False):
+ ret = []
+ opts = {}
+ for a in argv:
+ orig_a = a
+ if not a.startswith('--'):
+ ret.append(a)
+ continue
+ a = a[2:]
+ i = a.find('=')
+ op = '='
+ if i == -1:
+ if a.startswith("no-"):
+ k = a[3:]
+ v = False
+ else:
+ k = a
+ v = True
+ else:
+ k = a[:i]
+ if k[-1] in "-+":
+ op = k[-1]+'=' # Ops is '-=' or '+=' now.
+ k = k[:-1]
+ v = a[i+1:]
+ k = k.replace('-', '_')
+ if not hasattr(self, k):
+ if ignore_unknown == True or k in ignore_unknown:
+ ret.append(orig_a)
+ continue
+ else:
+ raise self.UnknownOptionError("Unknown option '%s'" % a)
+
+ ov = getattr(self, k)
+ if isinstance(ov, bool):
+ v = bool(v)
+ elif isinstance(ov, int):
+ v = int(v)
+ elif isinstance(ov, list):
+ vv = v.split(',')
+ if vv == ['']:
+ vv = []
+ vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
+ if op == '=':
+ v = vv
+ elif op == '+=':
+ v = ov
+ v.extend(vv)
+ elif op == '-=':
+ v = ov
+ for x in vv:
+ if x in v:
+ v.remove(x)
+ else:
+ assert 0
+
+ opts[k] = v
+ self.set(**opts)
+
+ return ret
+
+
+class Merger:
+
+ def __init__(self, options=None, log=None):
+
+ if not log:
+ log = Logger()
+ if not options:
+ options = Options()
+
+ self.options = options
+ self.log = log
+
+ def merge(self, fontfiles):
+
+ mega = ttLib.TTFont()
+
+ #
+ # Settle on a mega glyph order.
+ #
+ fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
+ glyphOrders = [font.getGlyphOrder() for font in fonts]
+ megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
+ # Reload fonts and set new glyph names on them.
+ # TODO Is it necessary to reload font? I think it is. At least
+ # it's safer, in case tables were loaded to provide glyph names.
+ fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
+ for font,glyphOrder in zip(fonts, glyphOrders):
+ font.setGlyphOrder(glyphOrder)
+ mega.setGlyphOrder(megaGlyphOrder)
+
+ allTags = reduce(set.union, (font.keys() for font in fonts), set())
+ allTags.remove('GlyphOrder')
+ for tag in allTags:
+
+ if tag in self.options.drop_tables:
+ self.log("Dropping '%s'." % tag)
+ continue
+
+ clazz = ttLib.getTableClass(tag)
+
+ if not hasattr(clazz, 'merge'):
+ self.log("Don't know how to merge '%s', dropped." % tag)
+ continue
+
+ # TODO For now assume all fonts have the same tables.
+ self.tables = [font[tag] for font in fonts]
+ table = clazz(tag)
+ if table.merge (self):
+ mega[tag] = table
+ self.log("Merged '%s'." % tag)
+ else:
+ self.log("Dropped '%s'. No need to merge explicitly." % tag)
+ self.log.lapse("merge '%s'" % tag)
+ del self.tables
+
+ return mega
+
+ def _mergeGlyphOrders(self, glyphOrders):
+ """Modifies passed-in glyphOrders to reflect new glyph names.
+ Returns glyphOrder for the merged font."""
+ # Simply append font index to the glyph name for now.
+ # TODO Even this simplistic numbering can result in conflicts.
+ # But then again, we have to improve this soon anyway.
+ mega = []
+ for n,glyphOrder in enumerate(glyphOrders):
+ for i,glyphName in enumerate(glyphOrder):
+ glyphName += "#" + `n`
+ glyphOrder[i] = glyphName
+ mega.append(glyphName)
+ return mega
+
+
+class Logger(object):
+
+ def __init__(self, verbose=False, xml=False, timing=False):
+ self.verbose = verbose
+ self.xml = xml
+ self.timing = timing
+ self.last_time = self.start_time = time.time()
+
+ def parse_opts(self, argv):
+ argv = argv[:]
+ for v in ['verbose', 'xml', 'timing']:
+ if "--"+v in argv:
+ setattr(self, v, True)
+ argv.remove("--"+v)
+ return argv
+
+ def __call__(self, *things):
+ if not self.verbose:
+ return
+ print ' '.join(str(x) for x in things)
+
+ def lapse(self, *things):
+ if not self.timing:
+ return
+ new_time = time.time()
+ print "Took %0.3fs to %s" %(new_time - self.last_time,
+ ' '.join(str(x) for x in things))
+ self.last_time = new_time
+
+ def font(self, font, file=sys.stdout):
+ if not self.xml:
+ return
+ from fontTools.misc import xmlWriter
+ writer = xmlWriter.XMLWriter(file)
+ font.disassembleInstructions = False # Work around ttLib bug
+ for tag in font.keys():
+ writer.begintag(tag)
+ writer.newline()
+ font[tag].toXML(writer, font)
+ writer.endtag(tag)
+ writer.newline()
+
+
+__all__ = [
+ 'Options',
+ 'Merger',
+ 'Logger',
+ 'main'
+]
+
+def main(args):
+
+ log = Logger()
+ args = log.parse_opts(args)
+
+ options = Options()
+ args = options.parse_opts(args)
+
+ if len(args) < 1:
+ print >>sys.stderr, "usage: pyftmerge font..."
+ sys.exit(1)
+
+ merger = Merger(options=options, log=log)
+ font = merger.merge(args)
+ outfile = 'merged.ttf'
+ font.save(outfile)
+ log.lapse("compile and save font")
+
+ log.last_time = log.start_time
+ log.lapse("make one with everything(TOTAL TIME)")
+
+if __name__ == "__main__":
+ main(sys.argv[1:])
diff --git a/Lib/fontTools/subset.py b/Lib/fontTools/subset.py
index a3b9f70..8ce37f2 100644
--- a/Lib/fontTools/subset.py
+++ b/Lib/fontTools/subset.py
@@ -2161,8 +2161,8 @@
for g in args))
font = load_font(fontfile, options, dontLoadGlyphNames=dontLoadGlyphNames)
- subsetter = Subsetter(options=options, log=log)
log.lapse("load font")
+ subsetter = Subsetter(options=options, log=log)
names = font.getGlyphNames()
log.lapse("loading glyph names")
diff --git a/Tools/pyftmerge b/Tools/pyftmerge
new file mode 100755
index 0000000..2479258
--- /dev/null
+++ b/Tools/pyftmerge
@@ -0,0 +1,6 @@
+#! /usr/bin/env python
+
+import sys
+from fontTools import merge
+
+merge.main(sys.argv[1:])