blob: b86bbd3d3deb6b0327c7a88f023ac35893a213d2 [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
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800223ttLib.getTableClass('gasp').mergeMap = {
224 'tableTag': equal,
225 'version': max,
226 'gaspRange': first, # FIXME? Appears irreconcilable
227}
228
229ttLib.getTableClass('name').mergeMap = {
230 'tableTag': equal,
231 'names': first, # FIXME? Does mixing name records make sense?
232}
233
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500234ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500235 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500236 'tableTag': equal,
237}
238
239ttLib.getTableClass('glyf').mergeMap = {
240 'tableTag': equal,
241 'glyphs': sumDicts,
242 'glyphOrder': sumLists,
243}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400244
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400245@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500246def merge(self, m, tables):
247 for table in tables:
Behdad Esfahbod43650332013-09-20 16:33:33 -0400248 for g in table.glyphs.values():
249 # Drop hints for now, since we don't remap
250 # functions / CVT values.
251 g.removeHinting()
252 # Expand composite glyphs to load their
253 # composite glyph names.
254 if g.isComposite():
255 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500256 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400257
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500258ttLib.getTableClass('prep').mergeMap = NotImplemented
259ttLib.getTableClass('fpgm').mergeMap = NotImplemented
260ttLib.getTableClass('cvt ').mergeMap = NotImplemented
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400261
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400262@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500263def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400264 # TODO Handle format=14.
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500265 cmapTables = [t for table in tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400266 if t.platformID == 3 and t.platEncID in [1, 10]]
267 # TODO Better handle format-4 and format-12 coexisting in same font.
268 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400269 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400270 assert all(t.format in [4, 12] for t in cmapTables)
271 format = max(t.format for t in cmapTables)
272 cmapTable = module.cmap_classes[format](format)
273 cmapTable.cmap = {}
274 cmapTable.platformID = 3
275 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
276 cmapTable.language = 0
277 for table in cmapTables:
278 # TODO handle duplicates.
279 cmapTable.cmap.update(table.cmap)
280 self.tableVersion = 0
281 self.tables = [cmapTable]
282 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500283 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400284
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400285
Behdad Esfahbod26429342013-12-19 11:53:47 -0500286otTables.ScriptList.mergeMap = {
287 'ScriptCount': sum,
288 'ScriptRecord': sumLists,
289}
290
291otTables.FeatureList.mergeMap = {
292 'FeatureCount': sum,
293 'FeatureRecord': sumLists,
294}
295
296otTables.LookupList.mergeMap = {
297 'LookupCount': sum,
298 'Lookup': sumLists,
299}
300
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500301otTables.Coverage.mergeMap = {
302 'glyphs': sumLists,
303}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400304
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500305otTables.ClassDef.mergeMap = {
306 'classDefs': sumDicts,
307}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400308
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500309otTables.LigCaretList.mergeMap = {
310 'Coverage': mergeObjects,
311 'LigGlyphCount': sum,
312 'LigGlyph': sumLists,
313}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400314
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500315otTables.AttachList.mergeMap = {
316 'Coverage': mergeObjects,
317 'GlyphCount': sum,
318 'AttachPoint': sumLists,
319}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400320
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500321# XXX Renumber MarkFilterSets of lookups
322otTables.MarkGlyphSetsDef.mergeMap = {
323 'MarkSetTableFormat': equal,
324 'MarkSetCount': sum,
325 'Coverage': sumLists,
326}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400327
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500328otTables.GDEF.mergeMap = {
329 '*': mergeObjects,
330 'Version': max,
331}
332
Behdad Esfahbod26429342013-12-19 11:53:47 -0500333otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
334 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500335 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500336}
337
338ttLib.getTableClass('GDEF').mergeMap = \
339ttLib.getTableClass('GSUB').mergeMap = \
340ttLib.getTableClass('GPOS').mergeMap = \
341ttLib.getTableClass('BASE').mergeMap = \
342ttLib.getTableClass('JSTF').mergeMap = \
343ttLib.getTableClass('MATH').mergeMap = \
344{
345 'tableTag': equal,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500346 'table': mergeObjects,
347}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400348
Behdad Esfahbod26429342013-12-19 11:53:47 -0500349
350@_add_method(otTables.Feature)
351def mapLookups(self, lookupMap):
352 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
353
354@_add_method(otTables.FeatureList)
355def mapLookups(self, lookupMap):
356 for f in self.FeatureRecord:
357 if not f or not f.Feature: continue
358 f.Feature.mapLookups(lookupMap)
359
360@_add_method(otTables.DefaultLangSys,
361 otTables.LangSys)
362def mapFeatures(self, featureMap):
363 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
364 if self.ReqFeatureIndex != 65535:
365 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
366
367@_add_method(otTables.Script)
368def mapFeatures(self, featureMap):
369 if self.DefaultLangSys:
370 self.DefaultLangSys.mapFeatures(featureMap)
371 for l in self.LangSysRecord:
372 if not l or not l.LangSys: continue
373 l.LangSys.mapFeatures(featureMap)
374
375@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500376def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500377 for s in self.ScriptRecord:
378 if not s or not s.Script: continue
379 s.Script.mapFeatures(featureMap)
380
381
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400382class Options(object):
383
384 class UnknownOptionError(Exception):
385 pass
386
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400387 def __init__(self, **kwargs):
388
389 self.set(**kwargs)
390
391 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500392 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400393 if not hasattr(self, k):
394 raise self.UnknownOptionError("Unknown option '%s'" % k)
395 setattr(self, k, v)
396
397 def parse_opts(self, argv, ignore_unknown=False):
398 ret = []
399 opts = {}
400 for a in argv:
401 orig_a = a
402 if not a.startswith('--'):
403 ret.append(a)
404 continue
405 a = a[2:]
406 i = a.find('=')
407 op = '='
408 if i == -1:
409 if a.startswith("no-"):
410 k = a[3:]
411 v = False
412 else:
413 k = a
414 v = True
415 else:
416 k = a[:i]
417 if k[-1] in "-+":
418 op = k[-1]+'=' # Ops is '-=' or '+=' now.
419 k = k[:-1]
420 v = a[i+1:]
421 k = k.replace('-', '_')
422 if not hasattr(self, k):
423 if ignore_unknown == True or k in ignore_unknown:
424 ret.append(orig_a)
425 continue
426 else:
427 raise self.UnknownOptionError("Unknown option '%s'" % a)
428
429 ov = getattr(self, k)
430 if isinstance(ov, bool):
431 v = bool(v)
432 elif isinstance(ov, int):
433 v = int(v)
434 elif isinstance(ov, list):
435 vv = v.split(',')
436 if vv == ['']:
437 vv = []
438 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
439 if op == '=':
440 v = vv
441 elif op == '+=':
442 v = ov
443 v.extend(vv)
444 elif op == '-=':
445 v = ov
446 for x in vv:
447 if x in v:
448 v.remove(x)
449 else:
450 assert 0
451
452 opts[k] = v
453 self.set(**opts)
454
455 return ret
456
457
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500458class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400459
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400460 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400461
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400462 if not log:
463 log = Logger()
464 if not options:
465 options = Options()
466
467 self.options = options
468 self.log = log
469
470 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400471
472 mega = ttLib.TTFont()
473
474 #
475 # Settle on a mega glyph order.
476 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400477 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400478 glyphOrders = [font.getGlyphOrder() for font in fonts]
479 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
480 # Reload fonts and set new glyph names on them.
481 # TODO Is it necessary to reload font? I think it is. At least
482 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400483 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400484 for font,glyphOrder in zip(fonts, glyphOrders):
485 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400486 mega.setGlyphOrder(megaGlyphOrder)
487
Behdad Esfahbod26429342013-12-19 11:53:47 -0500488 for font in fonts:
489 self._preMerge(font)
490
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500491 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400492 allTags.remove('GlyphOrder')
493 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400494
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400495 clazz = ttLib.getTableClass(tag)
496
Behdad Esfahbod26429342013-12-19 11:53:47 -0500497 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500498 table = clazz(tag).merge(self, tables)
499 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400500 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400501 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400502 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500503 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400504 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400505
Behdad Esfahbod26429342013-12-19 11:53:47 -0500506 self._postMerge(mega)
507
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400508 return mega
509
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400510 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400511 """Modifies passed-in glyphOrders to reflect new glyph names.
512 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400513 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400514 # TODO Even this simplistic numbering can result in conflicts.
515 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400516 mega = []
517 for n,glyphOrder in enumerate(glyphOrders):
518 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500519 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400520 glyphOrder[i] = glyphName
521 mega.append(glyphName)
522 return mega
523
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500524 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500525 # Right now we don't use self at all. Will use in the future
526 # for options and logging.
527
528 if logic is NotImplemented:
529 return NotImplemented
530
531 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800532 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800533 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500534 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800535 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500536 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500537 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500538 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500539 raise Exception("Don't know how to merge key %s of class %s" %
540 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500541 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800542 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500543 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
544 if value is not NotImplemented:
545 setattr(returnTable, key, value)
546
547 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800548
Behdad Esfahbod26429342013-12-19 11:53:47 -0500549 def _preMerge(self, font):
550
Behdad Esfahbod26429342013-12-19 11:53:47 -0500551 GDEF = font.get('GDEF')
552 GSUB = font.get('GSUB')
553 GPOS = font.get('GPOS')
554
555 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500556 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500557
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500558 if t.table.LookupList and t.table.FeatureList:
559 lookupMap = dict(enumerate(t.table.LookupList.Lookup))
560 t.table.FeatureList.mapLookups(lookupMap)
561
562 if t.table.FeatureList and t.table.ScriptList:
563 featureMap = dict(enumerate(t.table.FeatureList.FeatureRecord))
564 t.table.ScriptList.mapFeatures(featureMap)
565
566 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500567 # TODO FeatureParams nameIDs
568
569 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500570
571 GDEF = font.get('GDEF')
572 GSUB = font.get('GSUB')
573 GPOS = font.get('GPOS')
574
575 for t in [GSUB, GPOS]:
576 if not t: continue
577
578 if t.table.LookupList and t.table.FeatureList:
579 lookupMap = dict((v,i) for i,v in enumerate(t.table.LookupList.Lookup))
580 t.table.FeatureList.mapLookups(lookupMap)
581
582 if t.table.FeatureList and t.table.ScriptList:
583 featureMap = dict((v,i) for i,v in enumerate(t.table.FeatureList.FeatureRecord))
584 t.table.ScriptList.mapFeatures(featureMap)
585
586 # TODO GDEF/Lookup MarkFilteringSets
587 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500588
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400589
590class Logger(object):
591
592 def __init__(self, verbose=False, xml=False, timing=False):
593 self.verbose = verbose
594 self.xml = xml
595 self.timing = timing
596 self.last_time = self.start_time = time.time()
597
598 def parse_opts(self, argv):
599 argv = argv[:]
600 for v in ['verbose', 'xml', 'timing']:
601 if "--"+v in argv:
602 setattr(self, v, True)
603 argv.remove("--"+v)
604 return argv
605
606 def __call__(self, *things):
607 if not self.verbose:
608 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500609 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400610
611 def lapse(self, *things):
612 if not self.timing:
613 return
614 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500615 print("Took %0.3fs to %s" %(new_time - self.last_time,
616 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400617 self.last_time = new_time
618
619 def font(self, font, file=sys.stdout):
620 if not self.xml:
621 return
622 from fontTools.misc import xmlWriter
623 writer = xmlWriter.XMLWriter(file)
624 font.disassembleInstructions = False # Work around ttLib bug
625 for tag in font.keys():
626 writer.begintag(tag)
627 writer.newline()
628 font[tag].toXML(writer, font)
629 writer.endtag(tag)
630 writer.newline()
631
632
633__all__ = [
634 'Options',
635 'Merger',
636 'Logger',
637 'main'
638]
639
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400640def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400641
642 log = Logger()
643 args = log.parse_opts(args)
644
645 options = Options()
646 args = options.parse_opts(args)
647
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400648 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500649 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400650 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400651
652 merger = Merger(options=options, log=log)
653 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400654 outfile = 'merged.ttf'
655 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400656 log.lapse("compile and save font")
657
658 log.last_time = log.start_time
659 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400660
661if __name__ == "__main__":
662 main(sys.argv[1:])