merge.py revision 14f13a993eda70fd97e415cc9a043d5d2416c187
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
5"""Font merger.
6"""
7
8from __future__ import print_function, division, absolute_import
9from fontTools.misc.py23 import *
10from fontTools import ttLib, cffLib
11from fontTools.ttLib.tables import otTables, _h_e_a_d
12from fontTools.ttLib.tables.DefaultTable import DefaultTable
13from functools import reduce
14import sys
15import time
16import operator
17
18
19def _add_method(*clazzes, **kwargs):
20	"""Returns a decorator function that adds a new method to one or
21	more classes."""
22	allowDefault = kwargs.get('allowDefaultTable', False)
23	def wrapper(method):
24		for clazz in clazzes:
25			assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.'
26			assert method.__name__ not in clazz.__dict__, \
27				"Oops, class '%s' has method '%s'." % (clazz.__name__,
28								       method.__name__)
29			setattr(clazz, method.__name__, method)
30		return None
31	return wrapper
32
33# General utility functions for merging values from different fonts
34
35def equal(lst):
36	lst = list(lst)
37	t = iter(lst)
38	first = next(t)
39	assert all(item == first for item in t), "Expected all items to be equal: %s" % lst
40	return first
41
42def first(lst):
43	return next(iter(lst))
44
45def recalculate(lst):
46	return NotImplemented
47
48def current_time(lst):
49	return int(time.time() - _h_e_a_d.mac_epoch_diff)
50
51def bitwise_and(lst):
52	return reduce(operator.and_, lst)
53
54def bitwise_or(lst):
55	return reduce(operator.or_, lst)
56
57def avg_int(lst):
58	lst = list(lst)
59	return sum(lst) // len(lst)
60
61def onlyExisting(func):
62	"""Returns a filter func that when called with a list,
63	only calls func on the non-NotImplemented items of the list,
64	and only so if there's at least one item remaining.
65	Otherwise returns NotImplemented."""
66
67	def wrapper(lst):
68		items = [item for item in lst if item is not NotImplemented]
69		return func(items) if items else NotImplemented
70
71	return wrapper
72
73def sumLists(lst):
74	l = []
75	for item in lst:
76		l.extend(item)
77	return l
78
79def sumDicts(lst):
80	d = {}
81	for item in lst:
82		d.update(item)
83	return d
84
85def mergeObjects(lst):
86	lst = [item for item in lst if item is not NotImplemented]
87	if not lst:
88		return NotImplemented
89	lst = [item for item in lst if item is not None]
90	if not lst:
91		return None
92
93	clazz = lst[0].__class__
94	assert all(type(item) == clazz for item in lst), lst
95
96	logic = clazz.mergeMap
97	returnTable = clazz()
98	returnDict = {}
99
100	allKeys = set.union(set(), *(vars(table).keys() for table in lst))
101	for key in allKeys:
102		try:
103			mergeLogic = logic[key]
104		except KeyError:
105			try:
106				mergeLogic = logic['*']
107			except KeyError:
108				raise Exception("Don't know how to merge key %s of class %s" %
109						(key, clazz.__name__))
110		if mergeLogic is NotImplemented:
111			continue
112		value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
113		if value is not NotImplemented:
114			returnDict[key] = value
115
116	returnTable.__dict__ = returnDict
117
118	return returnTable
119
120def mergeBits(bitmap):
121
122	def wrapper(lst):
123		lst = list(lst)
124		returnValue = 0
125		for bitNumber in range(bitmap['size']):
126			try:
127				mergeLogic = bitmap[bitNumber]
128			except KeyError:
129				try:
130					mergeLogic = bitmap['*']
131				except KeyError:
132					raise Exception("Don't know how to merge bit %s" % bitNumber)
133			shiftedBit = 1 << bitNumber
134			mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst)
135			returnValue |= mergedValue << bitNumber
136		return returnValue
137
138	return wrapper
139
140
141@_add_method(DefaultTable, allowDefaultTable=True)
142def merge(self, m, tables):
143	if not hasattr(self, 'mergeMap'):
144		m.log("Don't know how to merge '%s'." % self.tableTag)
145		return NotImplemented
146
147	logic = self.mergeMap
148
149	if isinstance(logic, dict):
150		return m.mergeObjects(self, self.mergeMap, tables)
151	else:
152		return logic(tables)
153
154
155ttLib.getTableClass('maxp').mergeMap = {
156	'*': max,
157	'tableTag': equal,
158	'tableVersion': equal,
159	'numGlyphs': sum,
160	'maxStorage': first,
161	'maxFunctionDefs': first,
162	'maxInstructionDefs': first,
163	# TODO When we correctly merge hinting data, update these values:
164	# maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
165}
166
167headFlagsMergeBitMap = {
168	'size': 16,
169	'*': bitwise_or,
170	1: bitwise_and, # Baseline at y = 0
171	2: bitwise_and, # lsb at x = 0
172	3: bitwise_and, # Force ppem to integer values. FIXME?
173	5: bitwise_and, # Font is vertical
174	6: lambda bit: 0, # Always set to zero
175	11: bitwise_and, # Font data is 'lossless'
176	13: bitwise_and, # Optimized for ClearType
177	14: bitwise_and, # Last resort font. FIXME? equal or first may be better
178	15: lambda bit: 0, # Always set to zero
179}
180
181ttLib.getTableClass('head').mergeMap = {
182	'tableTag': equal,
183	'tableVersion': max,
184	'fontRevision': max,
185	'checkSumAdjustment': lambda lst: 0, # We need *something* here
186	'magicNumber': equal,
187	'flags': mergeBits(headFlagsMergeBitMap),
188	'unitsPerEm': equal,
189	'created': current_time,
190	'modified': current_time,
191	'xMin': min,
192	'yMin': min,
193	'xMax': max,
194	'yMax': max,
195	'macStyle': first,
196	'lowestRecPPEM': max,
197	'fontDirectionHint': lambda lst: 2,
198	'indexToLocFormat': recalculate,
199	'glyphDataFormat': equal,
200}
201
202ttLib.getTableClass('hhea').mergeMap = {
203	'*': equal,
204	'tableTag': equal,
205	'tableVersion': max,
206	'ascent': max,
207	'descent': min,
208	'lineGap': max,
209	'advanceWidthMax': max,
210	'minLeftSideBearing': min,
211	'minRightSideBearing': min,
212	'xMaxExtent': max,
213	'caretSlopeRise': first,
214	'caretSlopeRun': first,
215	'caretOffset': first,
216	'numberOfHMetrics': recalculate,
217}
218
219os2FsTypeMergeBitMap = {
220	'size': 16,
221	'*': lambda bit: 0,
222	1: bitwise_or, # no embedding permitted
223	2: bitwise_and, # allow previewing and printing documents
224	3: bitwise_and, # allow editing documents
225	8: bitwise_or, # no subsetting permitted
226	9: bitwise_or, # no embedding of outlines permitted
227}
228
229def mergeOs2FsType(lst):
230	lst = list(lst)
231	if all(item == 0 for item in lst):
232		return 0
233
234	# Compute least restrictive logic for each fsType value
235	for i in range(len(lst)):
236		# unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
237		if lst[i] & 0x000C:
238			lst[i] &= ~0x0002
239		# set bit 2 (allow previewing) if bit 3 is set (allow editing)
240		elif lst[i] & 0x0008:
241			lst[i] |= 0x0004
242		# set bits 2 and 3 if everything is allowed
243		elif lst[i] == 0:
244			lst[i] = 0x000C
245
246	fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
247	# unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
248	if fsType & 0x0002:
249		fsType &= ~0x000C
250	return fsType
251
252
253ttLib.getTableClass('OS/2').mergeMap = {
254	'*': first,
255	'tableTag': equal,
256	'version': max,
257	'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
258	'fsType': mergeOs2FsType, # Will be overwritten
259	'panose': first, # FIXME: should really be the first Latin font
260	'ulUnicodeRange1': bitwise_or,
261	'ulUnicodeRange2': bitwise_or,
262	'ulUnicodeRange3': bitwise_or,
263	'ulUnicodeRange4': bitwise_or,
264	'fsFirstCharIndex': min,
265	'fsLastCharIndex': max,
266	'sTypoAscender': max,
267	'sTypoDescender': min,
268	'sTypoLineGap': max,
269	'usWinAscent': max,
270	'usWinDescent': max,
271	# Version 2,3,4
272	'ulCodePageRange1': onlyExisting(bitwise_or),
273	'ulCodePageRange2': onlyExisting(bitwise_or),
274	'usMaxContex': onlyExisting(max),
275	# TODO version 5
276}
277
278@_add_method(ttLib.getTableClass('OS/2'))
279def merge(self, m, tables):
280	DefaultTable.merge(self, m, tables)
281	if self.version < 2:
282		# bits 8 and 9 are reserved and should be set to zero
283		self.fsType &= ~0x0300
284	if self.version >= 3:
285		# Only one of bits 1, 2, and 3 may be set. We already take
286		# care of bit 1 implications in mergeOs2FsType. So unset
287		# bit 2 if bit 3 is already set.
288		if self.fsType & 0x0008:
289			self.fsType &= ~0x0004
290	return self
291
292ttLib.getTableClass('post').mergeMap = {
293	'*': first,
294	'tableTag': equal,
295	'formatType': max,
296	'isFixedPitch': min,
297	'minMemType42': max,
298	'maxMemType42': lambda lst: 0,
299	'minMemType1': max,
300	'maxMemType1': lambda lst: 0,
301	'mapping': onlyExisting(sumDicts),
302	'extraNames': lambda lst: [],
303}
304
305ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
306	'tableTag': equal,
307	'metrics': sumDicts,
308}
309
310ttLib.getTableClass('gasp').mergeMap = {
311	'tableTag': equal,
312	'version': max,
313	'gaspRange': first, # FIXME? Appears irreconcilable
314}
315
316ttLib.getTableClass('name').mergeMap = {
317	'tableTag': equal,
318	'names': first, # FIXME? Does mixing name records make sense?
319}
320
321ttLib.getTableClass('loca').mergeMap = {
322	'*': recalculate,
323	'tableTag': equal,
324}
325
326ttLib.getTableClass('glyf').mergeMap = {
327	'tableTag': equal,
328	'glyphs': sumDicts,
329	'glyphOrder': sumLists,
330}
331
332@_add_method(ttLib.getTableClass('glyf'))
333def merge(self, m, tables):
334	for i,table in enumerate(tables):
335		for g in table.glyphs.values():
336			if i:
337				# Drop hints for all but first font, since
338				# we don't map functions / CVT values.
339				g.removeHinting()
340			# Expand composite glyphs to load their
341			# composite glyph names.
342			if g.isComposite():
343				g.expand(table)
344	return DefaultTable.merge(self, m, tables)
345
346ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
347ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
348ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
349
350@_add_method(ttLib.getTableClass('cmap'))
351def merge(self, m, tables):
352	# TODO Handle format=14.
353	cmapTables = [(t,fontIdx) for fontIdx,table in enumerate(tables) for t in table.tables
354		      if t.isUnicode()]
355	# TODO Better handle format-4 and format-12 coexisting in same font.
356	# TODO Insert both a format-4 and format-12 if needed.
357	module = ttLib.getTableModule('cmap')
358	assert all(t.format in [4, 12] for t,_ in cmapTables)
359	format = max(t.format for t,_ in cmapTables)
360	cmapTable = module.cmap_classes[format](format)
361	cmapTable.cmap = {}
362	cmapTable.platformID = 3
363	cmapTable.platEncID = max(t.platEncID for t,_ in cmapTables)
364	cmapTable.language = 0
365	cmap = cmapTable.cmap
366	for table,fontIdx in cmapTables:
367		# TODO handle duplicates.
368		for uni,gid in table.cmap.items():
369			oldgid = cmap.get(uni, None)
370			if oldgid is None:
371				cmap[uni] = gid
372			elif oldgid != gid:
373				# Char previously mapped to oldgid, now to gid.
374				# Record, to fix up in GSUB 'locl' later.
375				assert m.duplicateGlyphsPerFont[fontIdx].get(oldgid, gid) == gid
376				m.duplicateGlyphsPerFont[fontIdx][oldgid] = gid
377	self.tableVersion = 0
378	self.tables = [cmapTable]
379	self.numSubTables = len(self.tables)
380	return self
381
382
383otTables.ScriptList.mergeMap = {
384	'ScriptCount': sum,
385	'ScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.ScriptTag),
386}
387
388otTables.FeatureList.mergeMap = {
389	'FeatureCount': sum,
390	'FeatureRecord': sumLists,
391}
392
393otTables.LookupList.mergeMap = {
394	'LookupCount': sum,
395	'Lookup': sumLists,
396}
397
398otTables.Coverage.mergeMap = {
399	'glyphs': sumLists,
400}
401
402otTables.ClassDef.mergeMap = {
403	'classDefs': sumDicts,
404}
405
406otTables.LigCaretList.mergeMap = {
407	'Coverage': mergeObjects,
408	'LigGlyphCount': sum,
409	'LigGlyph': sumLists,
410}
411
412otTables.AttachList.mergeMap = {
413	'Coverage': mergeObjects,
414	'GlyphCount': sum,
415	'AttachPoint': sumLists,
416}
417
418# XXX Renumber MarkFilterSets of lookups
419otTables.MarkGlyphSetsDef.mergeMap = {
420	'MarkSetTableFormat': equal,
421	'MarkSetCount': sum,
422	'Coverage': sumLists,
423}
424
425otTables.GDEF.mergeMap = {
426	'*': mergeObjects,
427	'Version': max,
428}
429
430otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
431	'*': mergeObjects,
432	'Version': max,
433}
434
435ttLib.getTableClass('GDEF').mergeMap = \
436ttLib.getTableClass('GSUB').mergeMap = \
437ttLib.getTableClass('GPOS').mergeMap = \
438ttLib.getTableClass('BASE').mergeMap = \
439ttLib.getTableClass('JSTF').mergeMap = \
440ttLib.getTableClass('MATH').mergeMap = \
441{
442	'tableTag': onlyExisting(equal), # XXX clean me up
443	'table': mergeObjects,
444}
445
446@_add_method(ttLib.getTableClass('GSUB'))
447def merge(self, m, tables):
448
449	assert len(tables) == len(m.duplicateGlyphsPerFont)
450	for i,(table,dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)):
451		if not dups: continue
452		assert (table is not None and table is not NotImplemented), "Have duplicates to resolve for font %d but no GSUB" % (i + 1)
453		lookupMap = dict((id(v),v) for v in table.table.LookupList.Lookup)
454		featureMap = dict((id(v),v) for v in table.table.FeatureList.FeatureRecord)
455		synthFeature = None
456		synthLookup = None
457		for script in table.table.ScriptList.ScriptRecord:
458			if script.ScriptTag == 'DFLT': continue # XXX
459			for langsys in [script.Script.DefaultLangSys] + [l.LangSys for l in script.Script.LangSysRecord]:
460				feature = [featureMap[v] for v in langsys.FeatureIndex if featureMap[v].FeatureTag == 'locl']
461				assert len(feature) <= 1
462				if feature:
463					feature = feature[0]
464				else:
465					if not synthFeature:
466						synthFeature = otTables.FeatureRecord()
467						synthFeature.FeatureTag = 'locl'
468						f = synthFeature.Feature = otTables.Feature()
469						f.FeatureParams = None
470						f.LookupCount = 0
471						f.LookupListIndex = []
472						langsys.FeatureIndex.append(id(synthFeature))
473						featureMap[id(synthFeature)] = synthFeature
474						langsys.FeatureIndex.sort(key=lambda v: featureMap[v].FeatureTag)
475						table.table.FeatureList.FeatureRecord.append(synthFeature)
476						table.table.FeatureList.FeatureCount += 1
477					feature = synthFeature
478
479				if not synthLookup:
480					subtable = otTables.SingleSubst()
481					subtable.mapping = dups
482					synthLookup = otTables.Lookup()
483					synthLookup.LookupFlag = 0
484					synthLookup.LookupType = 1
485					synthLookup.SubTableCount = 1
486					synthLookup.SubTable = [subtable]
487					table.table.LookupList.Lookup.append(synthLookup)
488					table.table.LookupList.LookupCount += 1
489
490				feature.Feature.LookupListIndex[:0] = [id(synthLookup)]
491				feature.Feature.LookupCount += 1
492
493
494	DefaultTable.merge(self, m, tables)
495	return self
496
497
498
499@_add_method(otTables.SingleSubst,
500             otTables.MultipleSubst,
501             otTables.AlternateSubst,
502             otTables.LigatureSubst,
503             otTables.ReverseChainSingleSubst,
504             otTables.SinglePos,
505             otTables.PairPos,
506             otTables.CursivePos,
507             otTables.MarkBasePos,
508             otTables.MarkLigPos,
509             otTables.MarkMarkPos)
510def mapLookups(self, lookupMap):
511  pass
512
513# Copied and trimmed down from subset.py
514@_add_method(otTables.ContextSubst,
515             otTables.ChainContextSubst,
516             otTables.ContextPos,
517             otTables.ChainContextPos)
518def __classify_context(self):
519
520  class ContextHelper(object):
521    def __init__(self, klass, Format):
522      if klass.__name__.endswith('Subst'):
523        Typ = 'Sub'
524        Type = 'Subst'
525      else:
526        Typ = 'Pos'
527        Type = 'Pos'
528      if klass.__name__.startswith('Chain'):
529        Chain = 'Chain'
530      else:
531        Chain = ''
532      ChainTyp = Chain+Typ
533
534      self.Typ = Typ
535      self.Type = Type
536      self.Chain = Chain
537      self.ChainTyp = ChainTyp
538
539      self.LookupRecord = Type+'LookupRecord'
540
541      if Format == 1:
542        self.Rule = ChainTyp+'Rule'
543        self.RuleSet = ChainTyp+'RuleSet'
544      elif Format == 2:
545        self.Rule = ChainTyp+'ClassRule'
546        self.RuleSet = ChainTyp+'ClassSet'
547
548  if self.Format not in [1, 2, 3]:
549    return None  # Don't shoot the messenger; let it go
550  if not hasattr(self.__class__, "__ContextHelpers"):
551    self.__class__.__ContextHelpers = {}
552  if self.Format not in self.__class__.__ContextHelpers:
553    helper = ContextHelper(self.__class__, self.Format)
554    self.__class__.__ContextHelpers[self.Format] = helper
555  return self.__class__.__ContextHelpers[self.Format]
556
557
558@_add_method(otTables.ContextSubst,
559             otTables.ChainContextSubst,
560             otTables.ContextPos,
561             otTables.ChainContextPos)
562def mapLookups(self, lookupMap):
563  c = self.__classify_context()
564
565  if self.Format in [1, 2]:
566    for rs in getattr(self, c.RuleSet):
567      if not rs: continue
568      for r in getattr(rs, c.Rule):
569        if not r: continue
570        for ll in getattr(r, c.LookupRecord):
571          if not ll: continue
572          ll.LookupListIndex = lookupMap[ll.LookupListIndex]
573  elif self.Format == 3:
574    for ll in getattr(self, c.LookupRecord):
575      if not ll: continue
576      ll.LookupListIndex = lookupMap[ll.LookupListIndex]
577  else:
578    assert 0, "unknown format: %s" % self.Format
579
580@_add_method(otTables.Lookup)
581def mapLookups(self, lookupMap):
582	for st in self.SubTable:
583		if not st: continue
584		st.mapLookups(lookupMap)
585
586@_add_method(otTables.LookupList)
587def mapLookups(self, lookupMap):
588	for l in self.Lookup:
589		if not l: continue
590		l.mapLookups(lookupMap)
591
592@_add_method(otTables.Feature)
593def mapLookups(self, lookupMap):
594	self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
595
596@_add_method(otTables.FeatureList)
597def mapLookups(self, lookupMap):
598	for f in self.FeatureRecord:
599		if not f or not f.Feature: continue
600		f.Feature.mapLookups(lookupMap)
601
602@_add_method(otTables.DefaultLangSys,
603             otTables.LangSys)
604def mapFeatures(self, featureMap):
605	self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
606	if self.ReqFeatureIndex != 65535:
607		self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
608
609@_add_method(otTables.Script)
610def mapFeatures(self, featureMap):
611	if self.DefaultLangSys:
612		self.DefaultLangSys.mapFeatures(featureMap)
613	for l in self.LangSysRecord:
614		if not l or not l.LangSys: continue
615		l.LangSys.mapFeatures(featureMap)
616
617@_add_method(otTables.ScriptList)
618def mapFeatures(self, featureMap):
619	for s in self.ScriptRecord:
620		if not s or not s.Script: continue
621		s.Script.mapFeatures(featureMap)
622
623
624class Options(object):
625
626  class UnknownOptionError(Exception):
627    pass
628
629  def __init__(self, **kwargs):
630
631    self.set(**kwargs)
632
633  def set(self, **kwargs):
634    for k,v in kwargs.items():
635      if not hasattr(self, k):
636        raise self.UnknownOptionError("Unknown option '%s'" % k)
637      setattr(self, k, v)
638
639  def parse_opts(self, argv, ignore_unknown=False):
640    ret = []
641    opts = {}
642    for a in argv:
643      orig_a = a
644      if not a.startswith('--'):
645        ret.append(a)
646        continue
647      a = a[2:]
648      i = a.find('=')
649      op = '='
650      if i == -1:
651        if a.startswith("no-"):
652          k = a[3:]
653          v = False
654        else:
655          k = a
656          v = True
657      else:
658        k = a[:i]
659        if k[-1] in "-+":
660          op = k[-1]+'='  # Ops is '-=' or '+=' now.
661          k = k[:-1]
662        v = a[i+1:]
663      k = k.replace('-', '_')
664      if not hasattr(self, k):
665        if ignore_unknown == True or k in ignore_unknown:
666          ret.append(orig_a)
667          continue
668        else:
669          raise self.UnknownOptionError("Unknown option '%s'" % a)
670
671      ov = getattr(self, k)
672      if isinstance(ov, bool):
673        v = bool(v)
674      elif isinstance(ov, int):
675        v = int(v)
676      elif isinstance(ov, list):
677        vv = v.split(',')
678        if vv == ['']:
679          vv = []
680        vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
681        if op == '=':
682          v = vv
683        elif op == '+=':
684          v = ov
685          v.extend(vv)
686        elif op == '-=':
687          v = ov
688          for x in vv:
689            if x in v:
690              v.remove(x)
691        else:
692          assert 0
693
694      opts[k] = v
695    self.set(**opts)
696
697    return ret
698
699
700class Merger(object):
701
702	def __init__(self, options=None, log=None):
703
704		if not log:
705			log = Logger()
706		if not options:
707			options = Options()
708
709		self.options = options
710		self.log = log
711
712	def merge(self, fontfiles):
713
714		mega = ttLib.TTFont()
715
716		#
717		# Settle on a mega glyph order.
718		#
719		fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
720		glyphOrders = [font.getGlyphOrder() for font in fonts]
721		megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
722		# Reload fonts and set new glyph names on them.
723		# TODO Is it necessary to reload font?  I think it is.  At least
724		# it's safer, in case tables were loaded to provide glyph names.
725		fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
726		for font,glyphOrder in zip(fonts, glyphOrders):
727			font.setGlyphOrder(glyphOrder)
728		mega.setGlyphOrder(megaGlyphOrder)
729
730		for font in fonts:
731			self._preMerge(font)
732
733		self.duplicateGlyphsPerFont = [{} for f in fonts]
734
735		allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
736		allTags.remove('GlyphOrder')
737		allTags.remove('cmap')
738		allTags.remove('GSUB')
739		allTags = ['cmap', 'GSUB'] + list(allTags)
740		for tag in allTags:
741
742			tables = [font.get(tag, NotImplemented) for font in fonts]
743
744			clazz = ttLib.getTableClass(tag)
745			table = clazz(tag).merge(self, tables)
746			# XXX Clean this up and use:  table = mergeObjects(tables)
747
748			if table is not NotImplemented and table is not False:
749				mega[tag] = table
750				self.log("Merged '%s'." % tag)
751			else:
752				self.log("Dropped '%s'." % tag)
753			self.log.lapse("merge '%s'" % tag)
754
755		del self.duplicateGlyphsPerFont
756
757		self._postMerge(mega)
758
759		return mega
760
761	def _mergeGlyphOrders(self, glyphOrders):
762		"""Modifies passed-in glyphOrders to reflect new glyph names.
763		Returns glyphOrder for the merged font."""
764		# Simply append font index to the glyph name for now.
765		# TODO Even this simplistic numbering can result in conflicts.
766		# But then again, we have to improve this soon anyway.
767		mega = []
768		for n,glyphOrder in enumerate(glyphOrders):
769			for i,glyphName in enumerate(glyphOrder):
770				glyphName += "#" + repr(n)
771				glyphOrder[i] = glyphName
772				mega.append(glyphName)
773		return mega
774
775	def mergeObjects(self, returnTable, logic, tables):
776		# Right now we don't use self at all.  Will use in the future
777		# for options and logging.
778
779		allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
780		for key in allKeys:
781			try:
782				mergeLogic = logic[key]
783			except KeyError:
784				try:
785					mergeLogic = logic['*']
786				except KeyError:
787					raise Exception("Don't know how to merge key %s of class %s" %
788							(key, returnTable.__class__.__name__))
789			if mergeLogic is NotImplemented:
790				continue
791			value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
792			if value is not NotImplemented:
793				setattr(returnTable, key, value)
794
795		return returnTable
796
797	def _preMerge(self, font):
798
799		# Map indices to references
800
801		GDEF = font.get('GDEF')
802		GSUB = font.get('GSUB')
803		GPOS = font.get('GPOS')
804
805		for t in [GSUB, GPOS]:
806			if not t: continue
807
808			if t.table.LookupList:
809				lookupMap = dict((i,id(v)) for i,v in enumerate(t.table.LookupList.Lookup))
810				t.table.LookupList.mapLookups(lookupMap)
811				if t.table.FeatureList:
812					# XXX Handle present FeatureList but absent LookupList
813					t.table.FeatureList.mapLookups(lookupMap)
814
815			if t.table.FeatureList and t.table.ScriptList:
816				featureMap = dict((i,id(v)) for i,v in enumerate(t.table.FeatureList.FeatureRecord))
817				t.table.ScriptList.mapFeatures(featureMap)
818
819		# TODO GDEF/Lookup MarkFilteringSets
820		# TODO FeatureParams nameIDs
821
822	def _postMerge(self, font):
823
824		# Map references back to indices
825
826		GDEF = font.get('GDEF')
827		GSUB = font.get('GSUB')
828		GPOS = font.get('GPOS')
829
830		for t in [GSUB, GPOS]:
831			if not t: continue
832
833			if t.table.LookupList:
834				lookupMap = dict((id(v),i) for i,v in enumerate(t.table.LookupList.Lookup))
835				t.table.LookupList.mapLookups(lookupMap)
836				if t.table.FeatureList:
837					# XXX Handle present FeatureList but absent LookupList
838					t.table.FeatureList.mapLookups(lookupMap)
839
840			if t.table.FeatureList and t.table.ScriptList:
841				# XXX Handle present ScriptList but absent FeatureList
842				featureMap = dict((id(v),i) for i,v in enumerate(t.table.FeatureList.FeatureRecord))
843				t.table.ScriptList.mapFeatures(featureMap)
844
845		# TODO GDEF/Lookup MarkFilteringSets
846		# TODO FeatureParams nameIDs
847
848
849class Logger(object):
850
851  def __init__(self, verbose=False, xml=False, timing=False):
852    self.verbose = verbose
853    self.xml = xml
854    self.timing = timing
855    self.last_time = self.start_time = time.time()
856
857  def parse_opts(self, argv):
858    argv = argv[:]
859    for v in ['verbose', 'xml', 'timing']:
860      if "--"+v in argv:
861        setattr(self, v, True)
862        argv.remove("--"+v)
863    return argv
864
865  def __call__(self, *things):
866    if not self.verbose:
867      return
868    print(' '.join(str(x) for x in things))
869
870  def lapse(self, *things):
871    if not self.timing:
872      return
873    new_time = time.time()
874    print("Took %0.3fs to %s" %(new_time - self.last_time,
875                                 ' '.join(str(x) for x in things)))
876    self.last_time = new_time
877
878  def font(self, font, file=sys.stdout):
879    if not self.xml:
880      return
881    from fontTools.misc import xmlWriter
882    writer = xmlWriter.XMLWriter(file)
883    font.disassembleInstructions = False  # Work around ttLib bug
884    for tag in font.keys():
885      writer.begintag(tag)
886      writer.newline()
887      font[tag].toXML(writer, font)
888      writer.endtag(tag)
889      writer.newline()
890
891
892__all__ = [
893  'Options',
894  'Merger',
895  'Logger',
896  'main'
897]
898
899def main(args):
900
901	log = Logger()
902	args = log.parse_opts(args)
903
904	options = Options()
905	args = options.parse_opts(args)
906
907	if len(args) < 1:
908		print("usage: pyftmerge font...", file=sys.stderr)
909		sys.exit(1)
910
911	merger = Merger(options=options, log=log)
912	font = merger.merge(args)
913	outfile = 'merged.ttf'
914	font.save(outfile)
915	log.lapse("compile and save font")
916
917	log.last_time = log.start_time
918	log.lapse("make one with everything(TOTAL TIME)")
919
920if __name__ == "__main__":
921	main(sys.argv[1:])
922