blob: e368356bcb393cc4c81da6fb23dea449be12226a [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 Esfahbod92fd5662013-12-19 04:56:50 -050060def nonnone(func):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050061 """Returns a filter func that when called with a list,
62 only calls func on the non-None items of the list, and
63 only so if there's at least one non-None item in the
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050064 list. Otherwise returns None."""
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050065
66 def wrapper(lst):
67 items = [item for item in lst if item is not None]
68 return func(items) if items else None
69
70 return wrapper
71
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050072def implemented(func):
73 """Returns a filter func that when called with a list,
74 only calls func on the non-NotImplemented items of the list,
75 and only so if there's at least one item remaining.
76 Otherwise returns NotImplemented."""
77
78 def wrapper(lst):
79 items = [item for item in lst if item is not NotImplemented]
80 return func(items) if items else NotImplemented
81
82 return wrapper
83
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050084def sumLists(lst):
85 l = []
86 for item in lst:
87 l.extend(item)
88 return l
89
90def sumDicts(lst):
91 d = {}
92 for item in lst:
93 d.update(item)
94 return d
95
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050096def mergeObjects(lst):
97 lst = [item for item in lst if item is not None and item is not NotImplemented]
98 if not lst:
99 return None # Not all can be NotImplemented
100
101 clazz = lst[0].__class__
102 assert all(type(item) == clazz for item in lst), lst
103 logic = clazz.mergeMap
104 returnTable = clazz()
105
106 allKeys = set.union(set(), *(vars(table).keys() for table in lst))
107 for key in allKeys:
108 try:
109 mergeLogic = logic[key]
110 except KeyError:
111 try:
112 mergeLogic = logic['*']
113 except KeyError:
114 raise Exception("Don't know how to merge key %s of class %s" %
115 (key, clazz.__name__))
116 if mergeLogic is NotImplemented:
117 continue
118 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
119 if value is not NotImplemented:
120 setattr(returnTable, key, value)
121
122 return returnTable
123
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800124def mergeBits(logic, lst):
125 lst = list(lst)
126 returnValue = 0
127 for bitNumber in range(logic['size']):
128 try:
129 mergeLogic = logic[bitNumber]
130 except KeyError:
131 try:
132 mergeLogic = logic['*']
133 except KeyError:
134 raise Exception("Don't know how to merge bit %s" % bitNumber)
135 shiftedBit = 1 << bitNumber
136 mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst)
137 returnValue |= mergedValue << bitNumber
138 return returnValue
139
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 Esfahbod92fd5662013-12-19 04:56:50 -0500147 return m.mergeObjects(self, self.mergeMap, tables)
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500148
149ttLib.getTableClass('maxp').mergeMap = {
150 '*': max,
151 'tableTag': equal,
152 'tableVersion': equal,
153 'numGlyphs': sum,
154 'maxStorage': max, # FIXME: may need to be changed to sum
155 'maxFunctionDefs': sum,
156 'maxInstructionDefs': sum,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400157 # TODO When we correctly merge hinting data, update these values:
158 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500159}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400160
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800161headFlagsMergeMap = {
162 'size': 16,
163 '*': bitwise_or,
164 1: bitwise_and, # Baseline at y = 0
165 2: bitwise_and, # lsb at x = 0
166 3: bitwise_and, # Force ppem to integer values. FIXME?
167 5: bitwise_and, # Font is vertical
168 6: lambda bit: 0, # Always set to zero
169 11: bitwise_and, # Font data is 'lossless'
170 13: bitwise_and, # Optimized for ClearType
171 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
172 15: lambda bit: 0, # Always set to zero
173}
174
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500175ttLib.getTableClass('head').mergeMap = {
176 'tableTag': equal,
177 'tableVersion': max,
178 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500179 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500180 'magicNumber': equal,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800181 'flags': lambda lst: mergeBits(headFlagsMergeMap, lst),
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500182 'unitsPerEm': equal,
183 'created': current_time,
184 'modified': current_time,
185 'xMin': min,
186 'yMin': min,
187 'xMax': max,
188 'yMax': max,
189 'macStyle': first,
190 'lowestRecPPEM': max,
191 'fontDirectionHint': lambda lst: 2,
192 'indexToLocFormat': recalculate,
193 'glyphDataFormat': equal,
194}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400195
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500196ttLib.getTableClass('hhea').mergeMap = {
197 '*': equal,
198 'tableTag': equal,
199 'tableVersion': max,
200 'ascent': max,
201 'descent': min,
202 'lineGap': max,
203 'advanceWidthMax': max,
204 'minLeftSideBearing': min,
205 'minRightSideBearing': min,
206 'xMaxExtent': max,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800207 'caretSlopeRise': first,
208 'caretSlopeRun': first,
209 'caretOffset': first,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500210 'numberOfHMetrics': recalculate,
211}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400212
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800213os2FsTypeMergeMap = {
214 'size': 16,
215 '*': lambda bit: 0,
216 1: bitwise_or, # no embedding permitted
217 2: bitwise_and, # allow previewing and printing documents
218 3: bitwise_and, # allow editing documents
219 8: bitwise_or, # no subsetting permitted
220 9: bitwise_or, # no embedding of outlines permitted
221}
222
223def mergeOs2FsType(lst):
224 lst = list(lst)
225 if all(item == 0 for item in lst):
226 return 0
227
228 # Compute least restrictive logic for each fsType value
229 for i in range(len(lst)):
230 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
231 if lst[i] & 0x000C:
232 lst[i] &= ~0x0002
233 # set bit 2 (allow previewing) if bit 3 is set (allow editing)
234 elif lst[i] & 0x0008:
235 lst[i] |= 0x0004
236 # set bits 2 and 3 if everything is allowed
237 elif lst[i] == 0:
238 lst[i] = 0x000C
239
240 fsType = mergeBits(os2FsTypeMergeMap, lst)
241 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
242 if fsType & 0x0002:
243 fsType &= ~0x000C
244 return fsType
245
246
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500247ttLib.getTableClass('OS/2').mergeMap = {
248 '*': first,
249 'tableTag': equal,
250 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500251 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800252 'fsType': mergeOs2FsType, # Will be overwritten
253 'panose': first, # FIXME: should really be the first Latin font
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500254 'ulUnicodeRange1': bitwise_or,
255 'ulUnicodeRange2': bitwise_or,
256 'ulUnicodeRange3': bitwise_or,
257 'ulUnicodeRange4': bitwise_or,
258 'fsFirstCharIndex': min,
259 'fsLastCharIndex': max,
260 'sTypoAscender': max,
261 'sTypoDescender': min,
262 'sTypoLineGap': max,
263 'usWinAscent': max,
264 'usWinDescent': max,
265 'ulCodePageRange1': bitwise_or,
266 'ulCodePageRange2': bitwise_or,
267 'usMaxContex': max,
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500268 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500269}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400270
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800271@_add_method(ttLib.getTableClass('OS/2'))
272def merge(self, m, tables):
273 DefaultTable.merge(self, m, tables)
274 if self.version < 2:
275 # bits 8 and 9 are reserved and should be set to zero
276 self.fsType &= ~0x0300
277 if self.version >= 3:
278 # Only one of bits 1, 2, and 3 may be set. We already take
279 # care of bit 1 implications in mergeOs2FsType. So unset
280 # bit 2 if bit 3 is already set.
281 if self.fsType & 0x0008:
282 self.fsType &= ~0x0004
283 return self
284
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500285ttLib.getTableClass('post').mergeMap = {
286 '*': first,
287 'tableTag': equal,
288 'formatType': max,
289 'isFixedPitch': min,
290 'minMemType42': max,
291 'maxMemType42': lambda lst: 0,
292 'minMemType1': max,
293 'maxMemType1': lambda lst: 0,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500294 'mapping': implemented(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500295 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500296}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400297
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500298ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
299 'tableTag': equal,
300 'metrics': sumDicts,
301}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400302
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800303ttLib.getTableClass('gasp').mergeMap = {
304 'tableTag': equal,
305 'version': max,
306 'gaspRange': first, # FIXME? Appears irreconcilable
307}
308
309ttLib.getTableClass('name').mergeMap = {
310 'tableTag': equal,
311 'names': first, # FIXME? Does mixing name records make sense?
312}
313
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500314ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500315 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500316 'tableTag': equal,
317}
318
319ttLib.getTableClass('glyf').mergeMap = {
320 'tableTag': equal,
321 'glyphs': sumDicts,
322 'glyphOrder': sumLists,
323}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400324
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400325@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500326def merge(self, m, tables):
327 for table in tables:
Behdad Esfahbod43650332013-09-20 16:33:33 -0400328 for g in table.glyphs.values():
329 # Drop hints for now, since we don't remap
330 # functions / CVT values.
331 g.removeHinting()
332 # 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 Esfahbod92fd5662013-12-19 04:56:50 -0500338ttLib.getTableClass('prep').mergeMap = NotImplemented
339ttLib.getTableClass('fpgm').mergeMap = NotImplemented
340ttLib.getTableClass('cvt ').mergeMap = NotImplemented
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 Esfahbod71294de2013-09-19 19:43:17 -0400346 if t.platformID == 3 and t.platEncID in [1, 10]]
347 # 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,
368 'ScriptRecord': sumLists,
369}
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{
425 'tableTag': equal,
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
430@_add_method(otTables.Feature)
431def mapLookups(self, lookupMap):
432 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
433
434@_add_method(otTables.FeatureList)
435def mapLookups(self, lookupMap):
436 for f in self.FeatureRecord:
437 if not f or not f.Feature: continue
438 f.Feature.mapLookups(lookupMap)
439
440@_add_method(otTables.DefaultLangSys,
441 otTables.LangSys)
442def mapFeatures(self, featureMap):
443 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
444 if self.ReqFeatureIndex != 65535:
445 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
446
447@_add_method(otTables.Script)
448def mapFeatures(self, featureMap):
449 if self.DefaultLangSys:
450 self.DefaultLangSys.mapFeatures(featureMap)
451 for l in self.LangSysRecord:
452 if not l or not l.LangSys: continue
453 l.LangSys.mapFeatures(featureMap)
454
455@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500456def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500457 for s in self.ScriptRecord:
458 if not s or not s.Script: continue
459 s.Script.mapFeatures(featureMap)
460
461
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400462class Options(object):
463
464 class UnknownOptionError(Exception):
465 pass
466
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400467 def __init__(self, **kwargs):
468
469 self.set(**kwargs)
470
471 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500472 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400473 if not hasattr(self, k):
474 raise self.UnknownOptionError("Unknown option '%s'" % k)
475 setattr(self, k, v)
476
477 def parse_opts(self, argv, ignore_unknown=False):
478 ret = []
479 opts = {}
480 for a in argv:
481 orig_a = a
482 if not a.startswith('--'):
483 ret.append(a)
484 continue
485 a = a[2:]
486 i = a.find('=')
487 op = '='
488 if i == -1:
489 if a.startswith("no-"):
490 k = a[3:]
491 v = False
492 else:
493 k = a
494 v = True
495 else:
496 k = a[:i]
497 if k[-1] in "-+":
498 op = k[-1]+'=' # Ops is '-=' or '+=' now.
499 k = k[:-1]
500 v = a[i+1:]
501 k = k.replace('-', '_')
502 if not hasattr(self, k):
503 if ignore_unknown == True or k in ignore_unknown:
504 ret.append(orig_a)
505 continue
506 else:
507 raise self.UnknownOptionError("Unknown option '%s'" % a)
508
509 ov = getattr(self, k)
510 if isinstance(ov, bool):
511 v = bool(v)
512 elif isinstance(ov, int):
513 v = int(v)
514 elif isinstance(ov, list):
515 vv = v.split(',')
516 if vv == ['']:
517 vv = []
518 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
519 if op == '=':
520 v = vv
521 elif op == '+=':
522 v = ov
523 v.extend(vv)
524 elif op == '-=':
525 v = ov
526 for x in vv:
527 if x in v:
528 v.remove(x)
529 else:
530 assert 0
531
532 opts[k] = v
533 self.set(**opts)
534
535 return ret
536
537
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500538class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400539
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400540 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400541
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400542 if not log:
543 log = Logger()
544 if not options:
545 options = Options()
546
547 self.options = options
548 self.log = log
549
550 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400551
552 mega = ttLib.TTFont()
553
554 #
555 # Settle on a mega glyph order.
556 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400557 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400558 glyphOrders = [font.getGlyphOrder() for font in fonts]
559 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
560 # Reload fonts and set new glyph names on them.
561 # TODO Is it necessary to reload font? I think it is. At least
562 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400563 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400564 for font,glyphOrder in zip(fonts, glyphOrders):
565 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400566 mega.setGlyphOrder(megaGlyphOrder)
567
Behdad Esfahbod26429342013-12-19 11:53:47 -0500568 for font in fonts:
569 self._preMerge(font)
570
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500571 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400572 allTags.remove('GlyphOrder')
573 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400574
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400575 clazz = ttLib.getTableClass(tag)
576
Behdad Esfahbod26429342013-12-19 11:53:47 -0500577 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500578 table = clazz(tag).merge(self, tables)
579 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400580 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400581 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400582 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500583 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400584 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400585
Behdad Esfahbod26429342013-12-19 11:53:47 -0500586 self._postMerge(mega)
587
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400588 return mega
589
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400590 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400591 """Modifies passed-in glyphOrders to reflect new glyph names.
592 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400593 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400594 # TODO Even this simplistic numbering can result in conflicts.
595 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400596 mega = []
597 for n,glyphOrder in enumerate(glyphOrders):
598 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500599 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400600 glyphOrder[i] = glyphName
601 mega.append(glyphName)
602 return mega
603
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500604 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500605 # Right now we don't use self at all. Will use in the future
606 # for options and logging.
607
608 if logic is NotImplemented:
609 return NotImplemented
610
611 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800612 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800613 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500614 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800615 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500616 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500617 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500618 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500619 raise Exception("Don't know how to merge key %s of class %s" %
620 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500621 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800622 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500623 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
624 if value is not NotImplemented:
625 setattr(returnTable, key, value)
626
627 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800628
Behdad Esfahbod26429342013-12-19 11:53:47 -0500629 def _preMerge(self, font):
630
Behdad Esfahbod26429342013-12-19 11:53:47 -0500631 GDEF = font.get('GDEF')
632 GSUB = font.get('GSUB')
633 GPOS = font.get('GPOS')
634
635 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500636 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500637
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500638 if t.table.LookupList and t.table.FeatureList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500639 lookupMap = {i:id(v) for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500640 t.table.FeatureList.mapLookups(lookupMap)
641
642 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500643 featureMap = {i:id(v) for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500644 t.table.ScriptList.mapFeatures(featureMap)
645
646 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500647 # TODO FeatureParams nameIDs
648
649 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500650
651 GDEF = font.get('GDEF')
652 GSUB = font.get('GSUB')
653 GPOS = font.get('GPOS')
654
655 for t in [GSUB, GPOS]:
656 if not t: continue
657
658 if t.table.LookupList and t.table.FeatureList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500659 lookupMap = {id(v):i for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500660 t.table.FeatureList.mapLookups(lookupMap)
661
662 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500663 featureMap = {id(v):i for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500664 t.table.ScriptList.mapFeatures(featureMap)
665
666 # TODO GDEF/Lookup MarkFilteringSets
667 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500668
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400669
670class Logger(object):
671
672 def __init__(self, verbose=False, xml=False, timing=False):
673 self.verbose = verbose
674 self.xml = xml
675 self.timing = timing
676 self.last_time = self.start_time = time.time()
677
678 def parse_opts(self, argv):
679 argv = argv[:]
680 for v in ['verbose', 'xml', 'timing']:
681 if "--"+v in argv:
682 setattr(self, v, True)
683 argv.remove("--"+v)
684 return argv
685
686 def __call__(self, *things):
687 if not self.verbose:
688 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500689 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400690
691 def lapse(self, *things):
692 if not self.timing:
693 return
694 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500695 print("Took %0.3fs to %s" %(new_time - self.last_time,
696 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400697 self.last_time = new_time
698
699 def font(self, font, file=sys.stdout):
700 if not self.xml:
701 return
702 from fontTools.misc import xmlWriter
703 writer = xmlWriter.XMLWriter(file)
704 font.disassembleInstructions = False # Work around ttLib bug
705 for tag in font.keys():
706 writer.begintag(tag)
707 writer.newline()
708 font[tag].toXML(writer, font)
709 writer.endtag(tag)
710 writer.newline()
711
712
713__all__ = [
714 'Options',
715 'Merger',
716 'Logger',
717 'main'
718]
719
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400720def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400721
722 log = Logger()
723 args = log.parse_opts(args)
724
725 options = Options()
726 args = options.parse_opts(args)
727
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400728 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500729 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400730 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400731
732 merger = Merger(options=options, log=log)
733 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400734 outfile = 'merged.ttf'
735 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400736 log.lapse("compile and save font")
737
738 log.last_time = log.start_time
739 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400740
741if __name__ == "__main__":
742 main(sys.argv[1:])