blob: 48aee3c9500737b98927053dc994e90808425f94 [file] [log] [blame]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -04001# Copyright 2013 Google, Inc. All Rights Reserved.
2#
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -08003# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
Behdad Esfahbod45d2f382013-09-18 20:47:53 -04004
5"""Font merger.
6"""
7
Behdad Esfahbod1ae29592014-01-14 15:07:50 +08008from __future__ import print_function, division, absolute_import
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -05009from fontTools.misc.py23 import *
10from fontTools import ttLib, cffLib
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080011from fontTools.ttLib.tables import otTables, _h_e_a_d
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050012from fontTools.ttLib.tables.DefaultTable import DefaultTable
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -050013from functools import reduce
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040014import sys
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040015import time
Behdad Esfahbod49028b32013-12-18 17:34:17 -050016import operator
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040017
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040018
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050019def _add_method(*clazzes, **kwargs):
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040020 """Returns a decorator function that adds a new method to one or
21 more classes."""
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -050022 allowDefault = kwargs.get('allowDefaultTable', False)
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040023 def wrapper(method):
24 for clazz in clazzes:
Behdad Esfahbod35e3c722013-12-20 21:34:09 -050025 assert allowDefault or clazz != DefaultTable, 'Oops, table class not found.'
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050026 assert method.__name__ not in clazz.__dict__, \
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040027 "Oops, class '%s' has method '%s'." % (clazz.__name__,
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -050028 method.__name__)
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050029 setattr(clazz, method.__name__, method)
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040030 return None
31 return wrapper
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040032
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080033# General utility functions for merging values from different fonts
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050034
Behdad Esfahbod49028b32013-12-18 17:34:17 -050035def equal(lst):
Behdad Esfahbod477dad12014-03-28 13:52:48 -070036 lst = list(lst)
Behdad Esfahbod49028b32013-12-18 17:34:17 -050037 t = iter(lst)
38 first = next(t)
Behdad Esfahbod477dad12014-03-28 13:52:48 -070039 assert all(item == first for item in t), "Expected all items to be equal: %s" % lst
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080040 return first
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080041
42def first(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050043 return next(iter(lst))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -080044
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080045def recalculate(lst):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050046 return NotImplemented
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080047
48def current_time(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050049 return int(time.time() - _h_e_a_d.mac_epoch_diff)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080050
Roozbeh Pournader642eaf12013-12-21 01:04:18 -080051def bitwise_and(lst):
52 return reduce(operator.and_, lst)
53
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080054def bitwise_or(lst):
Behdad Esfahbod49028b32013-12-18 17:34:17 -050055 return reduce(operator.or_, lst)
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -080056
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050057def avg_int(lst):
58 lst = list(lst)
59 return sum(lst) // len(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040060
Behdad Esfahbod23366322013-12-31 18:12:28 +080061def implementedFilter(func):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -050062 """Returns a filter func that when called with a list,
63 only calls func on the non-NotImplemented items of the list,
64 and only so if there's at least one item remaining.
65 Otherwise returns NotImplemented."""
66
67 def wrapper(lst):
68 items = [item for item in lst if item is not NotImplemented]
69 return func(items) if items else NotImplemented
70
71 return wrapper
72
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -050073def sumLists(lst):
74 l = []
75 for item in lst:
76 l.extend(item)
77 return l
78
79def sumDicts(lst):
80 d = {}
81 for item in lst:
82 d.update(item)
83 return d
84
Behdad Esfahbod12dd5472013-12-19 05:58:06 -050085def mergeObjects(lst):
86 lst = [item for item in lst if item is not None and item is not NotImplemented]
87 if not lst:
88 return None # Not all can be NotImplemented
89
90 clazz = lst[0].__class__
91 assert all(type(item) == clazz for item in lst), lst
92 logic = clazz.mergeMap
93 returnTable = clazz()
94
95 allKeys = set.union(set(), *(vars(table).keys() for table in lst))
96 for key in allKeys:
97 try:
98 mergeLogic = logic[key]
99 except KeyError:
100 try:
101 mergeLogic = logic['*']
102 except KeyError:
103 raise Exception("Don't know how to merge key %s of class %s" %
104 (key, clazz.__name__))
105 if mergeLogic is NotImplemented:
106 continue
107 value = mergeLogic(getattr(table, key, NotImplemented) for table in lst)
108 if value is not NotImplemented:
109 setattr(returnTable, key, value)
110
111 return returnTable
112
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800113def mergeBits(logic, lst):
114 lst = list(lst)
115 returnValue = 0
116 for bitNumber in range(logic['size']):
117 try:
118 mergeLogic = logic[bitNumber]
119 except KeyError:
120 try:
121 mergeLogic = logic['*']
122 except KeyError:
123 raise Exception("Don't know how to merge bit %s" % bitNumber)
124 shiftedBit = 1 << bitNumber
125 mergedValue = mergeLogic(bool(item & shiftedBit) for item in lst)
126 returnValue |= mergedValue << bitNumber
127 return returnValue
128
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500129
130@_add_method(DefaultTable, allowDefaultTable=True)
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500131def merge(self, m, tables):
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500132 if not hasattr(self, 'mergeMap'):
133 m.log("Don't know how to merge '%s'." % self.tableTag)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500134 return NotImplemented
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500135
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500136 logic = self.mergeMap
137
138 if isinstance(logic, dict):
139 return m.mergeObjects(self, self.mergeMap, tables)
140 else:
141 return logic(tables)
142
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500143
144ttLib.getTableClass('maxp').mergeMap = {
145 '*': max,
146 'tableTag': equal,
147 'tableVersion': equal,
148 'numGlyphs': sum,
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500149 'maxStorage': first,
150 'maxFunctionDefs': first,
151 'maxInstructionDefs': first,
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400152 # TODO When we correctly merge hinting data, update these values:
153 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500154}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400155
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800156headFlagsMergeMap = {
157 'size': 16,
158 '*': bitwise_or,
159 1: bitwise_and, # Baseline at y = 0
160 2: bitwise_and, # lsb at x = 0
161 3: bitwise_and, # Force ppem to integer values. FIXME?
162 5: bitwise_and, # Font is vertical
163 6: lambda bit: 0, # Always set to zero
164 11: bitwise_and, # Font data is 'lossless'
165 13: bitwise_and, # Optimized for ClearType
166 14: bitwise_and, # Last resort font. FIXME? equal or first may be better
167 15: lambda bit: 0, # Always set to zero
168}
169
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500170ttLib.getTableClass('head').mergeMap = {
171 'tableTag': equal,
172 'tableVersion': max,
173 'fontRevision': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500174 'checkSumAdjustment': lambda lst: 0, # We need *something* here
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500175 'magicNumber': equal,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800176 'flags': lambda lst: mergeBits(headFlagsMergeMap, lst),
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500177 'unitsPerEm': equal,
178 'created': current_time,
179 'modified': current_time,
180 'xMin': min,
181 'yMin': min,
182 'xMax': max,
183 'yMax': max,
184 'macStyle': first,
185 'lowestRecPPEM': max,
186 'fontDirectionHint': lambda lst: 2,
187 'indexToLocFormat': recalculate,
188 'glyphDataFormat': equal,
189}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400190
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500191ttLib.getTableClass('hhea').mergeMap = {
192 '*': equal,
193 'tableTag': equal,
194 'tableVersion': max,
195 'ascent': max,
196 'descent': min,
197 'lineGap': max,
198 'advanceWidthMax': max,
199 'minLeftSideBearing': min,
200 'minRightSideBearing': min,
201 'xMaxExtent': max,
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800202 'caretSlopeRise': first,
203 'caretSlopeRun': first,
204 'caretOffset': first,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500205 'numberOfHMetrics': recalculate,
206}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400207
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800208os2FsTypeMergeMap = {
209 'size': 16,
210 '*': lambda bit: 0,
211 1: bitwise_or, # no embedding permitted
212 2: bitwise_and, # allow previewing and printing documents
213 3: bitwise_and, # allow editing documents
214 8: bitwise_or, # no subsetting permitted
215 9: bitwise_or, # no embedding of outlines permitted
216}
217
218def mergeOs2FsType(lst):
219 lst = list(lst)
220 if all(item == 0 for item in lst):
221 return 0
222
223 # Compute least restrictive logic for each fsType value
224 for i in range(len(lst)):
225 # unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
226 if lst[i] & 0x000C:
227 lst[i] &= ~0x0002
228 # set bit 2 (allow previewing) if bit 3 is set (allow editing)
229 elif lst[i] & 0x0008:
230 lst[i] |= 0x0004
231 # set bits 2 and 3 if everything is allowed
232 elif lst[i] == 0:
233 lst[i] = 0x000C
234
235 fsType = mergeBits(os2FsTypeMergeMap, lst)
236 # unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
237 if fsType & 0x0002:
238 fsType &= ~0x000C
239 return fsType
240
241
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500242ttLib.getTableClass('OS/2').mergeMap = {
243 '*': first,
244 'tableTag': equal,
245 'version': max,
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500246 'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800247 'fsType': mergeOs2FsType, # Will be overwritten
248 'panose': first, # FIXME: should really be the first Latin font
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500249 'ulUnicodeRange1': bitwise_or,
250 'ulUnicodeRange2': bitwise_or,
251 'ulUnicodeRange3': bitwise_or,
252 'ulUnicodeRange4': bitwise_or,
253 'fsFirstCharIndex': min,
254 'fsLastCharIndex': max,
255 'sTypoAscender': max,
256 'sTypoDescender': min,
257 'sTypoLineGap': max,
258 'usWinAscent': max,
259 'usWinDescent': max,
260 'ulCodePageRange1': bitwise_or,
261 'ulCodePageRange2': bitwise_or,
262 'usMaxContex': max,
Behdad Esfahboddb2410a2013-12-19 03:30:29 -0500263 # TODO version 5
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500264}
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400265
Roozbeh Pournader642eaf12013-12-21 01:04:18 -0800266@_add_method(ttLib.getTableClass('OS/2'))
267def merge(self, m, tables):
268 DefaultTable.merge(self, m, tables)
269 if self.version < 2:
270 # bits 8 and 9 are reserved and should be set to zero
271 self.fsType &= ~0x0300
272 if self.version >= 3:
273 # Only one of bits 1, 2, and 3 may be set. We already take
274 # care of bit 1 implications in mergeOs2FsType. So unset
275 # bit 2 if bit 3 is already set.
276 if self.fsType & 0x0008:
277 self.fsType &= ~0x0004
278 return self
279
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500280ttLib.getTableClass('post').mergeMap = {
281 '*': first,
282 'tableTag': equal,
283 'formatType': max,
284 'isFixedPitch': min,
285 'minMemType42': max,
286 'maxMemType42': lambda lst: 0,
287 'minMemType1': max,
288 'maxMemType1': lambda lst: 0,
Behdad Esfahbod23366322013-12-31 18:12:28 +0800289 'mapping': implementedFilter(sumDicts),
Behdad Esfahbodc68c0ff2013-12-19 14:19:23 -0500290 'extraNames': lambda lst: [],
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500291}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400292
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500293ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
294 'tableTag': equal,
295 'metrics': sumDicts,
296}
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400297
Roozbeh Pournader7a272142013-12-19 15:46:05 -0800298ttLib.getTableClass('gasp').mergeMap = {
299 'tableTag': equal,
300 'version': max,
301 'gaspRange': first, # FIXME? Appears irreconcilable
302}
303
304ttLib.getTableClass('name').mergeMap = {
305 'tableTag': equal,
306 'names': first, # FIXME? Does mixing name records make sense?
307}
308
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500309ttLib.getTableClass('loca').mergeMap = {
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500310 '*': recalculate,
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500311 'tableTag': equal,
312}
313
314ttLib.getTableClass('glyf').mergeMap = {
315 'tableTag': equal,
316 'glyphs': sumDicts,
317 'glyphOrder': sumLists,
318}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400319
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400320@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500321def merge(self, m, tables):
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500322 for i,table in enumerate(tables):
Behdad Esfahbod43650332013-09-20 16:33:33 -0400323 for g in table.glyphs.values():
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500324 if i:
325 # Drop hints for all but first font, since
326 # we don't map functions / CVT values.
327 g.removeHinting()
Behdad Esfahbod43650332013-09-20 16:33:33 -0400328 # Expand composite glyphs to load their
329 # composite glyph names.
330 if g.isComposite():
331 g.expand(table)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500332 return DefaultTable.merge(self, m, tables)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400333
Behdad Esfahbod27c71f92014-01-27 21:01:45 -0500334ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
335ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
336ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400337
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400338@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500339def merge(self, m, tables):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400340 # TODO Handle format=14.
Behdad Esfahbod3b36f552013-12-19 04:45:17 -0500341 cmapTables = [t for table in tables for t in table.tables
Behdad Esfahbodf480c7c2014-03-12 12:18:47 -0700342 if t.isUnicode()]
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400343 # TODO Better handle format-4 and format-12 coexisting in same font.
344 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400345 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400346 assert all(t.format in [4, 12] for t in cmapTables)
347 format = max(t.format for t in cmapTables)
348 cmapTable = module.cmap_classes[format](format)
349 cmapTable.cmap = {}
350 cmapTable.platformID = 3
351 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
352 cmapTable.language = 0
353 for table in cmapTables:
354 # TODO handle duplicates.
355 cmapTable.cmap.update(table.cmap)
356 self.tableVersion = 0
357 self.tables = [cmapTable]
358 self.numSubTables = len(self.tables)
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500359 return self
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400360
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400361
Behdad Esfahbod26429342013-12-19 11:53:47 -0500362otTables.ScriptList.mergeMap = {
363 'ScriptCount': sum,
Behdad Esfahbod972af5a2013-12-31 18:16:36 +0800364 'ScriptRecord': lambda lst: sorted(sumLists(lst), key=lambda s: s.ScriptTag),
Behdad Esfahbod26429342013-12-19 11:53:47 -0500365}
366
367otTables.FeatureList.mergeMap = {
368 'FeatureCount': sum,
369 'FeatureRecord': sumLists,
370}
371
372otTables.LookupList.mergeMap = {
373 'LookupCount': sum,
374 'Lookup': sumLists,
375}
376
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500377otTables.Coverage.mergeMap = {
378 'glyphs': sumLists,
379}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400380
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500381otTables.ClassDef.mergeMap = {
382 'classDefs': sumDicts,
383}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400384
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500385otTables.LigCaretList.mergeMap = {
386 'Coverage': mergeObjects,
387 'LigGlyphCount': sum,
388 'LigGlyph': sumLists,
389}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400390
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500391otTables.AttachList.mergeMap = {
392 'Coverage': mergeObjects,
393 'GlyphCount': sum,
394 'AttachPoint': sumLists,
395}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400396
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500397# XXX Renumber MarkFilterSets of lookups
398otTables.MarkGlyphSetsDef.mergeMap = {
399 'MarkSetTableFormat': equal,
400 'MarkSetCount': sum,
401 'Coverage': sumLists,
402}
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400403
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500404otTables.GDEF.mergeMap = {
405 '*': mergeObjects,
406 'Version': max,
407}
408
Behdad Esfahbod26429342013-12-19 11:53:47 -0500409otTables.GSUB.mergeMap = otTables.GPOS.mergeMap = {
410 '*': mergeObjects,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500411 'Version': max,
Behdad Esfahbod26429342013-12-19 11:53:47 -0500412}
413
414ttLib.getTableClass('GDEF').mergeMap = \
415ttLib.getTableClass('GSUB').mergeMap = \
416ttLib.getTableClass('GPOS').mergeMap = \
417ttLib.getTableClass('BASE').mergeMap = \
418ttLib.getTableClass('JSTF').mergeMap = \
419ttLib.getTableClass('MATH').mergeMap = \
420{
421 'tableTag': equal,
Behdad Esfahbod12dd5472013-12-19 05:58:06 -0500422 'table': mergeObjects,
423}
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400424
Behdad Esfahbod26429342013-12-19 11:53:47 -0500425
Behdad Esfahbod50803312014-02-10 18:14:37 -0500426@_add_method(otTables.SingleSubst,
427 otTables.MultipleSubst,
428 otTables.AlternateSubst,
429 otTables.LigatureSubst,
430 otTables.ReverseChainSingleSubst,
431 otTables.SinglePos,
432 otTables.PairPos,
433 otTables.CursivePos,
434 otTables.MarkBasePos,
435 otTables.MarkLigPos,
436 otTables.MarkMarkPos)
437def mapLookups(self, lookupMap):
438 pass
439
440# Copied and trimmed down from subset.py
441@_add_method(otTables.ContextSubst,
442 otTables.ChainContextSubst,
443 otTables.ContextPos,
444 otTables.ChainContextPos)
445def __classify_context(self):
446
447 class ContextHelper(object):
448 def __init__(self, klass, Format):
449 if klass.__name__.endswith('Subst'):
450 Typ = 'Sub'
451 Type = 'Subst'
452 else:
453 Typ = 'Pos'
454 Type = 'Pos'
455 if klass.__name__.startswith('Chain'):
456 Chain = 'Chain'
457 else:
458 Chain = ''
459 ChainTyp = Chain+Typ
460
461 self.Typ = Typ
462 self.Type = Type
463 self.Chain = Chain
464 self.ChainTyp = ChainTyp
465
466 self.LookupRecord = Type+'LookupRecord'
467
468 if Format == 1:
469 self.Rule = ChainTyp+'Rule'
470 self.RuleSet = ChainTyp+'RuleSet'
471 elif Format == 2:
472 self.Rule = ChainTyp+'ClassRule'
473 self.RuleSet = ChainTyp+'ClassSet'
474
475 if self.Format not in [1, 2, 3]:
476 return None # Don't shoot the messenger; let it go
477 if not hasattr(self.__class__, "__ContextHelpers"):
478 self.__class__.__ContextHelpers = {}
479 if self.Format not in self.__class__.__ContextHelpers:
480 helper = ContextHelper(self.__class__, self.Format)
481 self.__class__.__ContextHelpers[self.Format] = helper
482 return self.__class__.__ContextHelpers[self.Format]
483
484
485@_add_method(otTables.ContextSubst,
486 otTables.ChainContextSubst,
487 otTables.ContextPos,
488 otTables.ChainContextPos)
489def mapLookups(self, lookupMap):
490 c = self.__classify_context()
491
492 if self.Format in [1, 2]:
493 for rs in getattr(self, c.RuleSet):
494 if not rs: continue
495 for r in getattr(rs, c.Rule):
496 if not r: continue
497 for ll in getattr(r, c.LookupRecord):
498 if not ll: continue
499 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
500 elif self.Format == 3:
501 for ll in getattr(self, c.LookupRecord):
502 if not ll: continue
503 ll.LookupListIndex = lookupMap[ll.LookupListIndex]
504 else:
505 assert 0, "unknown format: %s" % self.Format
506
507@_add_method(otTables.Lookup)
508def mapLookups(self, lookupMap):
509 for st in self.SubTable:
510 if not st: continue
511 st.mapLookups(lookupMap)
512
513@_add_method(otTables.LookupList)
514def mapLookups(self, lookupMap):
515 for l in self.Lookup:
516 if not l: continue
517 l.mapLookups(lookupMap)
518
Behdad Esfahbod26429342013-12-19 11:53:47 -0500519@_add_method(otTables.Feature)
520def mapLookups(self, lookupMap):
521 self.LookupListIndex = [lookupMap[i] for i in self.LookupListIndex]
522
523@_add_method(otTables.FeatureList)
524def mapLookups(self, lookupMap):
525 for f in self.FeatureRecord:
526 if not f or not f.Feature: continue
527 f.Feature.mapLookups(lookupMap)
528
529@_add_method(otTables.DefaultLangSys,
530 otTables.LangSys)
531def mapFeatures(self, featureMap):
532 self.FeatureIndex = [featureMap[i] for i in self.FeatureIndex]
533 if self.ReqFeatureIndex != 65535:
534 self.ReqFeatureIndex = featureMap[self.ReqFeatureIndex]
535
536@_add_method(otTables.Script)
537def mapFeatures(self, featureMap):
538 if self.DefaultLangSys:
539 self.DefaultLangSys.mapFeatures(featureMap)
540 for l in self.LangSysRecord:
541 if not l or not l.LangSys: continue
542 l.LangSys.mapFeatures(featureMap)
543
544@_add_method(otTables.ScriptList)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500545def mapFeatures(self, featureMap):
Behdad Esfahbod26429342013-12-19 11:53:47 -0500546 for s in self.ScriptRecord:
547 if not s or not s.Script: continue
548 s.Script.mapFeatures(featureMap)
549
550
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400551class Options(object):
552
553 class UnknownOptionError(Exception):
554 pass
555
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400556 def __init__(self, **kwargs):
557
558 self.set(**kwargs)
559
560 def set(self, **kwargs):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500561 for k,v in kwargs.items():
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400562 if not hasattr(self, k):
563 raise self.UnknownOptionError("Unknown option '%s'" % k)
564 setattr(self, k, v)
565
566 def parse_opts(self, argv, ignore_unknown=False):
567 ret = []
568 opts = {}
569 for a in argv:
570 orig_a = a
571 if not a.startswith('--'):
572 ret.append(a)
573 continue
574 a = a[2:]
575 i = a.find('=')
576 op = '='
577 if i == -1:
578 if a.startswith("no-"):
579 k = a[3:]
580 v = False
581 else:
582 k = a
583 v = True
584 else:
585 k = a[:i]
586 if k[-1] in "-+":
587 op = k[-1]+'=' # Ops is '-=' or '+=' now.
588 k = k[:-1]
589 v = a[i+1:]
590 k = k.replace('-', '_')
591 if not hasattr(self, k):
592 if ignore_unknown == True or k in ignore_unknown:
593 ret.append(orig_a)
594 continue
595 else:
596 raise self.UnknownOptionError("Unknown option '%s'" % a)
597
598 ov = getattr(self, k)
599 if isinstance(ov, bool):
600 v = bool(v)
601 elif isinstance(ov, int):
602 v = int(v)
603 elif isinstance(ov, list):
604 vv = v.split(',')
605 if vv == ['']:
606 vv = []
607 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
608 if op == '=':
609 v = vv
610 elif op == '+=':
611 v = ov
612 v.extend(vv)
613 elif op == '-=':
614 v = ov
615 for x in vv:
616 if x in v:
617 v.remove(x)
618 else:
619 assert 0
620
621 opts[k] = v
622 self.set(**opts)
623
624 return ret
625
626
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500627class Merger(object):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400628
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400629 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400630
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400631 if not log:
632 log = Logger()
633 if not options:
634 options = Options()
635
636 self.options = options
637 self.log = log
638
639 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400640
641 mega = ttLib.TTFont()
642
643 #
644 # Settle on a mega glyph order.
645 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400646 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400647 glyphOrders = [font.getGlyphOrder() for font in fonts]
648 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
649 # Reload fonts and set new glyph names on them.
650 # TODO Is it necessary to reload font? I think it is. At least
651 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400652 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400653 for font,glyphOrder in zip(fonts, glyphOrders):
654 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400655 mega.setGlyphOrder(megaGlyphOrder)
656
Behdad Esfahbod26429342013-12-19 11:53:47 -0500657 for font in fonts:
658 self._preMerge(font)
659
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500660 allTags = reduce(set.union, (list(font.keys()) for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400661 allTags.remove('GlyphOrder')
662 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400663
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400664 clazz = ttLib.getTableClass(tag)
665
Behdad Esfahbod26429342013-12-19 11:53:47 -0500666 tables = [font.get(tag, NotImplemented) for font in fonts]
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500667 table = clazz(tag).merge(self, tables)
668 if table is not NotImplemented and table is not False:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400669 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400670 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400671 else:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500672 self.log("Dropped '%s'." % tag)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400673 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400674
Behdad Esfahbod26429342013-12-19 11:53:47 -0500675 self._postMerge(mega)
676
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400677 return mega
678
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400679 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400680 """Modifies passed-in glyphOrders to reflect new glyph names.
681 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400682 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400683 # TODO Even this simplistic numbering can result in conflicts.
684 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400685 mega = []
686 for n,glyphOrder in enumerate(glyphOrders):
687 for i,glyphName in enumerate(glyphOrder):
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500688 glyphName += "#" + repr(n)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400689 glyphOrder[i] = glyphName
690 mega.append(glyphName)
691 return mega
692
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500693 def mergeObjects(self, returnTable, logic, tables):
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500694 # Right now we don't use self at all. Will use in the future
695 # for options and logging.
696
697 if logic is NotImplemented:
698 return NotImplemented
699
700 allKeys = set.union(set(), *(vars(table).keys() for table in tables if table is not NotImplemented))
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800701 for key in allKeys:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800702 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500703 mergeLogic = logic[key]
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800704 except KeyError:
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500705 try:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500706 mergeLogic = logic['*']
Behdad Esfahbod9e6adb62013-12-19 04:20:26 -0500707 except KeyError:
Behdad Esfahbod6baf26e2013-12-19 04:47:34 -0500708 raise Exception("Don't know how to merge key %s of class %s" %
709 (key, returnTable.__class__.__name__))
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500710 if mergeLogic is NotImplemented:
Roozbeh Pournadere219c6c2013-12-18 12:15:46 -0800711 continue
Behdad Esfahbod92fd5662013-12-19 04:56:50 -0500712 value = mergeLogic(getattr(table, key, NotImplemented) for table in tables)
713 if value is not NotImplemented:
714 setattr(returnTable, key, value)
715
716 return returnTable
Roozbeh Pournader47bee9c2013-12-18 00:45:12 -0800717
Behdad Esfahbod26429342013-12-19 11:53:47 -0500718 def _preMerge(self, font):
719
Behdad Esfahbod26429342013-12-19 11:53:47 -0500720 GDEF = font.get('GDEF')
721 GSUB = font.get('GSUB')
722 GPOS = font.get('GPOS')
723
724 for t in [GSUB, GPOS]:
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500725 if not t: continue
Behdad Esfahbod26429342013-12-19 11:53:47 -0500726
Behdad Esfahbod50803312014-02-10 18:14:37 -0500727 if t.table.LookupList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500728 lookupMap = {i:id(v) for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod50803312014-02-10 18:14:37 -0500729 t.table.LookupList.mapLookups(lookupMap)
730 if t.table.FeatureList:
731 # XXX Handle present FeatureList but absent LookupList
732 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500733
734 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500735 featureMap = {i:id(v) for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500736 t.table.ScriptList.mapFeatures(featureMap)
737
738 # TODO GDEF/Lookup MarkFilteringSets
Behdad Esfahbod26429342013-12-19 11:53:47 -0500739 # TODO FeatureParams nameIDs
740
741 def _postMerge(self, font):
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500742
743 GDEF = font.get('GDEF')
744 GSUB = font.get('GSUB')
745 GPOS = font.get('GPOS')
746
747 for t in [GSUB, GPOS]:
748 if not t: continue
749
Behdad Esfahbod50803312014-02-10 18:14:37 -0500750 if t.table.LookupList:
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500751 lookupMap = {id(v):i for i,v in enumerate(t.table.LookupList.Lookup)}
Behdad Esfahbod50803312014-02-10 18:14:37 -0500752 t.table.LookupList.mapLookups(lookupMap)
753 if t.table.FeatureList:
754 # XXX Handle present FeatureList but absent LookupList
755 t.table.FeatureList.mapLookups(lookupMap)
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500756
757 if t.table.FeatureList and t.table.ScriptList:
Behdad Esfahbod50803312014-02-10 18:14:37 -0500758 # XXX Handle present ScriptList but absent FeatureList
Behdad Esfahbodb76d6ff2013-12-20 20:24:27 -0500759 featureMap = {id(v):i for i,v in enumerate(t.table.FeatureList.FeatureRecord)}
Behdad Esfahbod398770d2013-12-19 15:30:24 -0500760 t.table.ScriptList.mapFeatures(featureMap)
761
762 # TODO GDEF/Lookup MarkFilteringSets
763 # TODO FeatureParams nameIDs
Behdad Esfahbod26429342013-12-19 11:53:47 -0500764
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400765
766class Logger(object):
767
768 def __init__(self, verbose=False, xml=False, timing=False):
769 self.verbose = verbose
770 self.xml = xml
771 self.timing = timing
772 self.last_time = self.start_time = time.time()
773
774 def parse_opts(self, argv):
775 argv = argv[:]
776 for v in ['verbose', 'xml', 'timing']:
777 if "--"+v in argv:
778 setattr(self, v, True)
779 argv.remove("--"+v)
780 return argv
781
782 def __call__(self, *things):
783 if not self.verbose:
784 return
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500785 print(' '.join(str(x) for x in things))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400786
787 def lapse(self, *things):
788 if not self.timing:
789 return
790 new_time = time.time()
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500791 print("Took %0.3fs to %s" %(new_time - self.last_time,
792 ' '.join(str(x) for x in things)))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400793 self.last_time = new_time
794
795 def font(self, font, file=sys.stdout):
796 if not self.xml:
797 return
798 from fontTools.misc import xmlWriter
799 writer = xmlWriter.XMLWriter(file)
800 font.disassembleInstructions = False # Work around ttLib bug
801 for tag in font.keys():
802 writer.begintag(tag)
803 writer.newline()
804 font[tag].toXML(writer, font)
805 writer.endtag(tag)
806 writer.newline()
807
808
809__all__ = [
810 'Options',
811 'Merger',
812 'Logger',
813 'main'
814]
815
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400816def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400817
818 log = Logger()
819 args = log.parse_opts(args)
820
821 options = Options()
822 args = options.parse_opts(args)
823
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400824 if len(args) < 1:
Behdad Esfahbodf63e80e2013-12-18 17:14:26 -0500825 print("usage: pyftmerge font...", file=sys.stderr)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400826 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400827
828 merger = Merger(options=options, log=log)
829 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400830 outfile = 'merged.ttf'
831 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400832 log.lapse("compile and save font")
833
834 log.last_time = log.start_time
835 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400836
837if __name__ == "__main__":
838 main(sys.argv[1:])