blob: a432a3cea5a05e2d457794447ca04bf9d7d0ffed [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):
86 lst = [item for item in lst if item is not None and item is not NotImplemented]
87 if not lst:
88 return None # Not all can be NotImplemented
89
90 clazz = lst[0].__class__
91 assert all(type(item) == clazz for item in lst), lst
92 logic = clazz.mergeMap
93 returnTable = clazz()
Behdad Esfahbod82c54632014-03-28 14:41:53 -070094 returnDict = {}
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050095
96 allKeys = set.union(set(), *(vars(table).keys() for table in lst))
97 for key in allKeys:
98 try:
99 mergeLogic = logic[key]
100 except KeyError:
101 try:
102 mergeLogic = logic['*']
103 except KeyError:
104 raise Exception("Don't know how to merge key %s of class %s" %
105 (key, clazz.__name__))
106 if mergeLogic is NotImplemented:
107 continue
108 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
109 if value is not NotImplemented:
Behdad Esfahbod82c54632014-03-28 14:41:53 -0700110 returnDict[key] = value
111
112 returnTable.__dict__ = returnDict
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500113
114 return returnTable
115
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700116def mergeBits(bitmap, lst):
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800117 lst = list(lst)
118 returnValue = 0
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700119 for bitNumber in range(bitmap['size']):
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800120 try:
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700121 mergeLogic = bitmap[bitNumber]
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800122 except KeyError:
123 try:
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700124 mergeLogic = bitmap['*']
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800125 except KeyError:
126 raise Exception("Don't know how to merge bit %s" % bitNumber)
127 shiftedBit = 1 << bitNumber
128 mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst)
129 returnValue |= mergedValue << bitNumber
130 return returnValue
131
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500132
133@_add_method(DefaultTable, allowDefaultTable=True)
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500134def merge(self, m, tables):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500135 if not hasattr(self, 'mergeMap'):
136 m.log("Don't know how to merge '%s'." % self.tableTag)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500137 return NotImplemented
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500138
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500139 logic = self.mergeMap
140
141 if isinstance(logic, dict):
142 return m.mergeObjects(self, self.mergeMap, tables)
143 else:
144 return logic(tables)
145
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500146
147ttLib.getTableClass('maxp').mergeMap = {
148 '*': max,
149 'tableTag': equal,
150 'tableVersion': equal,
151 'numGlyphs': sum,
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500152 'maxStorage': first,
153 'maxFunctionDefs': first,
154 'maxInstructionDefs': first,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400155 # TODO When we correctly merge hinting data, update these values:
156 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500157}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400158
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700159headFlagsMergeBitMap = {
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800160 'size': 16,
161 '*': bitwise_or,
162 1: bitwise_and, # Baseline at y = 0
163 2: bitwise_and, # lsb at x = 0
164 3: bitwise_and, # Force ppem to integer values. FIXME?
165 5: bitwise_and, # Font is vertical
166 6: lambda bit: 0, # Always set to zero
167 11: bitwise_and, # Font data is 'lossless'
168 13: bitwise_and, # Optimized for ClearType
169 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
170 15: lambda bit: 0, # Always set to zero
171}
172
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500173ttLib.getTableClass('head').mergeMap = {
174 'tableTag': equal,
175 'tableVersion': max,
176 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500177 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500178 'magicNumber': equal,
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700179 'flags': lambda lst: mergeBits(headFlagsMergeBitMap, lst),
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500180 'unitsPerEm': equal,
181 'created': current_time,
182 'modified': current_time,
183 'xMin': min,
184 'yMin': min,
185 'xMax': max,
186 'yMax': max,
187 'macStyle': first,
188 'lowestRecPPEM': max,
189 'fontDirectionHint': lambda lst: 2,
190 'indexToLocFormat': recalculate,
191 'glyphDataFormat': equal,
192}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400193
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500194ttLib.getTableClass('hhea').mergeMap = {
195 '*': equal,
196 'tableTag': equal,
197 'tableVersion': max,
198 'ascent': max,
199 'descent': min,
200 'lineGap': max,
201 'advanceWidthMax': max,
202 'minLeftSideBearing': min,
203 'minRightSideBearing': min,
204 'xMaxExtent': max,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800205 'caretSlopeRise': first,
206 'caretSlopeRun': first,
207 'caretOffset': first,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500208 'numberOfHMetrics': recalculate,
209}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400210
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700211os2FsTypeMergeBitMap = {
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800212 'size': 16,
213 '*': lambda bit: 0,
214 1: bitwise_or, # no embedding permitted
215 2: bitwise_and, # allow previewing and printing documents
216 3: bitwise_and, # allow editing documents
217 8: bitwise_or, # no subsetting permitted
218 9: bitwise_or, # no embedding of outlines permitted
219}
220
221def mergeOs2FsType(lst):
222 lst = list(lst)
223 if all(item == 0 for item in lst):
224 return 0
225
226 # Compute least restrictive logic for each fsType value
227 for i in range(len(lst)):
228 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
229 if lst[i] & 0x000C:
230 lst[i] &= ~0x0002
231 # set bit 2 (allow previewing) if bit 3 is set (allow editing)
232 elif lst[i] & 0x0008:
233 lst[i] |= 0x0004
234 # set bits 2 and 3 if everything is allowed
235 elif lst[i] == 0:
236 lst[i] = 0x000C
237
Behdad Esfahbodb8039e22014-03-28 13:54:37 -0700238 fsType = mergeBits(os2FsTypeMergeBitMap, lst)
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800239 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
240 if fsType & 0x0002:
241 fsType &= ~0x000C
242 return fsType
243
244
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500245ttLib.getTableClass('OS/2').mergeMap = {
246 '*': first,
247 'tableTag': equal,
248 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500249 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800250 'fsType': mergeOs2FsType, # Will be overwritten
251 'panose': first, # FIXME: should really be the first Latin font
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500252 'ulUnicodeRange1': bitwise_or,
253 'ulUnicodeRange2': bitwise_or,
254 'ulUnicodeRange3': bitwise_or,
255 'ulUnicodeRange4': bitwise_or,
256 'fsFirstCharIndex': min,
257 'fsLastCharIndex': max,
258 'sTypoAscender': max,
259 'sTypoDescender': min,
260 'sTypoLineGap': max,
261 'usWinAscent': max,
262 'usWinDescent': max,
Behdad Esfahbod0e235be2014-03-28 14:56:27 -0700263 # Version 2,3,4
Behdad Esfahbod77654212014-03-28 14:48:09 -0700264 'ulCodePageRange1': onlyExisting(bitwise_or),
265 'ulCodePageRange2': onlyExisting(bitwise_or),
266 'usMaxContex': onlyExisting(max),
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500267 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500268}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400269
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800270@_add_method(ttLib.getTableClass('OS/2'))
271def merge(self, m, tables):
272 DefaultTable.merge(self, m, tables)
273 if self.version < 2:
274 # bits 8 and 9 are reserved and should be set to zero
275 self.fsType &= ~0x0300
276 if self.version >= 3:
277 # Only one of bits 1, 2, and 3 may be set. We already take
278 # care of bit 1 implications in mergeOs2FsType. So unset
279 # bit 2 if bit 3 is already set.
280 if self.fsType & 0x0008:
281 self.fsType &= ~0x0004
282 return self
283
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500284ttLib.getTableClass('post').mergeMap = {
285 '*': first,
286 'tableTag': equal,
287 'formatType': max,
288 'isFixedPitch': min,
289 'minMemType42': max,
290 'maxMemType42': lambda lst: 0,
291 'minMemType1': max,
292 'maxMemType1': lambda lst: 0,
Behdad Esfahbod0d5fcf42014-03-28 14:37:32 -0700293 'mapping': onlyExisting(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500294 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500295}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400296
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500297ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
298 'tableTag': equal,
299 'metrics': sumDicts,
300}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400301
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800302ttLib.getTableClass('gasp').mergeMap = {
303 'tableTag': equal,
304 'version': max,
305 'gaspRange': first, # FIXME? Appears irreconcilable
306}
307
308ttLib.getTableClass('name').mergeMap = {
309 'tableTag': equal,
310 'names': first, # FIXME? Does mixing name records make sense?
311}
312
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500313ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500314 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500315 'tableTag': equal,
316}
317
318ttLib.getTableClass('glyf').mergeMap = {
319 'tableTag': equal,
320 'glyphs': sumDicts,
321 'glyphOrder': sumLists,
322}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400323
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400324@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500325def merge(self, m, tables):
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500326 for i,table in enumerate(tables):
Behdad Esfahbod43650332013-09-20 16:33:33 -0400327 for g in table.glyphs.values():
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500328 if i:
329 # Drop hints for all but first font, since
330 # we don't map functions / CVT values.
331 g.removeHinting()
Behdad Esfahbod43650332013-09-20 16:33:33 -0400332 # Expand composite glyphs to load their
333 # composite glyph names.
334 if g.isComposite():
335 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500336 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400337
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500338ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
339ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
340ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400341
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400342@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500343def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400344 # TODO Handle format=14.
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500345 cmapTables = [t for table in tables for t in table.tables
Behdad Esfahbodf480c7c2014-03-12 12:18:47 -0700346 if t.isUnicode()]
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400347 # TODO Better handle format-4 and format-12 coexisting in same font.
348 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400349 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400350 assert all(t.format in [4, 12] for t in cmapTables)
351 format = max(t.format for t in cmapTables)
352 cmapTable = module.cmap_classes[format](format)
353 cmapTable.cmap = {}
354 cmapTable.platformID = 3
355 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
356 cmapTable.language = 0
357 for table in cmapTables:
358 # TODO handle duplicates.
359 cmapTable.cmap.update(table.cmap)
360 self.tableVersion = 0
361 self.tables = [cmapTable]
362 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500363 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400364
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400365
Behdad Esfahbod26429342013-12-19 11:53:47 -0500366otTables.ScriptList.mergeMap = {
367 'ScriptCount': sum,
Behdad Esfahbod972af5a2013-12-31 18:16:36 +0800368 'ScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.ScriptTag),
Behdad Esfahbod26429342013-12-19 11:53:47 -0500369}
370
371otTables.FeatureList.mergeMap = {
372 'FeatureCount': sum,
373 'FeatureRecord': sumLists,
374}
375
376otTables.LookupList.mergeMap = {
377 'LookupCount': sum,
378 'Lookup': sumLists,
379}
380
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500381otTables.Coverage.mergeMap = {
382 'glyphs': sumLists,
383}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400384
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500385otTables.ClassDef.mergeMap = {
386 'classDefs': sumDicts,
387}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400388
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500389otTables.LigCaretList.mergeMap = {
390 'Coverage': mergeObjects,
391 'LigGlyphCount': sum,
392 'LigGlyph': sumLists,
393}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400394
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500395otTables.AttachList.mergeMap = {
396 'Coverage': mergeObjects,
397 'GlyphCount': sum,
398 'AttachPoint': sumLists,
399}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400400
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500401# XXX Renumber MarkFilterSets of lookups
402otTables.MarkGlyphSetsDef.mergeMap = {
403 'MarkSetTableFormat': equal,
404 'MarkSetCount': sum,
405 'Coverage': sumLists,
406}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400407
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500408otTables.GDEF.mergeMap = {
409 '*': mergeObjects,
410 'Version': max,
411}
412
Behdad Esfahbod26429342013-12-19 11:53:47 -0500413otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
414 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500415 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500416}
417
418ttLib.getTableClass('GDEF').mergeMap = \
419ttLib.getTableClass('GSUB').mergeMap = \
420ttLib.getTableClass('GPOS').mergeMap = \
421ttLib.getTableClass('BASE').mergeMap = \
422ttLib.getTableClass('JSTF').mergeMap = \
423ttLib.getTableClass('MATH').mergeMap = \
424{
Behdad Esfahbod0d5fcf42014-03-28 14:37:32 -0700425 'tableTag': onlyExisting(equal), # XXX clean me up
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500426 'table': mergeObjects,
427}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400428
Behdad Esfahbod26429342013-12-19 11:53:47 -0500429
Behdad Esfahbod50803312014-02-10 18:14:37 -0500430@_add_method(otTables.SingleSubst,
431 otTables.MultipleSubst,
432 otTables.AlternateSubst,
433 otTables.LigatureSubst,
434 otTables.ReverseChainSingleSubst,
435 otTables.SinglePos,
436 otTables.PairPos,
437 otTables.CursivePos,
438 otTables.MarkBasePos,
439 otTables.MarkLigPos,
440 otTables.MarkMarkPos)
441def mapLookups(self, lookupMap):
442 pass
443
444# Copied and trimmed down from subset.py
445@_add_method(otTables.ContextSubst,
446 otTables.ChainContextSubst,
447 otTables.ContextPos,
448 otTables.ChainContextPos)
449def __classify_context(self):
450
451 class ContextHelper(object):
452 def __init__(self, klass, Format):
453 if klass.__name__.endswith('Subst'):
454 Typ = 'Sub'
455 Type = 'Subst'
456 else:
457 Typ = 'Pos'
458 Type = 'Pos'
459 if klass.__name__.startswith('Chain'):
460 Chain = 'Chain'
461 else:
462 Chain = ''
463 ChainTyp = Chain+Typ
464
465 self.Typ = Typ
466 self.Type = Type
467 self.Chain = Chain
468 self.ChainTyp = ChainTyp
469
470 self.LookupRecord = Type+'LookupRecord'
471
472 if Format == 1:
473 self.Rule = ChainTyp+'Rule'
474 self.RuleSet = ChainTyp+'RuleSet'
475 elif Format == 2:
476 self.Rule = ChainTyp+'ClassRule'
477 self.RuleSet = ChainTyp+'ClassSet'
478
479 if self.Format not in [1, 2, 3]:
480 return None # Don't shoot the messenger; let it go
481 if not hasattr(self.__class__, "__ContextHelpers"):
482 self.__class__.__ContextHelpers = {}
483 if self.Format not in self.__class__.__ContextHelpers:
484 helper = ContextHelper(self.__class__, self.Format)
485 self.__class__.__ContextHelpers[self.Format] = helper
486 return self.__class__.__ContextHelpers[self.Format]
487
488
489@_add_method(otTables.ContextSubst,
490 otTables.ChainContextSubst,
491 otTables.ContextPos,
492 otTables.ChainContextPos)
493def mapLookups(self, lookupMap):
494 c = self.__classify_context()
495
496 if self.Format in [1, 2]:
497 for rs in getattr(self, c.RuleSet):
498 if not rs: continue
499 for r in getattr(rs, c.Rule):
500 if not r: continue
501 for ll in getattr(r, c.LookupRecord):
502 if not ll: continue
503 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
504 elif self.Format == 3:
505 for ll in getattr(self, c.LookupRecord):
506 if not ll: continue
507 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
508 else:
509 assert 0, "unknown format: %s" % self.Format
510
511@_add_method(otTables.Lookup)
512def mapLookups(self, lookupMap):
513 for st in self.SubTable:
514 if not st: continue
515 st.mapLookups(lookupMap)
516
517@_add_method(otTables.LookupList)
518def mapLookups(self, lookupMap):
519 for l in self.Lookup:
520 if not l: continue
521 l.mapLookups(lookupMap)
522
Behdad Esfahbod26429342013-12-19 11:53:47 -0500523@_add_method(otTables.Feature)
524def mapLookups(self, lookupMap):
525 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
526
527@_add_method(otTables.FeatureList)
528def mapLookups(self, lookupMap):
529 for f in self.FeatureRecord:
530 if not f or not f.Feature: continue
531 f.Feature.mapLookups(lookupMap)
532
533@_add_method(otTables.DefaultLangSys,
534 otTables.LangSys)
535def mapFeatures(self, featureMap):
536 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
537 if self.ReqFeatureIndex != 65535:
538 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
539
540@_add_method(otTables.Script)
541def mapFeatures(self, featureMap):
542 if self.DefaultLangSys:
543 self.DefaultLangSys.mapFeatures(featureMap)
544 for l in self.LangSysRecord:
545 if not l or not l.LangSys: continue
546 l.LangSys.mapFeatures(featureMap)
547
548@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500549def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500550 for s in self.ScriptRecord:
551 if not s or not s.Script: continue
552 s.Script.mapFeatures(featureMap)
553
554
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400555class Options(object):
556
557 class UnknownOptionError(Exception):
558 pass
559
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400560 def __init__(self, **kwargs):
561
562 self.set(**kwargs)
563
564 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500565 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400566 if not hasattr(self, k):
567 raise self.UnknownOptionError("Unknown option '%s'" % k)
568 setattr(self, k, v)
569
570 def parse_opts(self, argv, ignore_unknown=False):
571 ret = []
572 opts = {}
573 for a in argv:
574 orig_a = a
575 if not a.startswith('--'):
576 ret.append(a)
577 continue
578 a = a[2:]
579 i = a.find('=')
580 op = '='
581 if i == -1:
582 if a.startswith("no-"):
583 k = a[3:]
584 v = False
585 else:
586 k = a
587 v = True
588 else:
589 k = a[:i]
590 if k[-1] in "-+":
591 op = k[-1]+'=' # Ops is '-=' or '+=' now.
592 k = k[:-1]
593 v = a[i+1:]
594 k = k.replace('-', '_')
595 if not hasattr(self, k):
596 if ignore_unknown == True or k in ignore_unknown:
597 ret.append(orig_a)
598 continue
599 else:
600 raise self.UnknownOptionError("Unknown option '%s'" % a)
601
602 ov = getattr(self, k)
603 if isinstance(ov, bool):
604 v = bool(v)
605 elif isinstance(ov, int):
606 v = int(v)
607 elif isinstance(ov, list):
608 vv = v.split(',')
609 if vv == ['']:
610 vv = []
611 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
612 if op == '=':
613 v = vv
614 elif op == '+=':
615 v = ov
616 v.extend(vv)
617 elif op == '-=':
618 v = ov
619 for x in vv:
620 if x in v:
621 v.remove(x)
622 else:
623 assert 0
624
625 opts[k] = v
626 self.set(**opts)
627
628 return ret
629
630
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500631class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400632
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400633 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400634
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400635 if not log:
636 log = Logger()
637 if not options:
638 options = Options()
639
640 self.options = options
641 self.log = log
642
643 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400644
645 mega = ttLib.TTFont()
646
647 #
648 # Settle on a mega glyph order.
649 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400650 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400651 glyphOrders = [font.getGlyphOrder() for font in fonts]
652 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
653 # Reload fonts and set new glyph names on them.
654 # TODO Is it necessary to reload font? I think it is. At least
655 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400656 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400657 for font,glyphOrder in zip(fonts, glyphOrders):
658 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400659 mega.setGlyphOrder(megaGlyphOrder)
660
Behdad Esfahbod26429342013-12-19 11:53:47 -0500661 for font in fonts:
662 self._preMerge(font)
663
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500664 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400665 allTags.remove('GlyphOrder')
666 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400667
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400668 clazz = ttLib.getTableClass(tag)
669
Behdad Esfahbod26429342013-12-19 11:53:47 -0500670 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500671 table = clazz(tag).merge(self, tables)
672 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400673 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400674 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400675 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500676 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400677 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400678
Behdad Esfahbod26429342013-12-19 11:53:47 -0500679 self._postMerge(mega)
680
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400681 return mega
682
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400683 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400684 """Modifies passed-in glyphOrders to reflect new glyph names.
685 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400686 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400687 # TODO Even this simplistic numbering can result in conflicts.
688 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400689 mega = []
690 for n,glyphOrder in enumerate(glyphOrders):
691 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500692 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400693 glyphOrder[i] = glyphName
694 mega.append(glyphName)
695 return mega
696
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500697 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500698 # Right now we don't use self at all. Will use in the future
699 # for options and logging.
700
701 if logic is NotImplemented:
702 return NotImplemented
703
704 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800705 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800706 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500707 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800708 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500709 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500710 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500711 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500712 raise Exception("Don't know how to merge key %s of class %s" %
713 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500714 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800715 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500716 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
717 if value is not NotImplemented:
718 setattr(returnTable, key, value)
719
720 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800721
Behdad Esfahbod26429342013-12-19 11:53:47 -0500722 def _preMerge(self, font):
723
Behdad Esfahbod26429342013-12-19 11:53:47 -0500724 GDEF = font.get('GDEF')
725 GSUB = font.get('GSUB')
726 GPOS = font.get('GPOS')
727
728 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500729 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500730
Behdad Esfahbod50803312014-02-10 18:14:37 -0500731 if t.table.LookupList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500732 lookupMap = {i:id(v) for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod50803312014-02-10 18:14:37 -0500733 t.table.LookupList.mapLookups(lookupMap)
734 if t.table.FeatureList:
735 # XXX Handle present FeatureList but absent LookupList
736 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500737
738 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500739 featureMap = {i:id(v) for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500740 t.table.ScriptList.mapFeatures(featureMap)
741
742 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500743 # TODO FeatureParams nameIDs
744
745 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500746
747 GDEF = font.get('GDEF')
748 GSUB = font.get('GSUB')
749 GPOS = font.get('GPOS')
750
751 for t in [GSUB, GPOS]:
752 if not t: continue
753
Behdad Esfahbod50803312014-02-10 18:14:37 -0500754 if t.table.LookupList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500755 lookupMap = {id(v):i for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod50803312014-02-10 18:14:37 -0500756 t.table.LookupList.mapLookups(lookupMap)
757 if t.table.FeatureList:
758 # XXX Handle present FeatureList but absent LookupList
759 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500760
761 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbod50803312014-02-10 18:14:37 -0500762 # XXX Handle present ScriptList but absent FeatureList
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500763 featureMap = {id(v):i for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500764 t.table.ScriptList.mapFeatures(featureMap)
765
766 # TODO GDEF/Lookup MarkFilteringSets
767 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500768
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400769
770class Logger(object):
771
772 def __init__(self, verbose=False, xml=False, timing=False):
773 self.verbose = verbose
774 self.xml = xml
775 self.timing = timing
776 self.last_time = self.start_time = time.time()
777
778 def parse_opts(self, argv):
779 argv = argv[:]
780 for v in ['verbose', 'xml', 'timing']:
781 if "--"+v in argv:
782 setattr(self, v, True)
783 argv.remove("--"+v)
784 return argv
785
786 def __call__(self, *things):
787 if not self.verbose:
788 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500789 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400790
791 def lapse(self, *things):
792 if not self.timing:
793 return
794 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500795 print("Took %0.3fs to %s" %(new_time - self.last_time,
796 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400797 self.last_time = new_time
798
799 def font(self, font, file=sys.stdout):
800 if not self.xml:
801 return
802 from fontTools.misc import xmlWriter
803 writer = xmlWriter.XMLWriter(file)
804 font.disassembleInstructions = False # Work around ttLib bug
805 for tag in font.keys():
806 writer.begintag(tag)
807 writer.newline()
808 font[tag].toXML(writer, font)
809 writer.endtag(tag)
810 writer.newline()
811
812
813__all__ = [
814 'Options',
815 'Merger',
816 'Logger',
817 'main'
818]
819
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400820def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400821
822 log = Logger()
823 args = log.parse_opts(args)
824
825 options = Options()
826 args = options.parse_opts(args)
827
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400828 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500829 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400830 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400831
832 merger = Merger(options=options, log=log)
833 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400834 outfile = 'merged.ttf'
835 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400836 log.lapse("compile and save font")
837
838 log.last_time = log.start_time
839 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400840
841if __name__ == "__main__":
842 main(sys.argv[1:])