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