blob: 7606d10a1a6c0a466a091dca68b8590b5396f9b9 [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 Esfahbod35e3c722013-12-20 21:34:09 -050025 assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.'
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050026 assert method.__name__ not in clazz.__dict__, \
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040027 "Oops, class '%s' has method '%s'." % (clazz.__name__,
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -050028 method.__name__)
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050029 setattr(clazz, method.__name__, method)
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040030 return None
31 return wrapper
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040032
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080033# General utility functions for merging values from different fonts
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050034
Behdad Esfahbod49028b32013-12-18 17:34:17 -050035def equal(lst):
36 t = iter(lst)
37 first = next(t)
38 assert all(item == first for item in t)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080039 return first
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080040
41def first(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050042 return next(iter(lst))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080043
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080044def recalculate(lst):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050045 return NotImplemented
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080046
47def current_time(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050048 return int(time.time() - _h_e_a_d.mac_epoch_diff)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080049
50def bitwise_or(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050051 return reduce(operator.or_, lst)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080052
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050053def avg_int(lst):
54 lst = list(lst)
55 return sum(lst) // len(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040056
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050057def nonnone(func):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050058 """Returns a filter func that when called with a list,
59 only calls func on the non-None items of the list, and
60 only so if there's at least one non-None item in the
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050061 list. Otherwise returns None."""
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050062
63 def wrapper(lst):
64 items = [item for item in lst if item is not None]
65 return func(items) if items else None
66
67 return wrapper
68
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050069def implemented(func):
70 """Returns a filter func that when called with a list,
71 only calls func on the non-NotImplemented items of the list,
72 and only so if there's at least one item remaining.
73 Otherwise returns NotImplemented."""
74
75 def wrapper(lst):
76 items = [item for item in lst if item is not NotImplemented]
77 return func(items) if items else NotImplemented
78
79 return wrapper
80
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050081def sumLists(lst):
82 l = []
83 for item in lst:
84 l.extend(item)
85 return l
86
87def sumDicts(lst):
88 d = {}
89 for item in lst:
90 d.update(item)
91 return d
92
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050093def mergeObjects(lst):
94 lst = [item for item in lst if item is not None and item is not NotImplemented]
95 if not lst:
96 return None # Not all can be NotImplemented
97
98 clazz = lst[0].__class__
99 assert all(type(item) == clazz for item in lst), lst
100 logic = clazz.mergeMap
101 returnTable = clazz()
102
103 allKeys = set.union(set(), *(vars(table).keys() for table in lst))
104 for key in allKeys:
105 try:
106 mergeLogic = logic[key]
107 except KeyError:
108 try:
109 mergeLogic = logic['*']
110 except KeyError:
111 raise Exception("Don't know how to merge key %s of class %s" %
112 (key, clazz.__name__))
113 if mergeLogic is NotImplemented:
114 continue
115 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
116 if value is not NotImplemented:
117 setattr(returnTable, key, value)
118
119 return returnTable
120
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500121
122@_add_method(DefaultTable, allowDefaultTable=True)
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500123def merge(self, m, tables):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500124 if not hasattr(self, 'mergeMap'):
125 m.log("Don't know how to merge '%s'." % self.tableTag)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500126 return NotImplemented
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500127
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500128 return m.mergeObjects(self, self.mergeMap, tables)
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500129
130ttLib.getTableClass('maxp').mergeMap = {
131 '*': max,
132 'tableTag': equal,
133 'tableVersion': equal,
134 'numGlyphs': sum,
135 'maxStorage': max, # FIXME: may need to be changed to sum
136 'maxFunctionDefs': sum,
137 'maxInstructionDefs': sum,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400138 # TODO When we correctly merge hinting data, update these values:
139 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500140}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400141
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500142ttLib.getTableClass('head').mergeMap = {
143 'tableTag': equal,
144 'tableVersion': max,
145 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500146 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500147 'magicNumber': equal,
148 'flags': first, # FIXME: replace with bit-sensitive code
149 'unitsPerEm': equal,
150 'created': current_time,
151 'modified': current_time,
152 'xMin': min,
153 'yMin': min,
154 'xMax': max,
155 'yMax': max,
156 'macStyle': first,
157 'lowestRecPPEM': max,
158 'fontDirectionHint': lambda lst: 2,
159 'indexToLocFormat': recalculate,
160 'glyphDataFormat': equal,
161}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400162
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500163ttLib.getTableClass('hhea').mergeMap = {
164 '*': equal,
165 'tableTag': equal,
166 'tableVersion': max,
167 'ascent': max,
168 'descent': min,
169 'lineGap': max,
170 'advanceWidthMax': max,
171 'minLeftSideBearing': min,
172 'minRightSideBearing': min,
173 'xMaxExtent': max,
174 'caretSlopeRise': first, # FIXME
175 'caretSlopeRun': first, # FIXME
176 'caretOffset': first, # FIXME
177 'numberOfHMetrics': recalculate,
178}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400179
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500180ttLib.getTableClass('OS/2').mergeMap = {
181 '*': first,
182 'tableTag': equal,
183 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500184 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500185 'fsType': first, # FIXME
186 'panose': first, # FIXME?
187 'ulUnicodeRange1': bitwise_or,
188 'ulUnicodeRange2': bitwise_or,
189 'ulUnicodeRange3': bitwise_or,
190 'ulUnicodeRange4': bitwise_or,
191 'fsFirstCharIndex': min,
192 'fsLastCharIndex': max,
193 'sTypoAscender': max,
194 'sTypoDescender': min,
195 'sTypoLineGap': max,
196 'usWinAscent': max,
197 'usWinDescent': max,
198 'ulCodePageRange1': bitwise_or,
199 'ulCodePageRange2': bitwise_or,
200 'usMaxContex': max,
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500201 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500202}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400203
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500204ttLib.getTableClass('post').mergeMap = {
205 '*': first,
206 'tableTag': equal,
207 'formatType': max,
208 'isFixedPitch': min,
209 'minMemType42': max,
210 'maxMemType42': lambda lst: 0,
211 'minMemType1': max,
212 'maxMemType1': lambda lst: 0,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500213 'mapping': implemented(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500214 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500215}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400216
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500217ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
218 'tableTag': equal,
219 'metrics': sumDicts,
220}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400221
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800222ttLib.getTableClass('gasp').mergeMap = {
223 'tableTag': equal,
224 'version': max,
225 'gaspRange': first, # FIXME? Appears irreconcilable
226}
227
228ttLib.getTableClass('name').mergeMap = {
229 'tableTag': equal,
230 'names': first, # FIXME? Does mixing name records make sense?
231}
232
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500233ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500234 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500235 'tableTag': equal,
236}
237
238ttLib.getTableClass('glyf').mergeMap = {
239 'tableTag': equal,
240 'glyphs': sumDicts,
241 'glyphOrder': sumLists,
242}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400243
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400244@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500245def merge(self, m, tables):
246 for table in tables:
Behdad Esfahbod43650332013-09-20 16:33:33 -0400247 for g in table.glyphs.values():
248 # Drop hints for now, since we don't remap
249 # functions / CVT values.
250 g.removeHinting()
251 # Expand composite glyphs to load their
252 # composite glyph names.
253 if g.isComposite():
254 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500255 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400256
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500257ttLib.getTableClass('prep').mergeMap = NotImplemented
258ttLib.getTableClass('fpgm').mergeMap = NotImplemented
259ttLib.getTableClass('cvt ').mergeMap = NotImplemented
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400260
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400261@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500262def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400263 # TODO Handle format=14.
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500264 cmapTables = [t for table in tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400265 if t.platformID == 3 and t.platEncID in [1, 10]]
266 # TODO Better handle format-4 and format-12 coexisting in same font.
267 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400268 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400269 assert all(t.format in [4, 12] for t in cmapTables)
270 format = max(t.format for t in cmapTables)
271 cmapTable = module.cmap_classes[format](format)
272 cmapTable.cmap = {}
273 cmapTable.platformID = 3
274 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
275 cmapTable.language = 0
276 for table in cmapTables:
277 # TODO handle duplicates.
278 cmapTable.cmap.update(table.cmap)
279 self.tableVersion = 0
280 self.tables = [cmapTable]
281 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500282 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400283
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400284
Behdad Esfahbod26429342013-12-19 11:53:47 -0500285otTables.ScriptList.mergeMap = {
286 'ScriptCount': sum,
287 'ScriptRecord': sumLists,
288}
289
290otTables.FeatureList.mergeMap = {
291 'FeatureCount': sum,
292 'FeatureRecord': sumLists,
293}
294
295otTables.LookupList.mergeMap = {
296 'LookupCount': sum,
297 'Lookup': sumLists,
298}
299
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500300otTables.Coverage.mergeMap = {
301 'glyphs': sumLists,
302}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400303
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500304otTables.ClassDef.mergeMap = {
305 'classDefs': sumDicts,
306}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400307
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500308otTables.LigCaretList.mergeMap = {
309 'Coverage': mergeObjects,
310 'LigGlyphCount': sum,
311 'LigGlyph': sumLists,
312}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400313
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500314otTables.AttachList.mergeMap = {
315 'Coverage': mergeObjects,
316 'GlyphCount': sum,
317 'AttachPoint': sumLists,
318}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400319
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500320# XXX Renumber MarkFilterSets of lookups
321otTables.MarkGlyphSetsDef.mergeMap = {
322 'MarkSetTableFormat': equal,
323 'MarkSetCount': sum,
324 'Coverage': sumLists,
325}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400326
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500327otTables.GDEF.mergeMap = {
328 '*': mergeObjects,
329 'Version': max,
330}
331
Behdad Esfahbod26429342013-12-19 11:53:47 -0500332otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
333 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500334 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500335}
336
337ttLib.getTableClass('GDEF').mergeMap = \
338ttLib.getTableClass('GSUB').mergeMap = \
339ttLib.getTableClass('GPOS').mergeMap = \
340ttLib.getTableClass('BASE').mergeMap = \
341ttLib.getTableClass('JSTF').mergeMap = \
342ttLib.getTableClass('MATH').mergeMap = \
343{
344 'tableTag': equal,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500345 'table': mergeObjects,
346}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400347
Behdad Esfahbod26429342013-12-19 11:53:47 -0500348
349@_add_method(otTables.Feature)
350def mapLookups(self, lookupMap):
351 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
352
353@_add_method(otTables.FeatureList)
354def mapLookups(self, lookupMap):
355 for f in self.FeatureRecord:
356 if not f or not f.Feature: continue
357 f.Feature.mapLookups(lookupMap)
358
359@_add_method(otTables.DefaultLangSys,
360 otTables.LangSys)
361def mapFeatures(self, featureMap):
362 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
363 if self.ReqFeatureIndex != 65535:
364 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
365
366@_add_method(otTables.Script)
367def mapFeatures(self, featureMap):
368 if self.DefaultLangSys:
369 self.DefaultLangSys.mapFeatures(featureMap)
370 for l in self.LangSysRecord:
371 if not l or not l.LangSys: continue
372 l.LangSys.mapFeatures(featureMap)
373
374@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500375def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500376 for s in self.ScriptRecord:
377 if not s or not s.Script: continue
378 s.Script.mapFeatures(featureMap)
379
380
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400381class Options(object):
382
383 class UnknownOptionError(Exception):
384 pass
385
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400386 def __init__(self, **kwargs):
387
388 self.set(**kwargs)
389
390 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500391 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400392 if not hasattr(self, k):
393 raise self.UnknownOptionError("Unknown option '%s'" % k)
394 setattr(self, k, v)
395
396 def parse_opts(self, argv, ignore_unknown=False):
397 ret = []
398 opts = {}
399 for a in argv:
400 orig_a = a
401 if not a.startswith('--'):
402 ret.append(a)
403 continue
404 a = a[2:]
405 i = a.find('=')
406 op = '='
407 if i == -1:
408 if a.startswith("no-"):
409 k = a[3:]
410 v = False
411 else:
412 k = a
413 v = True
414 else:
415 k = a[:i]
416 if k[-1] in "-+":
417 op = k[-1]+'=' # Ops is '-=' or '+=' now.
418 k = k[:-1]
419 v = a[i+1:]
420 k = k.replace('-', '_')
421 if not hasattr(self, k):
422 if ignore_unknown == True or k in ignore_unknown:
423 ret.append(orig_a)
424 continue
425 else:
426 raise self.UnknownOptionError("Unknown option '%s'" % a)
427
428 ov = getattr(self, k)
429 if isinstance(ov, bool):
430 v = bool(v)
431 elif isinstance(ov, int):
432 v = int(v)
433 elif isinstance(ov, list):
434 vv = v.split(',')
435 if vv == ['']:
436 vv = []
437 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
438 if op == '=':
439 v = vv
440 elif op == '+=':
441 v = ov
442 v.extend(vv)
443 elif op == '-=':
444 v = ov
445 for x in vv:
446 if x in v:
447 v.remove(x)
448 else:
449 assert 0
450
451 opts[k] = v
452 self.set(**opts)
453
454 return ret
455
456
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500457class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400458
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400459 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400460
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400461 if not log:
462 log = Logger()
463 if not options:
464 options = Options()
465
466 self.options = options
467 self.log = log
468
469 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400470
471 mega = ttLib.TTFont()
472
473 #
474 # Settle on a mega glyph order.
475 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400476 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400477 glyphOrders = [font.getGlyphOrder() for font in fonts]
478 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
479 # Reload fonts and set new glyph names on them.
480 # TODO Is it necessary to reload font? I think it is. At least
481 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400482 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400483 for font,glyphOrder in zip(fonts, glyphOrders):
484 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400485 mega.setGlyphOrder(megaGlyphOrder)
486
Behdad Esfahbod26429342013-12-19 11:53:47 -0500487 for font in fonts:
488 self._preMerge(font)
489
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500490 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400491 allTags.remove('GlyphOrder')
492 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400493
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400494 clazz = ttLib.getTableClass(tag)
495
Behdad Esfahbod26429342013-12-19 11:53:47 -0500496 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500497 table = clazz(tag).merge(self, tables)
498 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400499 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400500 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400501 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500502 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400503 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400504
Behdad Esfahbod26429342013-12-19 11:53:47 -0500505 self._postMerge(mega)
506
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400507 return mega
508
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400509 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400510 """Modifies passed-in glyphOrders to reflect new glyph names.
511 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400512 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400513 # TODO Even this simplistic numbering can result in conflicts.
514 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400515 mega = []
516 for n,glyphOrder in enumerate(glyphOrders):
517 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500518 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400519 glyphOrder[i] = glyphName
520 mega.append(glyphName)
521 return mega
522
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500523 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500524 # Right now we don't use self at all. Will use in the future
525 # for options and logging.
526
527 if logic is NotImplemented:
528 return NotImplemented
529
530 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800531 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800532 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500533 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800534 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500535 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500536 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500537 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500538 raise Exception("Don't know how to merge key %s of class %s" %
539 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500540 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800541 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500542 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
543 if value is not NotImplemented:
544 setattr(returnTable, key, value)
545
546 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800547
Behdad Esfahbod26429342013-12-19 11:53:47 -0500548 def _preMerge(self, font):
549
Behdad Esfahbod26429342013-12-19 11:53:47 -0500550 GDEF = font.get('GDEF')
551 GSUB = font.get('GSUB')
552 GPOS = font.get('GPOS')
553
554 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500555 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500556
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500557 if t.table.LookupList and t.table.FeatureList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500558 lookupMap = {i:id(v) for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500559 t.table.FeatureList.mapLookups(lookupMap)
560
561 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500562 featureMap = {i:id(v) for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500563 t.table.ScriptList.mapFeatures(featureMap)
564
565 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500566 # TODO FeatureParams nameIDs
567
568 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500569
570 GDEF = font.get('GDEF')
571 GSUB = font.get('GSUB')
572 GPOS = font.get('GPOS')
573
574 for t in [GSUB, GPOS]:
575 if not t: continue
576
577 if t.table.LookupList and t.table.FeatureList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500578 lookupMap = {id(v):i for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500579 t.table.FeatureList.mapLookups(lookupMap)
580
581 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500582 featureMap = {id(v):i for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500583 t.table.ScriptList.mapFeatures(featureMap)
584
585 # TODO GDEF/Lookup MarkFilteringSets
586 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500587
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400588
589class Logger(object):
590
591 def __init__(self, verbose=False, xml=False, timing=False):
592 self.verbose = verbose
593 self.xml = xml
594 self.timing = timing
595 self.last_time = self.start_time = time.time()
596
597 def parse_opts(self, argv):
598 argv = argv[:]
599 for v in ['verbose', 'xml', 'timing']:
600 if "--"+v in argv:
601 setattr(self, v, True)
602 argv.remove("--"+v)
603 return argv
604
605 def __call__(self, *things):
606 if not self.verbose:
607 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500608 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400609
610 def lapse(self, *things):
611 if not self.timing:
612 return
613 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500614 print("Took %0.3fs to %s" %(new_time - self.last_time,
615 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400616 self.last_time = new_time
617
618 def font(self, font, file=sys.stdout):
619 if not self.xml:
620 return
621 from fontTools.misc import xmlWriter
622 writer = xmlWriter.XMLWriter(file)
623 font.disassembleInstructions = False # Work around ttLib bug
624 for tag in font.keys():
625 writer.begintag(tag)
626 writer.newline()
627 font[tag].toXML(writer, font)
628 writer.endtag(tag)
629 writer.newline()
630
631
632__all__ = [
633 'Options',
634 'Merger',
635 'Logger',
636 'main'
637]
638
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400639def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400640
641 log = Logger()
642 args = log.parse_opts(args)
643
644 options = Options()
645 args = options.parse_opts(args)
646
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400647 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500648 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400649 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400650
651 merger = Merger(options=options, log=log)
652 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400653 outfile = 'merged.ttf'
654 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400655 log.lapse("compile and save font")
656
657 log.last_time = log.start_time
658 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400659
660if __name__ == "__main__":
661 main(sys.argv[1:])