blob: d71ed77f6c4fc520158bea03fbe7227aa72ac235 [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
Roozbeh Pournader642eaf12013-12-21 01:04:18 -080050def bitwise_and(lst):
51 return reduce(operator.and_, lst)
52
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080053def bitwise_or(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050054 return reduce(operator.or_, lst)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080055
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050056def avg_int(lst):
57 lst = list(lst)
58 return sum(lst) // len(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040059
Behdad Esfahbod23366322013-12-31 18:12:28 +080060def implementedFilter(func):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050061 """Returns a filter func that when called with a list,
62 only calls func on the non-NotImplemented items of the list,
63 and only so if there's at least one item remaining.
64 Otherwise returns NotImplemented."""
65
66 def wrapper(lst):
67 items = [item for item in lst if item is not NotImplemented]
68 return func(items) if items else NotImplemented
69
70 return wrapper
71
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050072def sumLists(lst):
73 l = []
74 for item in lst:
75 l.extend(item)
76 return l
77
78def sumDicts(lst):
79 d = {}
80 for item in lst:
81 d.update(item)
82 return d
83
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050084def mergeObjects(lst):
85 lst = [item for item in lst if item is not None and item is not NotImplemented]
86 if not lst:
87 return None # Not all can be NotImplemented
88
89 clazz = lst[0].__class__
90 assert all(type(item) == clazz for item in lst), lst
91 logic = clazz.mergeMap
92 returnTable = clazz()
93
94 allKeys = set.union(set(), *(vars(table).keys() for table in lst))
95 for key in allKeys:
96 try:
97 mergeLogic = logic[key]
98 except KeyError:
99 try:
100 mergeLogic = logic['*']
101 except KeyError:
102 raise Exception("Don't know how to merge key %s of class %s" %
103 (key, clazz.__name__))
104 if mergeLogic is NotImplemented:
105 continue
106 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
107 if value is not NotImplemented:
108 setattr(returnTable, key, value)
109
110 return returnTable
111
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800112def mergeBits(logic, lst):
113 lst = list(lst)
114 returnValue = 0
115 for bitNumber in range(logic['size']):
116 try:
117 mergeLogic = logic[bitNumber]
118 except KeyError:
119 try:
120 mergeLogic = logic['*']
121 except KeyError:
122 raise Exception("Don't know how to merge bit %s" % bitNumber)
123 shiftedBit = 1 << bitNumber
124 mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst)
125 returnValue |= mergedValue << bitNumber
126 return returnValue
127
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500128
129@_add_method(DefaultTable, allowDefaultTable=True)
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500130def merge(self, m, tables):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500131 if not hasattr(self, 'mergeMap'):
132 m.log("Don't know how to merge '%s'." % self.tableTag)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500133 return NotImplemented
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500134
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500135 return m.mergeObjects(self, self.mergeMap, tables)
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500136
137ttLib.getTableClass('maxp').mergeMap = {
138 '*': max,
139 'tableTag': equal,
140 'tableVersion': equal,
141 'numGlyphs': sum,
142 'maxStorage': max, # FIXME: may need to be changed to sum
143 'maxFunctionDefs': sum,
144 'maxInstructionDefs': sum,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400145 # TODO When we correctly merge hinting data, update these values:
146 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500147}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400148
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800149headFlagsMergeMap = {
150 'size': 16,
151 '*': bitwise_or,
152 1: bitwise_and, # Baseline at y = 0
153 2: bitwise_and, # lsb at x = 0
154 3: bitwise_and, # Force ppem to integer values. FIXME?
155 5: bitwise_and, # Font is vertical
156 6: lambda bit: 0, # Always set to zero
157 11: bitwise_and, # Font data is 'lossless'
158 13: bitwise_and, # Optimized for ClearType
159 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
160 15: lambda bit: 0, # Always set to zero
161}
162
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500163ttLib.getTableClass('head').mergeMap = {
164 'tableTag': equal,
165 'tableVersion': max,
166 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500167 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500168 'magicNumber': equal,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800169 'flags': lambda lst: mergeBits(headFlagsMergeMap, lst),
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500170 'unitsPerEm': equal,
171 'created': current_time,
172 'modified': current_time,
173 'xMin': min,
174 'yMin': min,
175 'xMax': max,
176 'yMax': max,
177 'macStyle': first,
178 'lowestRecPPEM': max,
179 'fontDirectionHint': lambda lst: 2,
180 'indexToLocFormat': recalculate,
181 'glyphDataFormat': equal,
182}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400183
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500184ttLib.getTableClass('hhea').mergeMap = {
185 '*': equal,
186 'tableTag': equal,
187 'tableVersion': max,
188 'ascent': max,
189 'descent': min,
190 'lineGap': max,
191 'advanceWidthMax': max,
192 'minLeftSideBearing': min,
193 'minRightSideBearing': min,
194 'xMaxExtent': max,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800195 'caretSlopeRise': first,
196 'caretSlopeRun': first,
197 'caretOffset': first,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500198 'numberOfHMetrics': recalculate,
199}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400200
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800201os2FsTypeMergeMap = {
202 'size': 16,
203 '*': lambda bit: 0,
204 1: bitwise_or, # no embedding permitted
205 2: bitwise_and, # allow previewing and printing documents
206 3: bitwise_and, # allow editing documents
207 8: bitwise_or, # no subsetting permitted
208 9: bitwise_or, # no embedding of outlines permitted
209}
210
211def mergeOs2FsType(lst):
212 lst = list(lst)
213 if all(item == 0 for item in lst):
214 return 0
215
216 # Compute least restrictive logic for each fsType value
217 for i in range(len(lst)):
218 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
219 if lst[i] & 0x000C:
220 lst[i] &= ~0x0002
221 # set bit 2 (allow previewing) if bit 3 is set (allow editing)
222 elif lst[i] & 0x0008:
223 lst[i] |= 0x0004
224 # set bits 2 and 3 if everything is allowed
225 elif lst[i] == 0:
226 lst[i] = 0x000C
227
228 fsType = mergeBits(os2FsTypeMergeMap, lst)
229 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
230 if fsType & 0x0002:
231 fsType &= ~0x000C
232 return fsType
233
234
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500235ttLib.getTableClass('OS/2').mergeMap = {
236 '*': first,
237 'tableTag': equal,
238 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500239 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800240 'fsType': mergeOs2FsType, # Will be overwritten
241 'panose': first, # FIXME: should really be the first Latin font
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500242 'ulUnicodeRange1': bitwise_or,
243 'ulUnicodeRange2': bitwise_or,
244 'ulUnicodeRange3': bitwise_or,
245 'ulUnicodeRange4': bitwise_or,
246 'fsFirstCharIndex': min,
247 'fsLastCharIndex': max,
248 'sTypoAscender': max,
249 'sTypoDescender': min,
250 'sTypoLineGap': max,
251 'usWinAscent': max,
252 'usWinDescent': max,
253 'ulCodePageRange1': bitwise_or,
254 'ulCodePageRange2': bitwise_or,
255 'usMaxContex': max,
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500256 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500257}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400258
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800259@_add_method(ttLib.getTableClass('OS/2'))
260def merge(self, m, tables):
261 DefaultTable.merge(self, m, tables)
262 if self.version < 2:
263 # bits 8 and 9 are reserved and should be set to zero
264 self.fsType &= ~0x0300
265 if self.version >= 3:
266 # Only one of bits 1, 2, and 3 may be set. We already take
267 # care of bit 1 implications in mergeOs2FsType. So unset
268 # bit 2 if bit 3 is already set.
269 if self.fsType & 0x0008:
270 self.fsType &= ~0x0004
271 return self
272
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500273ttLib.getTableClass('post').mergeMap = {
274 '*': first,
275 'tableTag': equal,
276 'formatType': max,
277 'isFixedPitch': min,
278 'minMemType42': max,
279 'maxMemType42': lambda lst: 0,
280 'minMemType1': max,
281 'maxMemType1': lambda lst: 0,
Behdad Esfahbod23366322013-12-31 18:12:28 +0800282 'mapping': implementedFilter(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500283 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500284}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400285
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500286ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
287 'tableTag': equal,
288 'metrics': sumDicts,
289}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400290
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800291ttLib.getTableClass('gasp').mergeMap = {
292 'tableTag': equal,
293 'version': max,
294 'gaspRange': first, # FIXME? Appears irreconcilable
295}
296
297ttLib.getTableClass('name').mergeMap = {
298 'tableTag': equal,
299 'names': first, # FIXME? Does mixing name records make sense?
300}
301
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500302ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500303 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500304 'tableTag': equal,
305}
306
307ttLib.getTableClass('glyf').mergeMap = {
308 'tableTag': equal,
309 'glyphs': sumDicts,
310 'glyphOrder': sumLists,
311}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400312
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400313@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500314def merge(self, m, tables):
315 for table in tables:
Behdad Esfahbod43650332013-09-20 16:33:33 -0400316 for g in table.glyphs.values():
317 # Drop hints for now, since we don't remap
318 # functions / CVT values.
319 g.removeHinting()
320 # Expand composite glyphs to load their
321 # composite glyph names.
322 if g.isComposite():
323 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500324 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400325
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500326ttLib.getTableClass('prep').mergeMap = NotImplemented
327ttLib.getTableClass('fpgm').mergeMap = NotImplemented
328ttLib.getTableClass('cvt ').mergeMap = NotImplemented
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400329
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400330@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500331def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400332 # TODO Handle format=14.
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500333 cmapTables = [t for table in tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400334 if t.platformID == 3 and t.platEncID in [1, 10]]
335 # TODO Better handle format-4 and format-12 coexisting in same font.
336 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400337 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400338 assert all(t.format in [4, 12] for t in cmapTables)
339 format = max(t.format for t in cmapTables)
340 cmapTable = module.cmap_classes[format](format)
341 cmapTable.cmap = {}
342 cmapTable.platformID = 3
343 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
344 cmapTable.language = 0
345 for table in cmapTables:
346 # TODO handle duplicates.
347 cmapTable.cmap.update(table.cmap)
348 self.tableVersion = 0
349 self.tables = [cmapTable]
350 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500351 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400352
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400353
Behdad Esfahbod26429342013-12-19 11:53:47 -0500354otTables.ScriptList.mergeMap = {
355 'ScriptCount': sum,
Behdad Esfahbod23366322013-12-31 18:12:28 +0800356 'ScriptRecord': sumLists, # XXX sort
Behdad Esfahbod26429342013-12-19 11:53:47 -0500357}
358
359otTables.FeatureList.mergeMap = {
360 'FeatureCount': sum,
361 'FeatureRecord': sumLists,
362}
363
364otTables.LookupList.mergeMap = {
365 'LookupCount': sum,
366 'Lookup': sumLists,
367}
368
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500369otTables.Coverage.mergeMap = {
370 'glyphs': sumLists,
371}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400372
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500373otTables.ClassDef.mergeMap = {
374 'classDefs': sumDicts,
375}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400376
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500377otTables.LigCaretList.mergeMap = {
378 'Coverage': mergeObjects,
379 'LigGlyphCount': sum,
380 'LigGlyph': sumLists,
381}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400382
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500383otTables.AttachList.mergeMap = {
384 'Coverage': mergeObjects,
385 'GlyphCount': sum,
386 'AttachPoint': sumLists,
387}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400388
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500389# XXX Renumber MarkFilterSets of lookups
390otTables.MarkGlyphSetsDef.mergeMap = {
391 'MarkSetTableFormat': equal,
392 'MarkSetCount': sum,
393 'Coverage': sumLists,
394}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400395
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500396otTables.GDEF.mergeMap = {
397 '*': mergeObjects,
398 'Version': max,
399}
400
Behdad Esfahbod26429342013-12-19 11:53:47 -0500401otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
402 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500403 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500404}
405
406ttLib.getTableClass('GDEF').mergeMap = \
407ttLib.getTableClass('GSUB').mergeMap = \
408ttLib.getTableClass('GPOS').mergeMap = \
409ttLib.getTableClass('BASE').mergeMap = \
410ttLib.getTableClass('JSTF').mergeMap = \
411ttLib.getTableClass('MATH').mergeMap = \
412{
413 'tableTag': equal,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500414 'table': mergeObjects,
415}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400416
Behdad Esfahbod26429342013-12-19 11:53:47 -0500417
418@_add_method(otTables.Feature)
419def mapLookups(self, lookupMap):
420 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
421
422@_add_method(otTables.FeatureList)
423def mapLookups(self, lookupMap):
424 for f in self.FeatureRecord:
425 if not f or not f.Feature: continue
426 f.Feature.mapLookups(lookupMap)
427
428@_add_method(otTables.DefaultLangSys,
429 otTables.LangSys)
430def mapFeatures(self, featureMap):
431 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
432 if self.ReqFeatureIndex != 65535:
433 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
434
435@_add_method(otTables.Script)
436def mapFeatures(self, featureMap):
437 if self.DefaultLangSys:
438 self.DefaultLangSys.mapFeatures(featureMap)
439 for l in self.LangSysRecord:
440 if not l or not l.LangSys: continue
441 l.LangSys.mapFeatures(featureMap)
442
443@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500444def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500445 for s in self.ScriptRecord:
446 if not s or not s.Script: continue
447 s.Script.mapFeatures(featureMap)
448
449
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400450class Options(object):
451
452 class UnknownOptionError(Exception):
453 pass
454
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400455 def __init__(self, **kwargs):
456
457 self.set(**kwargs)
458
459 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500460 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400461 if not hasattr(self, k):
462 raise self.UnknownOptionError("Unknown option '%s'" % k)
463 setattr(self, k, v)
464
465 def parse_opts(self, argv, ignore_unknown=False):
466 ret = []
467 opts = {}
468 for a in argv:
469 orig_a = a
470 if not a.startswith('--'):
471 ret.append(a)
472 continue
473 a = a[2:]
474 i = a.find('=')
475 op = '='
476 if i == -1:
477 if a.startswith("no-"):
478 k = a[3:]
479 v = False
480 else:
481 k = a
482 v = True
483 else:
484 k = a[:i]
485 if k[-1] in "-+":
486 op = k[-1]+'=' # Ops is '-=' or '+=' now.
487 k = k[:-1]
488 v = a[i+1:]
489 k = k.replace('-', '_')
490 if not hasattr(self, k):
491 if ignore_unknown == True or k in ignore_unknown:
492 ret.append(orig_a)
493 continue
494 else:
495 raise self.UnknownOptionError("Unknown option '%s'" % a)
496
497 ov = getattr(self, k)
498 if isinstance(ov, bool):
499 v = bool(v)
500 elif isinstance(ov, int):
501 v = int(v)
502 elif isinstance(ov, list):
503 vv = v.split(',')
504 if vv == ['']:
505 vv = []
506 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
507 if op == '=':
508 v = vv
509 elif op == '+=':
510 v = ov
511 v.extend(vv)
512 elif op == '-=':
513 v = ov
514 for x in vv:
515 if x in v:
516 v.remove(x)
517 else:
518 assert 0
519
520 opts[k] = v
521 self.set(**opts)
522
523 return ret
524
525
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500526class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400527
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400528 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400529
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400530 if not log:
531 log = Logger()
532 if not options:
533 options = Options()
534
535 self.options = options
536 self.log = log
537
538 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400539
540 mega = ttLib.TTFont()
541
542 #
543 # Settle on a mega glyph order.
544 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400545 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400546 glyphOrders = [font.getGlyphOrder() for font in fonts]
547 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
548 # Reload fonts and set new glyph names on them.
549 # TODO Is it necessary to reload font? I think it is. At least
550 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400551 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400552 for font,glyphOrder in zip(fonts, glyphOrders):
553 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400554 mega.setGlyphOrder(megaGlyphOrder)
555
Behdad Esfahbod26429342013-12-19 11:53:47 -0500556 for font in fonts:
557 self._preMerge(font)
558
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500559 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400560 allTags.remove('GlyphOrder')
561 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400562
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400563 clazz = ttLib.getTableClass(tag)
564
Behdad Esfahbod26429342013-12-19 11:53:47 -0500565 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500566 table = clazz(tag).merge(self, tables)
567 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400568 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400569 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400570 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500571 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400572 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400573
Behdad Esfahbod26429342013-12-19 11:53:47 -0500574 self._postMerge(mega)
575
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400576 return mega
577
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400578 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400579 """Modifies passed-in glyphOrders to reflect new glyph names.
580 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400581 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400582 # TODO Even this simplistic numbering can result in conflicts.
583 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400584 mega = []
585 for n,glyphOrder in enumerate(glyphOrders):
586 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500587 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400588 glyphOrder[i] = glyphName
589 mega.append(glyphName)
590 return mega
591
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500592 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500593 # Right now we don't use self at all. Will use in the future
594 # for options and logging.
595
596 if logic is NotImplemented:
597 return NotImplemented
598
599 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800600 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800601 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500602 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800603 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500604 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500605 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500606 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500607 raise Exception("Don't know how to merge key %s of class %s" %
608 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500609 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800610 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500611 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
612 if value is not NotImplemented:
613 setattr(returnTable, key, value)
614
615 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800616
Behdad Esfahbod26429342013-12-19 11:53:47 -0500617 def _preMerge(self, font):
618
Behdad Esfahbod26429342013-12-19 11:53:47 -0500619 GDEF = font.get('GDEF')
620 GSUB = font.get('GSUB')
621 GPOS = font.get('GPOS')
622
623 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500624 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500625
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500626 if t.table.LookupList and t.table.FeatureList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500627 lookupMap = {i:id(v) for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500628 t.table.FeatureList.mapLookups(lookupMap)
629
630 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500631 featureMap = {i:id(v) for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500632 t.table.ScriptList.mapFeatures(featureMap)
633
634 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500635 # TODO FeatureParams nameIDs
636
637 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500638
639 GDEF = font.get('GDEF')
640 GSUB = font.get('GSUB')
641 GPOS = font.get('GPOS')
642
643 for t in [GSUB, GPOS]:
644 if not t: continue
645
646 if t.table.LookupList and t.table.FeatureList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500647 lookupMap = {id(v):i for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500648 t.table.FeatureList.mapLookups(lookupMap)
649
650 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500651 featureMap = {id(v):i for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500652 t.table.ScriptList.mapFeatures(featureMap)
653
654 # TODO GDEF/Lookup MarkFilteringSets
655 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500656
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400657
658class Logger(object):
659
660 def __init__(self, verbose=False, xml=False, timing=False):
661 self.verbose = verbose
662 self.xml = xml
663 self.timing = timing
664 self.last_time = self.start_time = time.time()
665
666 def parse_opts(self, argv):
667 argv = argv[:]
668 for v in ['verbose', 'xml', 'timing']:
669 if "--"+v in argv:
670 setattr(self, v, True)
671 argv.remove("--"+v)
672 return argv
673
674 def __call__(self, *things):
675 if not self.verbose:
676 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500677 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400678
679 def lapse(self, *things):
680 if not self.timing:
681 return
682 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500683 print("Took %0.3fs to %s" %(new_time - self.last_time,
684 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400685 self.last_time = new_time
686
687 def font(self, font, file=sys.stdout):
688 if not self.xml:
689 return
690 from fontTools.misc import xmlWriter
691 writer = xmlWriter.XMLWriter(file)
692 font.disassembleInstructions = False # Work around ttLib bug
693 for tag in font.keys():
694 writer.begintag(tag)
695 writer.newline()
696 font[tag].toXML(writer, font)
697 writer.endtag(tag)
698 writer.newline()
699
700
701__all__ = [
702 'Options',
703 'Merger',
704 'Logger',
705 'main'
706]
707
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400708def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400709
710 log = Logger()
711 args = log.parse_opts(args)
712
713 options = Options()
714 args = options.parse_opts(args)
715
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400716 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500717 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400718 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400719
720 merger = Merger(options=options, log=log)
721 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400722 outfile = 'merged.ttf'
723 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400724 log.lapse("compile and save font")
725
726 log.last_time = log.start_time
727 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400728
729if __name__ == "__main__":
730 main(sys.argv[1:])