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