blob: 92c170a39cf3490f7ab97ed4ba67ec1631030ed2 [file] [log] [blame]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -04001# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod
4
5"""Font merger.
6"""
7
8import sys
Behdad Esfahbodf2d59822013-09-19 16:16:39 -04009import time
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040010
11import fontTools
12from fontTools import misc, ttLib, cffLib
Behdad Esfahbodc14ab482013-09-19 21:22:54 -040013from fontTools.ttLib.tables import otTables
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040014
15def _add_method(*clazzes):
Behdad Esfahbodc855f3a2013-09-19 20:09:23 -040016 """Returns a decorator function that adds a new method to one or
17 more classes."""
18 def wrapper(method):
19 for clazz in clazzes:
20 assert clazz.__name__ != 'DefaultTable', 'Oops, table class not found.'
21 assert not hasattr(clazz, method.func_name), \
22 "Oops, class '%s' has method '%s'." % (clazz.__name__,
23 method.func_name)
24 setattr(clazz, method.func_name, method)
25 return None
26 return wrapper
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040027
28
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040029@_add_method(ttLib.getTableClass('maxp'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040030def merge(self, m):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -040031 # TODO When we correctly merge hinting data, update these values:
32 # maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
33 # TODO Assumes that all tables have format 1.0; safe assumption.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040034 for key in set(sum((vars(table).keys() for table in m.tables), [])):
Behdad Esfahbod3235a042013-09-19 20:57:33 -040035 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040036 return True
37
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040038@_add_method(ttLib.getTableClass('head'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040039def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040040 # TODO Check that unitsPerEm are the same.
41 # TODO Use bitwise ops for flags, macStyle, fontDirectionHint
42 minMembers = ['xMin', 'yMin']
43 # Negate some members
44 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040045 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040046 setattr(table, key, -getattr(table, key))
47 # Get max over members
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040048 for key in set(sum((vars(table).keys() for table in m.tables), [])):
49 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040050 # Negate them back
51 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040052 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040053 setattr(table, key, -getattr(table, key))
54 setattr(self, key, -getattr(self, key))
55 return True
56
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040057@_add_method(ttLib.getTableClass('hhea'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040058def merge(self, m):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040059 # TODO Check that ascent, descent, slope, etc are the same.
60 minMembers = ['descent', 'minLeftSideBearing', 'minRightSideBearing']
61 # Negate some members
62 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040063 for table in m.tables:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040064 setattr(table, key, -getattr(table, key))
65 # Get max over members
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040066 for key in set(sum((vars(table).keys() for table in m.tables), [])):
67 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040068 # Negate them back
69 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040070 for table in m.tables:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040071 setattr(table, key, -getattr(table, key))
72 setattr(self, key, -getattr(self, key))
73 return True
74
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040075@_add_method(ttLib.getTableClass('OS/2'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040076def merge(self, m):
Behdad Esfahbod71294de2013-09-19 19:43:17 -040077 # TODO Check that weight/width/subscript/superscript/etc are the same.
78 # TODO Bitwise ops for UnicodeRange/CodePageRange.
79 # TODO Pretty much all fields generated here have bogus values.
80 # Get max over members
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040081 for key in set(sum((vars(table).keys() for table in m.tables), [])):
82 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbod71294de2013-09-19 19:43:17 -040083 return True
84
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040085@_add_method(ttLib.getTableClass('post'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040086def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040087 # TODO Check that italicAngle, underlinePosition, underlineThickness are the same.
88 minMembers = ['underlinePosition', 'minMemType42', 'minMemType1']
89 # Negate some members
90 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040091 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040092 setattr(table, key, -getattr(table, key))
93 # Get max over members
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040094 keys = set(sum((vars(table).keys() for table in m.tables), []))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040095 if 'mapping' in keys:
96 keys.remove('mapping')
97 keys.remove('extraNames')
98 for key in keys:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040099 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400100 # Negate them back
101 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400102 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400103 setattr(table, key, -getattr(table, key))
104 setattr(self, key, -getattr(self, key))
105 self.mapping = {}
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400106 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400107 if hasattr(table, 'mapping'):
108 self.mapping.update(table.mapping)
109 self.extraNames = []
110 return True
111
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400112@_add_method(ttLib.getTableClass('vmtx'),
113 ttLib.getTableClass('hmtx'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400114def merge(self, m):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400115 self.metrics = {}
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400116 for table in m.tables:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400117 self.metrics.update(table.metrics)
118 return True
119
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400120@_add_method(ttLib.getTableClass('loca'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400121def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400122 return True # Will be computed automatically
123
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400124@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400125def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400126 self.glyphs = {}
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400127 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400128 self.glyphs.update(table.glyphs)
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400129 # Drop hints for now, since we don't remap functions / CVT values.
130 for g in self.glyphs.values():
131 g.removeHinting()
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400132 return True
133
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400134@_add_method(ttLib.getTableClass('prep'),
135 ttLib.getTableClass('fpgm'),
136 ttLib.getTableClass('cvt '))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400137def merge(self, m):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400138 return False # Will be computed automatically
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400139
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400140@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400141def merge(self, m):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400142 # TODO Handle format=14.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400143 cmapTables = [t for table in m.tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400144 if t.platformID == 3 and t.platEncID in [1, 10]]
145 # TODO Better handle format-4 and format-12 coexisting in same font.
146 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400147 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400148 assert all(t.format in [4, 12] for t in cmapTables)
149 format = max(t.format for t in cmapTables)
150 cmapTable = module.cmap_classes[format](format)
151 cmapTable.cmap = {}
152 cmapTable.platformID = 3
153 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
154 cmapTable.language = 0
155 for table in cmapTables:
156 # TODO handle duplicates.
157 cmapTable.cmap.update(table.cmap)
158 self.tableVersion = 0
159 self.tables = [cmapTable]
160 self.numSubTables = len(self.tables)
161 return True
162
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400163@_add_method(ttLib.getTableClass('GDEF'))
164def merge(self, m):
165 self.table = otTables.GDEF()
166 self.table.Version = 1.0
167
168 if any(t.table.LigCaretList for t in m.tables):
169 glyphs = []
170 ligGlyphs = []
171 for table in m.tables:
172 if table.table.LigCaretList:
173 glyphs.extend(table.table.LigCaretList.Coverage.glyphs)
174 ligGlyphs.extend(table.table.LigCaretList.LigGlyph)
175 coverage = otTables.Coverage()
176 coverage.glyphs = glyphs
177 ligCaretList = otTables.LigCaretList()
178 ligCaretList.Coverage = coverage
179 ligCaretList.LigGlyph = ligGlyphs
180 ligCaretList.GlyphCount = len(ligGlyphs)
181 self.table.LigCaretList = ligCaretList
182 else:
183 self.table.LigCaretList = None
184
185 if any(t.table.MarkAttachClassDef for t in m.tables):
186 classDefs = {}
187 for table in m.tables:
188 if table.table.MarkAttachClassDef:
189 classDefs.update(table.table.MarkAttachClassDef.classDefs)
190 self.table.MarkAttachClassDef = otTables.MarkAttachClassDef()
191 self.table.MarkAttachClassDef.classDefs = classDefs
192 else:
193 self.table.MarkAttachClassDef = None
194
195 if any(t.table.GlyphClassDef for t in m.tables):
196 classDefs = {}
197 for table in m.tables:
198 if table.table.GlyphClassDef:
199 classDefs.update(table.table.GlyphClassDef.classDefs)
200 self.table.GlyphClassDef = otTables.GlyphClassDef()
201 self.table.GlyphClassDef.classDefs = classDefs
202 else:
203 self.table.GlyphClassDef = None
204
205 if any(t.table.AttachList for t in m.tables):
206 glyphs = []
207 attachPoints = []
208 for table in m.tables:
209 if table.table.AttachList:
210 glyphs.extend(table.table.AttachList.Coverage.glyphs)
211 attachPoints.extend(table.table.AttachList.AttachPoint)
212 coverage = otTables.Coverage()
213 coverage.glyphs = glyphs
214 attachList = otTables.AttachList()
215 attachList.Coverage = coverage
216 attachList.AttachPoint = attachPoints
217 attachList.GlyphCount = len(attachPoints)
218 self.table.AttachList = attachList
219 else:
220 self.table.AttachList = None
221
222 return True
223
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400224
225class Options(object):
226
227 class UnknownOptionError(Exception):
228 pass
229
230 _drop_tables_default = ['fpgm', 'prep', 'cvt ', 'gasp']
231 drop_tables = _drop_tables_default
232
233 def __init__(self, **kwargs):
234
235 self.set(**kwargs)
236
237 def set(self, **kwargs):
238 for k,v in kwargs.iteritems():
239 if not hasattr(self, k):
240 raise self.UnknownOptionError("Unknown option '%s'" % k)
241 setattr(self, k, v)
242
243 def parse_opts(self, argv, ignore_unknown=False):
244 ret = []
245 opts = {}
246 for a in argv:
247 orig_a = a
248 if not a.startswith('--'):
249 ret.append(a)
250 continue
251 a = a[2:]
252 i = a.find('=')
253 op = '='
254 if i == -1:
255 if a.startswith("no-"):
256 k = a[3:]
257 v = False
258 else:
259 k = a
260 v = True
261 else:
262 k = a[:i]
263 if k[-1] in "-+":
264 op = k[-1]+'=' # Ops is '-=' or '+=' now.
265 k = k[:-1]
266 v = a[i+1:]
267 k = k.replace('-', '_')
268 if not hasattr(self, k):
269 if ignore_unknown == True or k in ignore_unknown:
270 ret.append(orig_a)
271 continue
272 else:
273 raise self.UnknownOptionError("Unknown option '%s'" % a)
274
275 ov = getattr(self, k)
276 if isinstance(ov, bool):
277 v = bool(v)
278 elif isinstance(ov, int):
279 v = int(v)
280 elif isinstance(ov, list):
281 vv = v.split(',')
282 if vv == ['']:
283 vv = []
284 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
285 if op == '=':
286 v = vv
287 elif op == '+=':
288 v = ov
289 v.extend(vv)
290 elif op == '-=':
291 v = ov
292 for x in vv:
293 if x in v:
294 v.remove(x)
295 else:
296 assert 0
297
298 opts[k] = v
299 self.set(**opts)
300
301 return ret
302
303
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400304class Merger:
305
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400306 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400307
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400308 if not log:
309 log = Logger()
310 if not options:
311 options = Options()
312
313 self.options = options
314 self.log = log
315
316 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400317
318 mega = ttLib.TTFont()
319
320 #
321 # Settle on a mega glyph order.
322 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400323 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400324 glyphOrders = [font.getGlyphOrder() for font in fonts]
325 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
326 # Reload fonts and set new glyph names on them.
327 # TODO Is it necessary to reload font? I think it is. At least
328 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400329 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400330 for font,glyphOrder in zip(fonts, glyphOrders):
331 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400332 mega.setGlyphOrder(megaGlyphOrder)
333
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400334 allTags = reduce(set.union, (font.keys() for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400335 allTags.remove('GlyphOrder')
336 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400337
338 if tag in self.options.drop_tables:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400339 self.log("Dropping '%s'." % tag)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400340 continue
341
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400342 clazz = ttLib.getTableClass(tag)
343
344 if not hasattr(clazz, 'merge'):
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400345 self.log("Don't know how to merge '%s', dropped." % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400346 continue
347
348 # TODO For now assume all fonts have the same tables.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400349 self.tables = [font[tag] for font in fonts]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400350 table = clazz(tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400351 if table.merge (self):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400352 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400353 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400354 else:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400355 self.log("Dropped '%s'. No need to merge explicitly." % tag)
356 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400357 del self.tables
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400358
359 return mega
360
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400361 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400362 """Modifies passed-in glyphOrders to reflect new glyph names.
363 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400364 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400365 # TODO Even this simplistic numbering can result in conflicts.
366 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400367 mega = []
368 for n,glyphOrder in enumerate(glyphOrders):
369 for i,glyphName in enumerate(glyphOrder):
370 glyphName += "#" + `n`
371 glyphOrder[i] = glyphName
372 mega.append(glyphName)
373 return mega
374
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400375
376class Logger(object):
377
378 def __init__(self, verbose=False, xml=False, timing=False):
379 self.verbose = verbose
380 self.xml = xml
381 self.timing = timing
382 self.last_time = self.start_time = time.time()
383
384 def parse_opts(self, argv):
385 argv = argv[:]
386 for v in ['verbose', 'xml', 'timing']:
387 if "--"+v in argv:
388 setattr(self, v, True)
389 argv.remove("--"+v)
390 return argv
391
392 def __call__(self, *things):
393 if not self.verbose:
394 return
395 print ' '.join(str(x) for x in things)
396
397 def lapse(self, *things):
398 if not self.timing:
399 return
400 new_time = time.time()
401 print "Took %0.3fs to %s" %(new_time - self.last_time,
402 ' '.join(str(x) for x in things))
403 self.last_time = new_time
404
405 def font(self, font, file=sys.stdout):
406 if not self.xml:
407 return
408 from fontTools.misc import xmlWriter
409 writer = xmlWriter.XMLWriter(file)
410 font.disassembleInstructions = False # Work around ttLib bug
411 for tag in font.keys():
412 writer.begintag(tag)
413 writer.newline()
414 font[tag].toXML(writer, font)
415 writer.endtag(tag)
416 writer.newline()
417
418
419__all__ = [
420 'Options',
421 'Merger',
422 'Logger',
423 'main'
424]
425
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400426def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400427
428 log = Logger()
429 args = log.parse_opts(args)
430
431 options = Options()
432 args = options.parse_opts(args)
433
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400434 if len(args) < 1:
435 print >>sys.stderr, "usage: pyftmerge font..."
436 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400437
438 merger = Merger(options=options, log=log)
439 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400440 outfile = 'merged.ttf'
441 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400442 log.lapse("compile and save font")
443
444 log.last_time = log.start_time
445 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400446
447if __name__ == "__main__":
448 main(sys.argv[1:])