blob: 3e5acec40693bd8efd49c87b21cb53fad8fd6711 [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):
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 Esfahbod27c71f92014-01-27 21:01:45 -0500135 logic = self.mergeMap
136
137 if isinstance(logic, dict):
138 return m.mergeObjects(self, self.mergeMap, tables)
139 else:
140 return logic(tables)
141
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500142
143ttLib.getTableClass('maxp').mergeMap = {
144 '*': max,
145 'tableTag': equal,
146 'tableVersion': equal,
147 'numGlyphs': sum,
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500148 'maxStorage': first,
149 'maxFunctionDefs': first,
150 'maxInstructionDefs': first,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400151 # TODO When we correctly merge hinting data, update these values:
152 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500153}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400154
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800155headFlagsMergeMap = {
156 'size': 16,
157 '*': bitwise_or,
158 1: bitwise_and, # Baseline at y = 0
159 2: bitwise_and, # lsb at x = 0
160 3: bitwise_and, # Force ppem to integer values. FIXME?
161 5: bitwise_and, # Font is vertical
162 6: lambda bit: 0, # Always set to zero
163 11: bitwise_and, # Font data is 'lossless'
164 13: bitwise_and, # Optimized for ClearType
165 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
166 15: lambda bit: 0, # Always set to zero
167}
168
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500169ttLib.getTableClass('head').mergeMap = {
170 'tableTag': equal,
171 'tableVersion': max,
172 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500173 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500174 'magicNumber': equal,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800175 'flags': lambda lst: mergeBits(headFlagsMergeMap, lst),
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500176 'unitsPerEm': equal,
177 'created': current_time,
178 'modified': current_time,
179 'xMin': min,
180 'yMin': min,
181 'xMax': max,
182 'yMax': max,
183 'macStyle': first,
184 'lowestRecPPEM': max,
185 'fontDirectionHint': lambda lst: 2,
186 'indexToLocFormat': recalculate,
187 'glyphDataFormat': equal,
188}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400189
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500190ttLib.getTableClass('hhea').mergeMap = {
191 '*': equal,
192 'tableTag': equal,
193 'tableVersion': max,
194 'ascent': max,
195 'descent': min,
196 'lineGap': max,
197 'advanceWidthMax': max,
198 'minLeftSideBearing': min,
199 'minRightSideBearing': min,
200 'xMaxExtent': max,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800201 'caretSlopeRise': first,
202 'caretSlopeRun': first,
203 'caretOffset': first,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500204 'numberOfHMetrics': recalculate,
205}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400206
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800207os2FsTypeMergeMap = {
208 'size': 16,
209 '*': lambda bit: 0,
210 1: bitwise_or, # no embedding permitted
211 2: bitwise_and, # allow previewing and printing documents
212 3: bitwise_and, # allow editing documents
213 8: bitwise_or, # no subsetting permitted
214 9: bitwise_or, # no embedding of outlines permitted
215}
216
217def mergeOs2FsType(lst):
218 lst = list(lst)
219 if all(item == 0 for item in lst):
220 return 0
221
222 # Compute least restrictive logic for each fsType value
223 for i in range(len(lst)):
224 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
225 if lst[i] & 0x000C:
226 lst[i] &= ~0x0002
227 # set bit 2 (allow previewing) if bit 3 is set (allow editing)
228 elif lst[i] & 0x0008:
229 lst[i] |= 0x0004
230 # set bits 2 and 3 if everything is allowed
231 elif lst[i] == 0:
232 lst[i] = 0x000C
233
234 fsType = mergeBits(os2FsTypeMergeMap, lst)
235 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
236 if fsType & 0x0002:
237 fsType &= ~0x000C
238 return fsType
239
240
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500241ttLib.getTableClass('OS/2').mergeMap = {
242 '*': first,
243 'tableTag': equal,
244 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500245 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800246 'fsType': mergeOs2FsType, # Will be overwritten
247 'panose': first, # FIXME: should really be the first Latin font
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500248 'ulUnicodeRange1': bitwise_or,
249 'ulUnicodeRange2': bitwise_or,
250 'ulUnicodeRange3': bitwise_or,
251 'ulUnicodeRange4': bitwise_or,
252 'fsFirstCharIndex': min,
253 'fsLastCharIndex': max,
254 'sTypoAscender': max,
255 'sTypoDescender': min,
256 'sTypoLineGap': max,
257 'usWinAscent': max,
258 'usWinDescent': max,
259 'ulCodePageRange1': bitwise_or,
260 'ulCodePageRange2': bitwise_or,
261 'usMaxContex': max,
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500262 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500263}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400264
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800265@_add_method(ttLib.getTableClass('OS/2'))
266def merge(self, m, tables):
267 DefaultTable.merge(self, m, tables)
268 if self.version < 2:
269 # bits 8 and 9 are reserved and should be set to zero
270 self.fsType &= ~0x0300
271 if self.version >= 3:
272 # Only one of bits 1, 2, and 3 may be set. We already take
273 # care of bit 1 implications in mergeOs2FsType. So unset
274 # bit 2 if bit 3 is already set.
275 if self.fsType & 0x0008:
276 self.fsType &= ~0x0004
277 return self
278
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500279ttLib.getTableClass('post').mergeMap = {
280 '*': first,
281 'tableTag': equal,
282 'formatType': max,
283 'isFixedPitch': min,
284 'minMemType42': max,
285 'maxMemType42': lambda lst: 0,
286 'minMemType1': max,
287 'maxMemType1': lambda lst: 0,
Behdad Esfahbod23366322013-12-31 18:12:28 +0800288 'mapping': implementedFilter(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500289 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500290}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400291
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500292ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
293 'tableTag': equal,
294 'metrics': sumDicts,
295}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400296
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800297ttLib.getTableClass('gasp').mergeMap = {
298 'tableTag': equal,
299 'version': max,
300 'gaspRange': first, # FIXME? Appears irreconcilable
301}
302
303ttLib.getTableClass('name').mergeMap = {
304 'tableTag': equal,
305 'names': first, # FIXME? Does mixing name records make sense?
306}
307
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500308ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500309 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500310 'tableTag': equal,
311}
312
313ttLib.getTableClass('glyf').mergeMap = {
314 'tableTag': equal,
315 'glyphs': sumDicts,
316 'glyphOrder': sumLists,
317}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400318
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400319@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500320def merge(self, m, tables):
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500321 for i,table in enumerate(tables):
Behdad Esfahbod43650332013-09-20 16:33:33 -0400322 for g in table.glyphs.values():
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500323 if i:
324 # Drop hints for all but first font, since
325 # we don't map functions / CVT values.
326 g.removeHinting()
Behdad Esfahbod43650332013-09-20 16:33:33 -0400327 # Expand composite glyphs to load their
328 # composite glyph names.
329 if g.isComposite():
330 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500331 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400332
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500333ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
334ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
335ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400336
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400337@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500338def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400339 # TODO Handle format=14.
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500340 cmapTables = [t for table in tables for t in table.tables
Behdad Esfahbodf480c7c2014-03-12 12:18:47 -0700341 if t.isUnicode()]
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400342 # TODO Better handle format-4 and format-12 coexisting in same font.
343 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400344 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400345 assert all(t.format in [4, 12] for t in cmapTables)
346 format = max(t.format for t in cmapTables)
347 cmapTable = module.cmap_classes[format](format)
348 cmapTable.cmap = {}
349 cmapTable.platformID = 3
350 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
351 cmapTable.language = 0
352 for table in cmapTables:
353 # TODO handle duplicates.
354 cmapTable.cmap.update(table.cmap)
355 self.tableVersion = 0
356 self.tables = [cmapTable]
357 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500358 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400359
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400360
Behdad Esfahbod26429342013-12-19 11:53:47 -0500361otTables.ScriptList.mergeMap = {
362 'ScriptCount': sum,
Behdad Esfahbod972af5a2013-12-31 18:16:36 +0800363 'ScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.ScriptTag),
Behdad Esfahbod26429342013-12-19 11:53:47 -0500364}
365
366otTables.FeatureList.mergeMap = {
367 'FeatureCount': sum,
368 'FeatureRecord': sumLists,
369}
370
371otTables.LookupList.mergeMap = {
372 'LookupCount': sum,
373 'Lookup': sumLists,
374}
375
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500376otTables.Coverage.mergeMap = {
377 'glyphs': sumLists,
378}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400379
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500380otTables.ClassDef.mergeMap = {
381 'classDefs': sumDicts,
382}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400383
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500384otTables.LigCaretList.mergeMap = {
385 'Coverage': mergeObjects,
386 'LigGlyphCount': sum,
387 'LigGlyph': sumLists,
388}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400389
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500390otTables.AttachList.mergeMap = {
391 'Coverage': mergeObjects,
392 'GlyphCount': sum,
393 'AttachPoint': sumLists,
394}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400395
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500396# XXX Renumber MarkFilterSets of lookups
397otTables.MarkGlyphSetsDef.mergeMap = {
398 'MarkSetTableFormat': equal,
399 'MarkSetCount': sum,
400 'Coverage': sumLists,
401}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400402
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500403otTables.GDEF.mergeMap = {
404 '*': mergeObjects,
405 'Version': max,
406}
407
Behdad Esfahbod26429342013-12-19 11:53:47 -0500408otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
409 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500410 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500411}
412
413ttLib.getTableClass('GDEF').mergeMap = \
414ttLib.getTableClass('GSUB').mergeMap = \
415ttLib.getTableClass('GPOS').mergeMap = \
416ttLib.getTableClass('BASE').mergeMap = \
417ttLib.getTableClass('JSTF').mergeMap = \
418ttLib.getTableClass('MATH').mergeMap = \
419{
420 'tableTag': equal,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500421 'table': mergeObjects,
422}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400423
Behdad Esfahbod26429342013-12-19 11:53:47 -0500424
Behdad Esfahbod50803312014-02-10 18:14:37 -0500425@_add_method(otTables.SingleSubst,
426 otTables.MultipleSubst,
427 otTables.AlternateSubst,
428 otTables.LigatureSubst,
429 otTables.ReverseChainSingleSubst,
430 otTables.SinglePos,
431 otTables.PairPos,
432 otTables.CursivePos,
433 otTables.MarkBasePos,
434 otTables.MarkLigPos,
435 otTables.MarkMarkPos)
436def mapLookups(self, lookupMap):
437 pass
438
439# Copied and trimmed down from subset.py
440@_add_method(otTables.ContextSubst,
441 otTables.ChainContextSubst,
442 otTables.ContextPos,
443 otTables.ChainContextPos)
444def __classify_context(self):
445
446 class ContextHelper(object):
447 def __init__(self, klass, Format):
448 if klass.__name__.endswith('Subst'):
449 Typ = 'Sub'
450 Type = 'Subst'
451 else:
452 Typ = 'Pos'
453 Type = 'Pos'
454 if klass.__name__.startswith('Chain'):
455 Chain = 'Chain'
456 else:
457 Chain = ''
458 ChainTyp = Chain+Typ
459
460 self.Typ = Typ
461 self.Type = Type
462 self.Chain = Chain
463 self.ChainTyp = ChainTyp
464
465 self.LookupRecord = Type+'LookupRecord'
466
467 if Format == 1:
468 self.Rule = ChainTyp+'Rule'
469 self.RuleSet = ChainTyp+'RuleSet'
470 elif Format == 2:
471 self.Rule = ChainTyp+'ClassRule'
472 self.RuleSet = ChainTyp+'ClassSet'
473
474 if self.Format not in [1, 2, 3]:
475 return None # Don't shoot the messenger; let it go
476 if not hasattr(self.__class__, "__ContextHelpers"):
477 self.__class__.__ContextHelpers = {}
478 if self.Format not in self.__class__.__ContextHelpers:
479 helper = ContextHelper(self.__class__, self.Format)
480 self.__class__.__ContextHelpers[self.Format] = helper
481 return self.__class__.__ContextHelpers[self.Format]
482
483
484@_add_method(otTables.ContextSubst,
485 otTables.ChainContextSubst,
486 otTables.ContextPos,
487 otTables.ChainContextPos)
488def mapLookups(self, lookupMap):
489 c = self.__classify_context()
490
491 if self.Format in [1, 2]:
492 for rs in getattr(self, c.RuleSet):
493 if not rs: continue
494 for r in getattr(rs, c.Rule):
495 if not r: continue
496 for ll in getattr(r, c.LookupRecord):
497 if not ll: continue
498 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
499 elif self.Format == 3:
500 for ll in getattr(self, c.LookupRecord):
501 if not ll: continue
502 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
503 else:
504 assert 0, "unknown format: %s" % self.Format
505
506@_add_method(otTables.Lookup)
507def mapLookups(self, lookupMap):
508 for st in self.SubTable:
509 if not st: continue
510 st.mapLookups(lookupMap)
511
512@_add_method(otTables.LookupList)
513def mapLookups(self, lookupMap):
514 for l in self.Lookup:
515 if not l: continue
516 l.mapLookups(lookupMap)
517
Behdad Esfahbod26429342013-12-19 11:53:47 -0500518@_add_method(otTables.Feature)
519def mapLookups(self, lookupMap):
520 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
521
522@_add_method(otTables.FeatureList)
523def mapLookups(self, lookupMap):
524 for f in self.FeatureRecord:
525 if not f or not f.Feature: continue
526 f.Feature.mapLookups(lookupMap)
527
528@_add_method(otTables.DefaultLangSys,
529 otTables.LangSys)
530def mapFeatures(self, featureMap):
531 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
532 if self.ReqFeatureIndex != 65535:
533 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
534
535@_add_method(otTables.Script)
536def mapFeatures(self, featureMap):
537 if self.DefaultLangSys:
538 self.DefaultLangSys.mapFeatures(featureMap)
539 for l in self.LangSysRecord:
540 if not l or not l.LangSys: continue
541 l.LangSys.mapFeatures(featureMap)
542
543@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500544def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500545 for s in self.ScriptRecord:
546 if not s or not s.Script: continue
547 s.Script.mapFeatures(featureMap)
548
549
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400550class Options(object):
551
552 class UnknownOptionError(Exception):
553 pass
554
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400555 def __init__(self, **kwargs):
556
557 self.set(**kwargs)
558
559 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500560 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400561 if not hasattr(self, k):
562 raise self.UnknownOptionError("Unknown option '%s'" % k)
563 setattr(self, k, v)
564
565 def parse_opts(self, argv, ignore_unknown=False):
566 ret = []
567 opts = {}
568 for a in argv:
569 orig_a = a
570 if not a.startswith('--'):
571 ret.append(a)
572 continue
573 a = a[2:]
574 i = a.find('=')
575 op = '='
576 if i == -1:
577 if a.startswith("no-"):
578 k = a[3:]
579 v = False
580 else:
581 k = a
582 v = True
583 else:
584 k = a[:i]
585 if k[-1] in "-+":
586 op = k[-1]+'=' # Ops is '-=' or '+=' now.
587 k = k[:-1]
588 v = a[i+1:]
589 k = k.replace('-', '_')
590 if not hasattr(self, k):
591 if ignore_unknown == True or k in ignore_unknown:
592 ret.append(orig_a)
593 continue
594 else:
595 raise self.UnknownOptionError("Unknown option '%s'" % a)
596
597 ov = getattr(self, k)
598 if isinstance(ov, bool):
599 v = bool(v)
600 elif isinstance(ov, int):
601 v = int(v)
602 elif isinstance(ov, list):
603 vv = v.split(',')
604 if vv == ['']:
605 vv = []
606 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
607 if op == '=':
608 v = vv
609 elif op == '+=':
610 v = ov
611 v.extend(vv)
612 elif op == '-=':
613 v = ov
614 for x in vv:
615 if x in v:
616 v.remove(x)
617 else:
618 assert 0
619
620 opts[k] = v
621 self.set(**opts)
622
623 return ret
624
625
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500626class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400627
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400628 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400629
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400630 if not log:
631 log = Logger()
632 if not options:
633 options = Options()
634
635 self.options = options
636 self.log = log
637
638 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400639
640 mega = ttLib.TTFont()
641
642 #
643 # Settle on a mega glyph order.
644 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400645 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400646 glyphOrders = [font.getGlyphOrder() for font in fonts]
647 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
648 # Reload fonts and set new glyph names on them.
649 # TODO Is it necessary to reload font? I think it is. At least
650 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400651 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400652 for font,glyphOrder in zip(fonts, glyphOrders):
653 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400654 mega.setGlyphOrder(megaGlyphOrder)
655
Behdad Esfahbod26429342013-12-19 11:53:47 -0500656 for font in fonts:
657 self._preMerge(font)
658
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500659 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400660 allTags.remove('GlyphOrder')
661 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400662
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400663 clazz = ttLib.getTableClass(tag)
664
Behdad Esfahbod26429342013-12-19 11:53:47 -0500665 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500666 table = clazz(tag).merge(self, tables)
667 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400668 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400669 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400670 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500671 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400672 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400673
Behdad Esfahbod26429342013-12-19 11:53:47 -0500674 self._postMerge(mega)
675
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400676 return mega
677
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400678 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400679 """Modifies passed-in glyphOrders to reflect new glyph names.
680 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400681 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400682 # TODO Even this simplistic numbering can result in conflicts.
683 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400684 mega = []
685 for n,glyphOrder in enumerate(glyphOrders):
686 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500687 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400688 glyphOrder[i] = glyphName
689 mega.append(glyphName)
690 return mega
691
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500692 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500693 # Right now we don't use self at all. Will use in the future
694 # for options and logging.
695
696 if logic is NotImplemented:
697 return NotImplemented
698
699 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800700 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800701 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500702 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800703 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500704 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500705 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500706 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500707 raise Exception("Don't know how to merge key %s of class %s" %
708 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500709 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800710 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500711 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
712 if value is not NotImplemented:
713 setattr(returnTable, key, value)
714
715 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800716
Behdad Esfahbod26429342013-12-19 11:53:47 -0500717 def _preMerge(self, font):
718
Behdad Esfahbod26429342013-12-19 11:53:47 -0500719 GDEF = font.get('GDEF')
720 GSUB = font.get('GSUB')
721 GPOS = font.get('GPOS')
722
723 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500724 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500725
Behdad Esfahbod50803312014-02-10 18:14:37 -0500726 if t.table.LookupList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500727 lookupMap = {i:id(v) for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod50803312014-02-10 18:14:37 -0500728 t.table.LookupList.mapLookups(lookupMap)
729 if t.table.FeatureList:
730 # XXX Handle present FeatureList but absent LookupList
731 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500732
733 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500734 featureMap = {i:id(v) for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500735 t.table.ScriptList.mapFeatures(featureMap)
736
737 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500738 # TODO FeatureParams nameIDs
739
740 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500741
742 GDEF = font.get('GDEF')
743 GSUB = font.get('GSUB')
744 GPOS = font.get('GPOS')
745
746 for t in [GSUB, GPOS]:
747 if not t: continue
748
Behdad Esfahbod50803312014-02-10 18:14:37 -0500749 if t.table.LookupList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500750 lookupMap = {id(v):i for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod50803312014-02-10 18:14:37 -0500751 t.table.LookupList.mapLookups(lookupMap)
752 if t.table.FeatureList:
753 # XXX Handle present FeatureList but absent LookupList
754 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500755
756 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbod50803312014-02-10 18:14:37 -0500757 # XXX Handle present ScriptList but absent FeatureList
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500758 featureMap = {id(v):i for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500759 t.table.ScriptList.mapFeatures(featureMap)
760
761 # TODO GDEF/Lookup MarkFilteringSets
762 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500763
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400764
765class Logger(object):
766
767 def __init__(self, verbose=False, xml=False, timing=False):
768 self.verbose = verbose
769 self.xml = xml
770 self.timing = timing
771 self.last_time = self.start_time = time.time()
772
773 def parse_opts(self, argv):
774 argv = argv[:]
775 for v in ['verbose', 'xml', 'timing']:
776 if "--"+v in argv:
777 setattr(self, v, True)
778 argv.remove("--"+v)
779 return argv
780
781 def __call__(self, *things):
782 if not self.verbose:
783 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500784 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400785
786 def lapse(self, *things):
787 if not self.timing:
788 return
789 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500790 print("Took %0.3fs to %s" %(new_time - self.last_time,
791 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400792 self.last_time = new_time
793
794 def font(self, font, file=sys.stdout):
795 if not self.xml:
796 return
797 from fontTools.misc import xmlWriter
798 writer = xmlWriter.XMLWriter(file)
799 font.disassembleInstructions = False # Work around ttLib bug
800 for tag in font.keys():
801 writer.begintag(tag)
802 writer.newline()
803 font[tag].toXML(writer, font)
804 writer.endtag(tag)
805 writer.newline()
806
807
808__all__ = [
809 'Options',
810 'Merger',
811 'Logger',
812 'main'
813]
814
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400815def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400816
817 log = Logger()
818 args = log.parse_opts(args)
819
820 options = Options()
821 args = options.parse_opts(args)
822
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400823 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500824 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400825 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400826
827 merger = Merger(options=options, log=log)
828 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400829 outfile = 'merged.ttf'
830 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400831 log.lapse("compile and save font")
832
833 log.last_time = log.start_time
834 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400835
836if __name__ == "__main__":
837 main(sys.argv[1:])