blob: c74410845929b725f499bf9536748643042f05dd [file] [log] [blame]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -04001# Copyright 2013 Google, Inc. All Rights Reserved.
2#
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -08003# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
Behdad Esfahbod45d2f382013-09-18 20:47:53 -04004
5"""Font merger.
6"""
7
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -05008from __future__ import print_function, division
9from fontTools.misc.py23 import *
10from fontTools import ttLib, cffLib
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080011from fontTools.ttLib.tables import otTables, _h_e_a_d
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050012from fontTools.ttLib.tables.DefaultTable import DefaultTable
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -050013from functools import reduce
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040014import sys
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040015import time
Behdad Esfahbod49028b32013-12-18 17:34:17 -050016import operator
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040017
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040018
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050019def _add_method(*clazzes, **kwargs):
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040020 """Returns a decorator function that adds a new method to one or
21 more classes."""
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -050022 allowDefault = kwargs.get('allowDefaultTable', False)
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040023 def wrapper(method):
24 for clazz in clazzes:
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -050025 if not allowDefault:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050026 assert clazz != DefaultTable, 'Oops, table class not found.'
27 assert method.__name__ not in clazz.__dict__, \
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040028 "Oops, class '%s' has method '%s'." % (clazz.__name__,
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -050029 method.__name__)
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050030 setattr(clazz, method.__name__, method)
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040031 return None
32 return wrapper
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040033
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080034# General utility functions for merging values from different fonts
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050035
Behdad Esfahbod49028b32013-12-18 17:34:17 -050036def equal(lst):
37 t = iter(lst)
38 first = next(t)
39 assert all(item == first for item in t)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080040 return first
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080041
42def first(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050043 return next(iter(lst))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080044
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080045def recalculate(lst):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050046 return NotImplemented
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080047
48def current_time(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050049 return int(time.time() - _h_e_a_d.mac_epoch_diff)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080050
51def bitwise_or(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050052 return reduce(operator.or_, lst)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080053
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050054def avg_int(lst):
55 lst = list(lst)
56 return sum(lst) // len(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040057
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050058def nonnone(func):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050059 """Returns a filter func that when called with a list,
60 only calls func on the non-None items of the list, and
61 only so if there's at least one non-None item in the
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050062 list. Otherwise returns None."""
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050063
64 def wrapper(lst):
65 items = [item for item in lst if item is not None]
66 return func(items) if items else None
67
68 return wrapper
69
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050070def implemented(func):
71 """Returns a filter func that when called with a list,
72 only calls func on the non-NotImplemented items of the list,
73 and only so if there's at least one item remaining.
74 Otherwise returns NotImplemented."""
75
76 def wrapper(lst):
77 items = [item for item in lst if item is not NotImplemented]
78 return func(items) if items else NotImplemented
79
80 return wrapper
81
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050082def sumLists(lst):
83 l = []
84 for item in lst:
85 l.extend(item)
86 return l
87
88def sumDicts(lst):
89 d = {}
90 for item in lst:
91 d.update(item)
92 return d
93
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050094def mergeObjects(lst):
95 lst = [item for item in lst if item is not None and item is not NotImplemented]
96 if not lst:
97 return None # Not all can be NotImplemented
98
99 clazz = lst[0].__class__
100 assert all(type(item) == clazz for item in lst), lst
101 logic = clazz.mergeMap
102 returnTable = clazz()
103
104 allKeys = set.union(set(), *(vars(table).keys() for table in lst))
105 for key in allKeys:
106 try:
107 mergeLogic = logic[key]
108 except KeyError:
109 try:
110 mergeLogic = logic['*']
111 except KeyError:
112 raise Exception("Don't know how to merge key %s of class %s" %
113 (key, clazz.__name__))
114 if mergeLogic is NotImplemented:
115 continue
116 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
117 if value is not NotImplemented:
118 setattr(returnTable, key, value)
119
120 return returnTable
121
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500122
123@_add_method(DefaultTable, allowDefaultTable=True)
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500124def merge(self, m, tables):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500125 if not hasattr(self, 'mergeMap'):
126 m.log("Don't know how to merge '%s'." % self.tableTag)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500127 return NotImplemented
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500128
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500129 return m.mergeObjects(self, self.mergeMap, tables)
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500130
131ttLib.getTableClass('maxp').mergeMap = {
132 '*': max,
133 'tableTag': equal,
134 'tableVersion': equal,
135 'numGlyphs': sum,
136 'maxStorage': max, # FIXME: may need to be changed to sum
137 'maxFunctionDefs': sum,
138 'maxInstructionDefs': sum,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400139 # TODO When we correctly merge hinting data, update these values:
140 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500141}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400142
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500143ttLib.getTableClass('head').mergeMap = {
144 'tableTag': equal,
145 'tableVersion': max,
146 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500147 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500148 'magicNumber': equal,
149 'flags': first, # FIXME: replace with bit-sensitive code
150 'unitsPerEm': equal,
151 'created': current_time,
152 'modified': current_time,
153 'xMin': min,
154 'yMin': min,
155 'xMax': max,
156 'yMax': max,
157 'macStyle': first,
158 'lowestRecPPEM': max,
159 'fontDirectionHint': lambda lst: 2,
160 'indexToLocFormat': recalculate,
161 'glyphDataFormat': equal,
162}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400163
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500164ttLib.getTableClass('hhea').mergeMap = {
165 '*': equal,
166 'tableTag': equal,
167 'tableVersion': max,
168 'ascent': max,
169 'descent': min,
170 'lineGap': max,
171 'advanceWidthMax': max,
172 'minLeftSideBearing': min,
173 'minRightSideBearing': min,
174 'xMaxExtent': max,
175 'caretSlopeRise': first, # FIXME
176 'caretSlopeRun': first, # FIXME
177 'caretOffset': first, # FIXME
178 'numberOfHMetrics': recalculate,
179}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400180
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500181ttLib.getTableClass('OS/2').mergeMap = {
182 '*': first,
183 'tableTag': equal,
184 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500185 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500186 'fsType': first, # FIXME
187 'panose': first, # FIXME?
188 'ulUnicodeRange1': bitwise_or,
189 'ulUnicodeRange2': bitwise_or,
190 'ulUnicodeRange3': bitwise_or,
191 'ulUnicodeRange4': bitwise_or,
192 'fsFirstCharIndex': min,
193 'fsLastCharIndex': max,
194 'sTypoAscender': max,
195 'sTypoDescender': min,
196 'sTypoLineGap': max,
197 'usWinAscent': max,
198 'usWinDescent': max,
199 'ulCodePageRange1': bitwise_or,
200 'ulCodePageRange2': bitwise_or,
201 'usMaxContex': max,
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500202 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500203}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400204
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500205ttLib.getTableClass('post').mergeMap = {
206 '*': first,
207 'tableTag': equal,
208 'formatType': max,
209 'isFixedPitch': min,
210 'minMemType42': max,
211 'maxMemType42': lambda lst: 0,
212 'minMemType1': max,
213 'maxMemType1': lambda lst: 0,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500214 'mapping': implemented(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500215 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500216}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400217
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500218ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
219 'tableTag': equal,
220 'metrics': sumDicts,
221}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400222
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500223ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500224 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500225 'tableTag': equal,
226}
227
228ttLib.getTableClass('glyf').mergeMap = {
229 'tableTag': equal,
230 'glyphs': sumDicts,
231 'glyphOrder': sumLists,
232}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400233
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400234@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500235def merge(self, m, tables):
236 for table in tables:
Behdad Esfahbod43650332013-09-20 16:33:33 -0400237 for g in table.glyphs.values():
238 # Drop hints for now, since we don't remap
239 # functions / CVT values.
240 g.removeHinting()
241 # Expand composite glyphs to load their
242 # composite glyph names.
243 if g.isComposite():
244 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500245 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400246
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500247ttLib.getTableClass('prep').mergeMap = NotImplemented
248ttLib.getTableClass('fpgm').mergeMap = NotImplemented
249ttLib.getTableClass('cvt ').mergeMap = NotImplemented
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400250
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400251@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500252def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400253 # TODO Handle format=14.
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500254 cmapTables = [t for table in tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400255 if t.platformID == 3 and t.platEncID in [1, 10]]
256 # TODO Better handle format-4 and format-12 coexisting in same font.
257 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400258 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400259 assert all(t.format in [4, 12] for t in cmapTables)
260 format = max(t.format for t in cmapTables)
261 cmapTable = module.cmap_classes[format](format)
262 cmapTable.cmap = {}
263 cmapTable.platformID = 3
264 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
265 cmapTable.language = 0
266 for table in cmapTables:
267 # TODO handle duplicates.
268 cmapTable.cmap.update(table.cmap)
269 self.tableVersion = 0
270 self.tables = [cmapTable]
271 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500272 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400273
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400274
Behdad Esfahbod26429342013-12-19 11:53:47 -0500275otTables.ScriptList.mergeMap = {
276 'ScriptCount': sum,
277 'ScriptRecord': sumLists,
278}
279
280otTables.FeatureList.mergeMap = {
281 'FeatureCount': sum,
282 'FeatureRecord': sumLists,
283}
284
285otTables.LookupList.mergeMap = {
286 'LookupCount': sum,
287 'Lookup': sumLists,
288}
289
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500290otTables.Coverage.mergeMap = {
291 'glyphs': sumLists,
292}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400293
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500294otTables.ClassDef.mergeMap = {
295 'classDefs': sumDicts,
296}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400297
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500298otTables.LigCaretList.mergeMap = {
299 'Coverage': mergeObjects,
300 'LigGlyphCount': sum,
301 'LigGlyph': sumLists,
302}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400303
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500304otTables.AttachList.mergeMap = {
305 'Coverage': mergeObjects,
306 'GlyphCount': sum,
307 'AttachPoint': sumLists,
308}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400309
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500310# XXX Renumber MarkFilterSets of lookups
311otTables.MarkGlyphSetsDef.mergeMap = {
312 'MarkSetTableFormat': equal,
313 'MarkSetCount': sum,
314 'Coverage': sumLists,
315}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400316
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500317otTables.GDEF.mergeMap = {
318 '*': mergeObjects,
319 'Version': max,
320}
321
Behdad Esfahbod26429342013-12-19 11:53:47 -0500322otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
323 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500324 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500325}
326
327ttLib.getTableClass('GDEF').mergeMap = \
328ttLib.getTableClass('GSUB').mergeMap = \
329ttLib.getTableClass('GPOS').mergeMap = \
330ttLib.getTableClass('BASE').mergeMap = \
331ttLib.getTableClass('JSTF').mergeMap = \
332ttLib.getTableClass('MATH').mergeMap = \
333{
334 'tableTag': equal,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500335 'table': mergeObjects,
336}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400337
Behdad Esfahbod26429342013-12-19 11:53:47 -0500338
339@_add_method(otTables.Feature)
340def mapLookups(self, lookupMap):
341 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
342
343@_add_method(otTables.FeatureList)
344def mapLookups(self, lookupMap):
345 for f in self.FeatureRecord:
346 if not f or not f.Feature: continue
347 f.Feature.mapLookups(lookupMap)
348
349@_add_method(otTables.DefaultLangSys,
350 otTables.LangSys)
351def mapFeatures(self, featureMap):
352 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
353 if self.ReqFeatureIndex != 65535:
354 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
355
356@_add_method(otTables.Script)
357def mapFeatures(self, featureMap):
358 if self.DefaultLangSys:
359 self.DefaultLangSys.mapFeatures(featureMap)
360 for l in self.LangSysRecord:
361 if not l or not l.LangSys: continue
362 l.LangSys.mapFeatures(featureMap)
363
364@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500365def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500366 for s in self.ScriptRecord:
367 if not s or not s.Script: continue
368 s.Script.mapFeatures(featureMap)
369
370
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400371class Options(object):
372
373 class UnknownOptionError(Exception):
374 pass
375
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400376 def __init__(self, **kwargs):
377
378 self.set(**kwargs)
379
380 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500381 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400382 if not hasattr(self, k):
383 raise self.UnknownOptionError("Unknown option '%s'" % k)
384 setattr(self, k, v)
385
386 def parse_opts(self, argv, ignore_unknown=False):
387 ret = []
388 opts = {}
389 for a in argv:
390 orig_a = a
391 if not a.startswith('--'):
392 ret.append(a)
393 continue
394 a = a[2:]
395 i = a.find('=')
396 op = '='
397 if i == -1:
398 if a.startswith("no-"):
399 k = a[3:]
400 v = False
401 else:
402 k = a
403 v = True
404 else:
405 k = a[:i]
406 if k[-1] in "-+":
407 op = k[-1]+'=' # Ops is '-=' or '+=' now.
408 k = k[:-1]
409 v = a[i+1:]
410 k = k.replace('-', '_')
411 if not hasattr(self, k):
412 if ignore_unknown == True or k in ignore_unknown:
413 ret.append(orig_a)
414 continue
415 else:
416 raise self.UnknownOptionError("Unknown option '%s'" % a)
417
418 ov = getattr(self, k)
419 if isinstance(ov, bool):
420 v = bool(v)
421 elif isinstance(ov, int):
422 v = int(v)
423 elif isinstance(ov, list):
424 vv = v.split(',')
425 if vv == ['']:
426 vv = []
427 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
428 if op == '=':
429 v = vv
430 elif op == '+=':
431 v = ov
432 v.extend(vv)
433 elif op == '-=':
434 v = ov
435 for x in vv:
436 if x in v:
437 v.remove(x)
438 else:
439 assert 0
440
441 opts[k] = v
442 self.set(**opts)
443
444 return ret
445
446
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500447class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400448
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400449 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400450
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400451 if not log:
452 log = Logger()
453 if not options:
454 options = Options()
455
456 self.options = options
457 self.log = log
458
459 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400460
461 mega = ttLib.TTFont()
462
463 #
464 # Settle on a mega glyph order.
465 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400466 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400467 glyphOrders = [font.getGlyphOrder() for font in fonts]
468 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
469 # Reload fonts and set new glyph names on them.
470 # TODO Is it necessary to reload font? I think it is. At least
471 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400472 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400473 for font,glyphOrder in zip(fonts, glyphOrders):
474 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400475 mega.setGlyphOrder(megaGlyphOrder)
476
Behdad Esfahbod26429342013-12-19 11:53:47 -0500477 for font in fonts:
478 self._preMerge(font)
479
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500480 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400481 allTags.remove('GlyphOrder')
482 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400483
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400484 clazz = ttLib.getTableClass(tag)
485
Behdad Esfahbod26429342013-12-19 11:53:47 -0500486 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500487 table = clazz(tag).merge(self, tables)
488 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400489 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400490 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400491 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500492 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400493 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400494
Behdad Esfahbod26429342013-12-19 11:53:47 -0500495 self._postMerge(mega)
496
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400497 return mega
498
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400499 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400500 """Modifies passed-in glyphOrders to reflect new glyph names.
501 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400502 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400503 # TODO Even this simplistic numbering can result in conflicts.
504 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400505 mega = []
506 for n,glyphOrder in enumerate(glyphOrders):
507 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500508 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400509 glyphOrder[i] = glyphName
510 mega.append(glyphName)
511 return mega
512
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500513 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500514 # Right now we don't use self at all. Will use in the future
515 # for options and logging.
516
517 if logic is NotImplemented:
518 return NotImplemented
519
520 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800521 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800522 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500523 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800524 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500525 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500526 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500527 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500528 raise Exception("Don't know how to merge key %s of class %s" %
529 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500530 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800531 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500532 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
533 if value is not NotImplemented:
534 setattr(returnTable, key, value)
535
536 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800537
Behdad Esfahbod26429342013-12-19 11:53:47 -0500538 def _preMerge(self, font):
539
Behdad Esfahbod26429342013-12-19 11:53:47 -0500540 GDEF = font.get('GDEF')
541 GSUB = font.get('GSUB')
542 GPOS = font.get('GPOS')
543
544 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500545 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500546
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500547 if t.table.LookupList and t.table.FeatureList:
548 lookupMap = dict(enumerate(t.table.LookupList.Lookup))
549 t.table.FeatureList.mapLookups(lookupMap)
550
551 if t.table.FeatureList and t.table.ScriptList:
552 featureMap = dict(enumerate(t.table.FeatureList.FeatureRecord))
553 t.table.ScriptList.mapFeatures(featureMap)
554
555 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500556 # TODO FeatureParams nameIDs
557
558 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500559
560 GDEF = font.get('GDEF')
561 GSUB = font.get('GSUB')
562 GPOS = font.get('GPOS')
563
564 for t in [GSUB, GPOS]:
565 if not t: continue
566
567 if t.table.LookupList and t.table.FeatureList:
568 lookupMap = dict((v,i) for i,v in enumerate(t.table.LookupList.Lookup))
569 t.table.FeatureList.mapLookups(lookupMap)
570
571 if t.table.FeatureList and t.table.ScriptList:
572 featureMap = dict((v,i) for i,v in enumerate(t.table.FeatureList.FeatureRecord))
573 t.table.ScriptList.mapFeatures(featureMap)
574
575 # TODO GDEF/Lookup MarkFilteringSets
576 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500577
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400578
579class Logger(object):
580
581 def __init__(self, verbose=False, xml=False, timing=False):
582 self.verbose = verbose
583 self.xml = xml
584 self.timing = timing
585 self.last_time = self.start_time = time.time()
586
587 def parse_opts(self, argv):
588 argv = argv[:]
589 for v in ['verbose', 'xml', 'timing']:
590 if "--"+v in argv:
591 setattr(self, v, True)
592 argv.remove("--"+v)
593 return argv
594
595 def __call__(self, *things):
596 if not self.verbose:
597 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500598 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400599
600 def lapse(self, *things):
601 if not self.timing:
602 return
603 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500604 print("Took %0.3fs to %s" %(new_time - self.last_time,
605 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400606 self.last_time = new_time
607
608 def font(self, font, file=sys.stdout):
609 if not self.xml:
610 return
611 from fontTools.misc import xmlWriter
612 writer = xmlWriter.XMLWriter(file)
613 font.disassembleInstructions = False # Work around ttLib bug
614 for tag in font.keys():
615 writer.begintag(tag)
616 writer.newline()
617 font[tag].toXML(writer, font)
618 writer.endtag(tag)
619 writer.newline()
620
621
622__all__ = [
623 'Options',
624 'Merger',
625 'Logger',
626 'main'
627]
628
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400629def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400630
631 log = Logger()
632 args = log.parse_opts(args)
633
634 options = Options()
635 args = options.parse_opts(args)
636
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400637 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500638 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400639 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400640
641 merger = Merger(options=options, log=log)
642 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400643 outfile = 'merged.ttf'
644 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400645 log.lapse("compile and save font")
646
647 log.last_time = log.start_time
648 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400649
650if __name__ == "__main__":
651 main(sys.argv[1:])