blob: 54e91e37b9e938c114822e4018aeb6a7e3a41b76 [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 Esfahbod1ae29592014-01-14 15:07:50 +08008from __future__ import print_function, division, absolute_import
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -05009from 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):
Behdad Esfahbod477dad12014-03-28 13:52:48 -070036 lst = list(lst)
Behdad Esfahbod49028b32013-12-18 17:34:17 -050037 t = iter(lst)
38 first = next(t)
Behdad Esfahbod477dad12014-03-28 13:52:48 -070039 assert all(item == first for item in t), "Expected all items to be equal: %s" % lst
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
Roozbeh Pournader642eaf12013-12-21 01:04:18 -080051def bitwise_and(lst):
52 return reduce(operator.and_, lst)
53
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080054def bitwise_or(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050055 return reduce(operator.or_, lst)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080056
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050057def avg_int(lst):
58 lst = list(lst)
59 return sum(lst) // len(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040060
Behdad Esfahbod0d5fcf42014-03-28 14:37:32 -070061def onlyExisting(func):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050062 """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
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050073def 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
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050085def mergeObjects(lst):
Behdad Esfahbod08845072014-03-28 15:02:40 -070086 lst = [item for item in lst if item is not NotImplemented]
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050087 if not lst:
Behdad Esfahbod08845072014-03-28 15:02:40 -070088 return NotImplemented
89 lst = [item for item in lst if item is not None]
90 if not lst:
91 return None
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050092
93 clazz = lst[0].__class__
94 assert all(type(item) == clazz for item in lst), lst
Behdad Esfahbod08845072014-03-28 15:02:40 -070095
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050096 logic = clazz.mergeMap
97 returnTable = clazz()
Behdad Esfahbod82c54632014-03-28 14:41:53 -070098 returnDict = {}
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050099
100 allKeys = set.union(set(), *(vars(table).keys() for table in lst))
101 for key in allKeys:
102 try:
103 mergeLogic = logic[key]
104 except KeyError:
105 try:
106 mergeLogic = logic['*']
107 except KeyError:
108 raise Exception("Don't know how to merge key %s of class %s" %
109 (key, clazz.__name__))
110 if mergeLogic is NotImplemented:
111 continue
112 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
113 if value is not NotImplemented:
Behdad Esfahbod82c54632014-03-28 14:41:53 -0700114 returnDict[key] = value
115
116 returnTable.__dict__ = returnDict
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500117
118 return returnTable
119
Behdad Esfahbod201a6812014-03-28 14:58:12 -0700120def mergeBits(bitmap):
121
122 def wrapper(lst):
123 lst = list(lst)
124 returnValue = 0
125 for bitNumber in range(bitmap['size']):
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800126 try:
Behdad Esfahbod201a6812014-03-28 14:58:12 -0700127 mergeLogic = bitmap[bitNumber]
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800128 except KeyError:
Behdad Esfahbod201a6812014-03-28 14:58:12 -0700129 try:
130 mergeLogic = bitmap['*']
131 except KeyError:
132 raise Exception("Don't know how to merge bit %s" % bitNumber)
133 shiftedBit = 1 << bitNumber
134 mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst)
135 returnValue |= mergedValue << bitNumber
136 return returnValue
137
138 return wrapper
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800139
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500140
141@_add_method(DefaultTable, allowDefaultTable=True)
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500142def merge(self, m, tables):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500143 if not hasattr(self, 'mergeMap'):
144 m.log("Don't know how to merge '%s'." % self.tableTag)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500145 return NotImplemented
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500146
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500147 logic = self.mergeMap
148
149 if isinstance(logic, dict):
150 return m.mergeObjects(self, self.mergeMap, tables)
151 else:
152 return logic(tables)
153
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500154
155ttLib.getTableClass('maxp').mergeMap = {
156 '*': max,
157 'tableTag': equal,
158 'tableVersion': equal,
159 'numGlyphs': sum,
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500160 'maxStorage': first,
161 'maxFunctionDefs': first,
162 'maxInstructionDefs': first,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400163 # TODO When we correctly merge hinting data, update these values:
164 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500165}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400166
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700167headFlagsMergeBitMap = {
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800168 'size': 16,
169 '*': bitwise_or,
170 1: bitwise_and, # Baseline at y = 0
171 2: bitwise_and, # lsb at x = 0
172 3: bitwise_and, # Force ppem to integer values. FIXME?
173 5: bitwise_and, # Font is vertical
174 6: lambda bit: 0, # Always set to zero
175 11: bitwise_and, # Font data is 'lossless'
176 13: bitwise_and, # Optimized for ClearType
177 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
178 15: lambda bit: 0, # Always set to zero
179}
180
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500181ttLib.getTableClass('head').mergeMap = {
182 'tableTag': equal,
183 'tableVersion': max,
184 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500185 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500186 'magicNumber': equal,
Behdad Esfahbod201a6812014-03-28 14:58:12 -0700187 'flags': mergeBits(headFlagsMergeBitMap),
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500188 'unitsPerEm': equal,
189 'created': current_time,
190 'modified': current_time,
191 'xMin': min,
192 'yMin': min,
193 'xMax': max,
194 'yMax': max,
195 'macStyle': first,
196 'lowestRecPPEM': max,
197 'fontDirectionHint': lambda lst: 2,
198 'indexToLocFormat': recalculate,
199 'glyphDataFormat': equal,
200}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400201
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500202ttLib.getTableClass('hhea').mergeMap = {
203 '*': equal,
204 'tableTag': equal,
205 'tableVersion': max,
206 'ascent': max,
207 'descent': min,
208 'lineGap': max,
209 'advanceWidthMax': max,
210 'minLeftSideBearing': min,
211 'minRightSideBearing': min,
212 'xMaxExtent': max,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800213 'caretSlopeRise': first,
214 'caretSlopeRun': first,
215 'caretOffset': first,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500216 'numberOfHMetrics': recalculate,
217}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400218
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700219os2FsTypeMergeBitMap = {
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800220 'size': 16,
221 '*': lambda bit: 0,
222 1: bitwise_or, # no embedding permitted
223 2: bitwise_and, # allow previewing and printing documents
224 3: bitwise_and, # allow editing documents
225 8: bitwise_or, # no subsetting permitted
226 9: bitwise_or, # no embedding of outlines permitted
227}
228
229def mergeOs2FsType(lst):
230 lst = list(lst)
231 if all(item == 0 for item in lst):
232 return 0
233
234 # Compute least restrictive logic for each fsType value
235 for i in range(len(lst)):
236 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
237 if lst[i] & 0x000C:
238 lst[i] &= ~0x0002
239 # set bit 2 (allow previewing) if bit 3 is set (allow editing)
240 elif lst[i] & 0x0008:
241 lst[i] |= 0x0004
242 # set bits 2 and 3 if everything is allowed
243 elif lst[i] == 0:
244 lst[i] = 0x000C
245
Behdad Esfahbod201a6812014-03-28 14:58:12 -0700246 fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800247 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
248 if fsType & 0x0002:
249 fsType &= ~0x000C
250 return fsType
251
252
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500253ttLib.getTableClass('OS/2').mergeMap = {
254 '*': first,
255 'tableTag': equal,
256 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500257 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800258 'fsType': mergeOs2FsType, # Will be overwritten
259 'panose': first, # FIXME: should really be the first Latin font
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500260 'ulUnicodeRange1': bitwise_or,
261 'ulUnicodeRange2': bitwise_or,
262 'ulUnicodeRange3': bitwise_or,
263 'ulUnicodeRange4': bitwise_or,
264 'fsFirstCharIndex': min,
265 'fsLastCharIndex': max,
266 'sTypoAscender': max,
267 'sTypoDescender': min,
268 'sTypoLineGap': max,
269 'usWinAscent': max,
270 'usWinDescent': max,
Behdad Esfahbod0e235be2014-03-28 14:56:27 -0700271 # Version 2,3,4
Behdad Esfahbod77654212014-03-28 14:48:09 -0700272 'ulCodePageRange1': onlyExisting(bitwise_or),
273 'ulCodePageRange2': onlyExisting(bitwise_or),
274 'usMaxContex': onlyExisting(max),
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500275 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500276}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400277
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800278@_add_method(ttLib.getTableClass('OS/2'))
279def merge(self, m, tables):
280 DefaultTable.merge(self, m, tables)
281 if self.version < 2:
282 # bits 8 and 9 are reserved and should be set to zero
283 self.fsType &= ~0x0300
284 if self.version >= 3:
285 # Only one of bits 1, 2, and 3 may be set. We already take
286 # care of bit 1 implications in mergeOs2FsType. So unset
287 # bit 2 if bit 3 is already set.
288 if self.fsType & 0x0008:
289 self.fsType &= ~0x0004
290 return self
291
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500292ttLib.getTableClass('post').mergeMap = {
293 '*': first,
294 'tableTag': equal,
295 'formatType': max,
296 'isFixedPitch': min,
297 'minMemType42': max,
298 'maxMemType42': lambda lst: 0,
299 'minMemType1': max,
300 'maxMemType1': lambda lst: 0,
Behdad Esfahbod0d5fcf42014-03-28 14:37:32 -0700301 'mapping': onlyExisting(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500302 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500303}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400304
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500305ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
306 'tableTag': equal,
307 'metrics': sumDicts,
308}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400309
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800310ttLib.getTableClass('gasp').mergeMap = {
311 'tableTag': equal,
312 'version': max,
313 'gaspRange': first, # FIXME? Appears irreconcilable
314}
315
316ttLib.getTableClass('name').mergeMap = {
317 'tableTag': equal,
318 'names': first, # FIXME? Does mixing name records make sense?
319}
320
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500321ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500322 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500323 'tableTag': equal,
324}
325
326ttLib.getTableClass('glyf').mergeMap = {
327 'tableTag': equal,
328 'glyphs': sumDicts,
329 'glyphOrder': sumLists,
330}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400331
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400332@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500333def merge(self, m, tables):
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500334 for i,table in enumerate(tables):
Behdad Esfahbod43650332013-09-20 16:33:33 -0400335 for g in table.glyphs.values():
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500336 if i:
337 # Drop hints for all but first font, since
338 # we don't map functions / CVT values.
339 g.removeHinting()
Behdad Esfahbod43650332013-09-20 16:33:33 -0400340 # Expand composite glyphs to load their
341 # composite glyph names.
342 if g.isComposite():
343 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500344 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400345
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500346ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
347ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
348ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400349
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400350@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500351def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400352 # TODO Handle format=14.
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700353 cmapTables = [(t,fontIdx) for fontIdx,table in enumerate(tables) for t in table.tables
Behdad Esfahbodf480c7c2014-03-12 12:18:47 -0700354 if t.isUnicode()]
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400355 # TODO Better handle format-4 and format-12 coexisting in same font.
356 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400357 module = ttLib.getTableModule('cmap')
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700358 assert all(t.format in [4, 12] for t,_ in cmapTables)
359 format = max(t.format for t,_ in cmapTables)
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400360 cmapTable = module.cmap_classes[format](format)
361 cmapTable.cmap = {}
362 cmapTable.platformID = 3
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700363 cmapTable.platEncID = max(t.platEncID for t,_ in cmapTables)
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400364 cmapTable.language = 0
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700365 cmap = cmapTable.cmap
366 for table,fontIdx in cmapTables:
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400367 # TODO handle duplicates.
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700368 for uni,gid in table.cmap.items():
369 oldgid = cmap.get(uni, None)
370 if oldgid is None:
371 cmap[uni] = gid
372 elif oldgid != gid:
373 # Char previously mapped to oldgid, now to gid.
374 # Record, to fix up in GSUB 'locl' later.
375 assert m.duplicateGlyphsPerFont[fontIdx].get(oldgid, gid) == gid
376 m.duplicateGlyphsPerFont[fontIdx][oldgid] = gid
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400377 self.tableVersion = 0
378 self.tables = [cmapTable]
379 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500380 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400381
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400382
Behdad Esfahbod26429342013-12-19 11:53:47 -0500383otTables.ScriptList.mergeMap = {
384 'ScriptCount': sum,
Behdad Esfahbod972af5a2013-12-31 18:16:36 +0800385 'ScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.ScriptTag),
Behdad Esfahbod26429342013-12-19 11:53:47 -0500386}
387
388otTables.FeatureList.mergeMap = {
389 'FeatureCount': sum,
390 'FeatureRecord': sumLists,
391}
392
393otTables.LookupList.mergeMap = {
394 'LookupCount': sum,
395 'Lookup': sumLists,
396}
397
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500398otTables.Coverage.mergeMap = {
399 'glyphs': sumLists,
400}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400401
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500402otTables.ClassDef.mergeMap = {
403 'classDefs': sumDicts,
404}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400405
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500406otTables.LigCaretList.mergeMap = {
407 'Coverage': mergeObjects,
408 'LigGlyphCount': sum,
409 'LigGlyph': sumLists,
410}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400411
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500412otTables.AttachList.mergeMap = {
413 'Coverage': mergeObjects,
414 'GlyphCount': sum,
415 'AttachPoint': sumLists,
416}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400417
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500418# XXX Renumber MarkFilterSets of lookups
419otTables.MarkGlyphSetsDef.mergeMap = {
420 'MarkSetTableFormat': equal,
421 'MarkSetCount': sum,
422 'Coverage': sumLists,
423}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400424
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500425otTables.GDEF.mergeMap = {
426 '*': mergeObjects,
427 'Version': max,
428}
429
Behdad Esfahbod26429342013-12-19 11:53:47 -0500430otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
431 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500432 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500433}
434
435ttLib.getTableClass('GDEF').mergeMap = \
436ttLib.getTableClass('GSUB').mergeMap = \
437ttLib.getTableClass('GPOS').mergeMap = \
438ttLib.getTableClass('BASE').mergeMap = \
439ttLib.getTableClass('JSTF').mergeMap = \
440ttLib.getTableClass('MATH').mergeMap = \
441{
Behdad Esfahbod0d5fcf42014-03-28 14:37:32 -0700442 'tableTag': onlyExisting(equal), # XXX clean me up
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500443 'table': mergeObjects,
444}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400445
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700446@_add_method(ttLib.getTableClass('GSUB'))
447def merge(self, m, tables):
448
449 assert len(tables) == len(m.duplicateGlyphsPerFont)
450 for i,(table,dups) in enumerate(zip(tables, m.duplicateGlyphsPerFont)):
451 if not dups: continue
452 assert (table is not None and table is not NotImplemented), "Have duplicates to resolve for font %d but no GSUB" % (i + 1)
Behdad Esfahbod14f13a92014-04-02 18:54:53 -0700453 lookupMap = dict((id(v),v) for v in table.table.LookupList.Lookup)
454 featureMap = dict((id(v),v) for v in table.table.FeatureList.FeatureRecord)
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700455 synthFeature = None
456 synthLookup = None
457 for script in table.table.ScriptList.ScriptRecord:
458 if script.ScriptTag == 'DFLT': continue # XXX
459 for langsys in [script.Script.DefaultLangSys] + [l.LangSys for l in script.Script.LangSysRecord]:
460 feature = [featureMap[v] for v in langsys.FeatureIndex if featureMap[v].FeatureTag == 'locl']
461 assert len(feature) <= 1
462 if feature:
463 feature = feature[0]
464 else:
465 if not synthFeature:
466 synthFeature = otTables.FeatureRecord()
467 synthFeature.FeatureTag = 'locl'
468 f = synthFeature.Feature = otTables.Feature()
469 f.FeatureParams = None
470 f.LookupCount = 0
471 f.LookupListIndex = []
472 langsys.FeatureIndex.append(id(synthFeature))
473 featureMap[id(synthFeature)] = synthFeature
474 langsys.FeatureIndex.sort(key=lambda v: featureMap[v].FeatureTag)
475 table.table.FeatureList.FeatureRecord.append(synthFeature)
476 table.table.FeatureList.FeatureCount += 1
477 feature = synthFeature
478
479 if not synthLookup:
480 subtable = otTables.SingleSubst()
481 subtable.mapping = dups
482 synthLookup = otTables.Lookup()
483 synthLookup.LookupFlag = 0
484 synthLookup.LookupType = 1
485 synthLookup.SubTableCount = 1
486 synthLookup.SubTable = [subtable]
487 table.table.LookupList.Lookup.append(synthLookup)
488 table.table.LookupList.LookupCount += 1
489
490 feature.Feature.LookupListIndex[:0] = [id(synthLookup)]
491 feature.Feature.LookupCount += 1
492
493
494 DefaultTable.merge(self, m, tables)
495 return self
496
497
Behdad Esfahbod26429342013-12-19 11:53:47 -0500498
Behdad Esfahbod50803312014-02-10 18:14:37 -0500499@_add_method(otTables.SingleSubst,
500 otTables.MultipleSubst,
501 otTables.AlternateSubst,
502 otTables.LigatureSubst,
503 otTables.ReverseChainSingleSubst,
504 otTables.SinglePos,
505 otTables.PairPos,
506 otTables.CursivePos,
507 otTables.MarkBasePos,
508 otTables.MarkLigPos,
509 otTables.MarkMarkPos)
510def mapLookups(self, lookupMap):
511 pass
512
513# Copied and trimmed down from subset.py
514@_add_method(otTables.ContextSubst,
515 otTables.ChainContextSubst,
516 otTables.ContextPos,
517 otTables.ChainContextPos)
518def __classify_context(self):
519
520 class ContextHelper(object):
521 def __init__(self, klass, Format):
522 if klass.__name__.endswith('Subst'):
523 Typ = 'Sub'
524 Type = 'Subst'
525 else:
526 Typ = 'Pos'
527 Type = 'Pos'
528 if klass.__name__.startswith('Chain'):
529 Chain = 'Chain'
530 else:
531 Chain = ''
532 ChainTyp = Chain+Typ
533
534 self.Typ = Typ
535 self.Type = Type
536 self.Chain = Chain
537 self.ChainTyp = ChainTyp
538
539 self.LookupRecord = Type+'LookupRecord'
540
541 if Format == 1:
542 self.Rule = ChainTyp+'Rule'
543 self.RuleSet = ChainTyp+'RuleSet'
544 elif Format == 2:
545 self.Rule = ChainTyp+'ClassRule'
546 self.RuleSet = ChainTyp+'ClassSet'
547
548 if self.Format not in [1, 2, 3]:
549 return None # Don't shoot the messenger; let it go
550 if not hasattr(self.__class__, "__ContextHelpers"):
551 self.__class__.__ContextHelpers = {}
552 if self.Format not in self.__class__.__ContextHelpers:
553 helper = ContextHelper(self.__class__, self.Format)
554 self.__class__.__ContextHelpers[self.Format] = helper
555 return self.__class__.__ContextHelpers[self.Format]
556
557
558@_add_method(otTables.ContextSubst,
559 otTables.ChainContextSubst,
560 otTables.ContextPos,
561 otTables.ChainContextPos)
562def mapLookups(self, lookupMap):
563 c = self.__classify_context()
564
565 if self.Format in [1, 2]:
566 for rs in getattr(self, c.RuleSet):
567 if not rs: continue
568 for r in getattr(rs, c.Rule):
569 if not r: continue
570 for ll in getattr(r, c.LookupRecord):
571 if not ll: continue
572 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
573 elif self.Format == 3:
574 for ll in getattr(self, c.LookupRecord):
575 if not ll: continue
576 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
577 else:
578 assert 0, "unknown format: %s" % self.Format
579
580@_add_method(otTables.Lookup)
581def mapLookups(self, lookupMap):
582 for st in self.SubTable:
583 if not st: continue
584 st.mapLookups(lookupMap)
585
586@_add_method(otTables.LookupList)
587def mapLookups(self, lookupMap):
588 for l in self.Lookup:
589 if not l: continue
590 l.mapLookups(lookupMap)
591
Behdad Esfahbod26429342013-12-19 11:53:47 -0500592@_add_method(otTables.Feature)
593def mapLookups(self, lookupMap):
594 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
595
596@_add_method(otTables.FeatureList)
597def mapLookups(self, lookupMap):
598 for f in self.FeatureRecord:
599 if not f or not f.Feature: continue
600 f.Feature.mapLookups(lookupMap)
601
602@_add_method(otTables.DefaultLangSys,
603 otTables.LangSys)
604def mapFeatures(self, featureMap):
605 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
606 if self.ReqFeatureIndex != 65535:
607 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
608
609@_add_method(otTables.Script)
610def mapFeatures(self, featureMap):
611 if self.DefaultLangSys:
612 self.DefaultLangSys.mapFeatures(featureMap)
613 for l in self.LangSysRecord:
614 if not l or not l.LangSys: continue
615 l.LangSys.mapFeatures(featureMap)
616
617@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500618def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500619 for s in self.ScriptRecord:
620 if not s or not s.Script: continue
621 s.Script.mapFeatures(featureMap)
622
623
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400624class Options(object):
625
626 class UnknownOptionError(Exception):
627 pass
628
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400629 def __init__(self, **kwargs):
630
631 self.set(**kwargs)
632
633 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500634 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400635 if not hasattr(self, k):
636 raise self.UnknownOptionError("Unknown option '%s'" % k)
637 setattr(self, k, v)
638
639 def parse_opts(self, argv, ignore_unknown=False):
640 ret = []
641 opts = {}
642 for a in argv:
643 orig_a = a
644 if not a.startswith('--'):
645 ret.append(a)
646 continue
647 a = a[2:]
648 i = a.find('=')
649 op = '='
650 if i == -1:
651 if a.startswith("no-"):
652 k = a[3:]
653 v = False
654 else:
655 k = a
656 v = True
657 else:
658 k = a[:i]
659 if k[-1] in "-+":
660 op = k[-1]+'=' # Ops is '-=' or '+=' now.
661 k = k[:-1]
662 v = a[i+1:]
663 k = k.replace('-', '_')
664 if not hasattr(self, k):
665 if ignore_unknown == True or k in ignore_unknown:
666 ret.append(orig_a)
667 continue
668 else:
669 raise self.UnknownOptionError("Unknown option '%s'" % a)
670
671 ov = getattr(self, k)
672 if isinstance(ov, bool):
673 v = bool(v)
674 elif isinstance(ov, int):
675 v = int(v)
676 elif isinstance(ov, list):
677 vv = v.split(',')
678 if vv == ['']:
679 vv = []
680 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
681 if op == '=':
682 v = vv
683 elif op == '+=':
684 v = ov
685 v.extend(vv)
686 elif op == '-=':
687 v = ov
688 for x in vv:
689 if x in v:
690 v.remove(x)
691 else:
692 assert 0
693
694 opts[k] = v
695 self.set(**opts)
696
697 return ret
698
699
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500700class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400701
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400702 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400703
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400704 if not log:
705 log = Logger()
706 if not options:
707 options = Options()
708
709 self.options = options
710 self.log = log
711
712 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400713
714 mega = ttLib.TTFont()
715
716 #
717 # Settle on a mega glyph order.
718 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400719 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400720 glyphOrders = [font.getGlyphOrder() for font in fonts]
721 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
722 # Reload fonts and set new glyph names on them.
723 # TODO Is it necessary to reload font? I think it is. At least
724 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400725 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400726 for font,glyphOrder in zip(fonts, glyphOrders):
727 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400728 mega.setGlyphOrder(megaGlyphOrder)
729
Behdad Esfahbod26429342013-12-19 11:53:47 -0500730 for font in fonts:
731 self._preMerge(font)
732
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700733 self.duplicateGlyphsPerFont = [{} for f in fonts]
734
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500735 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400736 allTags.remove('GlyphOrder')
Behdad Esfahbodd0903e32014-03-28 16:39:49 -0700737 allTags.remove('cmap')
738 allTags.remove('GSUB')
739 allTags = ['cmap', 'GSUB'] + list(allTags)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400740 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400741
Behdad Esfahbod26429342013-12-19 11:53:47 -0500742 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod2772d842014-03-28 15:37:18 -0700743
744 clazz = ttLib.getTableClass(tag)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500745 table = clazz(tag).merge(self, tables)
Behdad Esfahbod2772d842014-03-28 15:37:18 -0700746 # XXX Clean this up and use: table = mergeObjects(tables)
747
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500748 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400749 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400750 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400751 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500752 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400753 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400754
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700755 del self.duplicateGlyphsPerFont
756
Behdad Esfahbod26429342013-12-19 11:53:47 -0500757 self._postMerge(mega)
758
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400759 return mega
760
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400761 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400762 """Modifies passed-in glyphOrders to reflect new glyph names.
763 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400764 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400765 # TODO Even this simplistic numbering can result in conflicts.
766 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400767 mega = []
768 for n,glyphOrder in enumerate(glyphOrders):
769 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500770 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400771 glyphOrder[i] = glyphName
772 mega.append(glyphName)
773 return mega
774
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500775 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500776 # Right now we don't use self at all. Will use in the future
777 # for options and logging.
778
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500779 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800780 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800781 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500782 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800783 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500784 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500785 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500786 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500787 raise Exception("Don't know how to merge key %s of class %s" %
788 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500789 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800790 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500791 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
792 if value is not NotImplemented:
793 setattr(returnTable, key, value)
794
795 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800796
Behdad Esfahbod26429342013-12-19 11:53:47 -0500797 def _preMerge(self, font):
798
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700799 # Map indices to references
800
Behdad Esfahbod26429342013-12-19 11:53:47 -0500801 GDEF = font.get('GDEF')
802 GSUB = font.get('GSUB')
803 GPOS = font.get('GPOS')
804
805 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500806 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500807
Behdad Esfahbod50803312014-02-10 18:14:37 -0500808 if t.table.LookupList:
Behdad Esfahbod14f13a92014-04-02 18:54:53 -0700809 lookupMap = dict((i,id(v)) for i,v in enumerate(t.table.LookupList.Lookup))
Behdad Esfahbod50803312014-02-10 18:14:37 -0500810 t.table.LookupList.mapLookups(lookupMap)
811 if t.table.FeatureList:
812 # XXX Handle present FeatureList but absent LookupList
813 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500814
815 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbod14f13a92014-04-02 18:54:53 -0700816 featureMap = dict((i,id(v)) for i,v in enumerate(t.table.FeatureList.FeatureRecord))
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500817 t.table.ScriptList.mapFeatures(featureMap)
818
819 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500820 # TODO FeatureParams nameIDs
821
822 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500823
Behdad Esfahbod8fec6872014-03-28 17:41:01 -0700824 # Map references back to indices
825
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500826 GDEF = font.get('GDEF')
827 GSUB = font.get('GSUB')
828 GPOS = font.get('GPOS')
829
830 for t in [GSUB, GPOS]:
831 if not t: continue
832
Behdad Esfahbod50803312014-02-10 18:14:37 -0500833 if t.table.LookupList:
Behdad Esfahbod14f13a92014-04-02 18:54:53 -0700834 lookupMap = dict((id(v),i) for i,v in enumerate(t.table.LookupList.Lookup))
Behdad Esfahbod50803312014-02-10 18:14:37 -0500835 t.table.LookupList.mapLookups(lookupMap)
836 if t.table.FeatureList:
837 # XXX Handle present FeatureList but absent LookupList
838 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500839
840 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbod50803312014-02-10 18:14:37 -0500841 # XXX Handle present ScriptList but absent FeatureList
Behdad Esfahbod14f13a92014-04-02 18:54:53 -0700842 featureMap = dict((id(v),i) for i,v in enumerate(t.table.FeatureList.FeatureRecord))
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500843 t.table.ScriptList.mapFeatures(featureMap)
844
845 # TODO GDEF/Lookup MarkFilteringSets
846 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500847
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400848
849class Logger(object):
850
851 def __init__(self, verbose=False, xml=False, timing=False):
852 self.verbose = verbose
853 self.xml = xml
854 self.timing = timing
855 self.last_time = self.start_time = time.time()
856
857 def parse_opts(self, argv):
858 argv = argv[:]
859 for v in ['verbose', 'xml', 'timing']:
860 if "--"+v in argv:
861 setattr(self, v, True)
862 argv.remove("--"+v)
863 return argv
864
865 def __call__(self, *things):
866 if not self.verbose:
867 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500868 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400869
870 def lapse(self, *things):
871 if not self.timing:
872 return
873 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500874 print("Took %0.3fs to %s" %(new_time - self.last_time,
875 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400876 self.last_time = new_time
877
878 def font(self, font, file=sys.stdout):
879 if not self.xml:
880 return
881 from fontTools.misc import xmlWriter
882 writer = xmlWriter.XMLWriter(file)
883 font.disassembleInstructions = False # Work around ttLib bug
884 for tag in font.keys():
885 writer.begintag(tag)
886 writer.newline()
887 font[tag].toXML(writer, font)
888 writer.endtag(tag)
889 writer.newline()
890
891
892__all__ = [
893 'Options',
894 'Merger',
895 'Logger',
896 'main'
897]
898
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400899def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400900
901 log = Logger()
902 args = log.parse_opts(args)
903
904 options = Options()
905 args = options.parse_opts(args)
906
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400907 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500908 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400909 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400910
911 merger = Merger(options=options, log=log)
912 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400913 outfile = 'merged.ttf'
914 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400915 log.lapse("compile and save font")
916
917 log.last_time = log.start_time
918 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400919
920if __name__ == "__main__":
921 main(sys.argv[1:])