460 lines
13 KiB
Python
460 lines
13 KiB
Python
# Copyright 2013 Google, Inc. All Rights Reserved.
|
|
#
|
|
# Google Author(s): Behdad Esfahbod
|
|
|
|
"""Font merger.
|
|
"""
|
|
|
|
from __future__ import print_function, division
|
|
from fontTools.misc.py23 import *
|
|
from fontTools import ttLib, cffLib
|
|
from fontTools.ttLib.tables import otTables
|
|
from functools import reduce
|
|
import sys
|
|
import time
|
|
|
|
|
|
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.__name__), \
|
|
"Oops, class '%s' has method '%s'." % (clazz.__name__,
|
|
method.__name__)
|
|
setattr(clazz, method.__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, (list(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, (list(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, (list(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, (list(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, (list(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.items():
|
|
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, (list(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 += "#" + repr(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("usage: pyftmerge font...", file=sys.stderr)
|
|
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:])
|