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