blob: 8a30bbbf1f893aad0bde1421995962b531c37530 [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)
129 # TODO Drop hints?
130 return True
131
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400132@_add_method(ttLib.getTableClass('prep'),
133 ttLib.getTableClass('fpgm'),
134 ttLib.getTableClass('cvt '))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400135def merge(self, m):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400136 return False # Will be computed automatically
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400137
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400138@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400139def merge(self, m):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400140 # TODO Handle format=14.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400141 cmapTables = [t for table in m.tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400142 if t.platformID == 3 and t.platEncID in [1, 10]]
143 # TODO Better handle format-4 and format-12 coexisting in same font.
144 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400145 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400146 assert all(t.format in [4, 12] for t in cmapTables)
147 format = max(t.format for t in cmapTables)
148 cmapTable = module.cmap_classes[format](format)
149 cmapTable.cmap = {}
150 cmapTable.platformID = 3
151 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
152 cmapTable.language = 0
153 for table in cmapTables:
154 # TODO handle duplicates.
155 cmapTable.cmap.update(table.cmap)
156 self.tableVersion = 0
157 self.tables = [cmapTable]
158 self.numSubTables = len(self.tables)
159 return True
160
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400161@_add_method(ttLib.getTableClass('GDEF'))
162def merge(self, m):
163 self.table = otTables.GDEF()
164 self.table.Version = 1.0
165
166 if any(t.table.LigCaretList for t in m.tables):
167 glyphs = []
168 ligGlyphs = []
169 for table in m.tables:
170 if table.table.LigCaretList:
171 glyphs.extend(table.table.LigCaretList.Coverage.glyphs)
172 ligGlyphs.extend(table.table.LigCaretList.LigGlyph)
173 coverage = otTables.Coverage()
174 coverage.glyphs = glyphs
175 ligCaretList = otTables.LigCaretList()
176 ligCaretList.Coverage = coverage
177 ligCaretList.LigGlyph = ligGlyphs
178 ligCaretList.GlyphCount = len(ligGlyphs)
179 self.table.LigCaretList = ligCaretList
180 else:
181 self.table.LigCaretList = None
182
183 if any(t.table.MarkAttachClassDef for t in m.tables):
184 classDefs = {}
185 for table in m.tables:
186 if table.table.MarkAttachClassDef:
187 classDefs.update(table.table.MarkAttachClassDef.classDefs)
188 self.table.MarkAttachClassDef = otTables.MarkAttachClassDef()
189 self.table.MarkAttachClassDef.classDefs = classDefs
190 else:
191 self.table.MarkAttachClassDef = None
192
193 if any(t.table.GlyphClassDef for t in m.tables):
194 classDefs = {}
195 for table in m.tables:
196 if table.table.GlyphClassDef:
197 classDefs.update(table.table.GlyphClassDef.classDefs)
198 self.table.GlyphClassDef = otTables.GlyphClassDef()
199 self.table.GlyphClassDef.classDefs = classDefs
200 else:
201 self.table.GlyphClassDef = None
202
203 if any(t.table.AttachList for t in m.tables):
204 glyphs = []
205 attachPoints = []
206 for table in m.tables:
207 if table.table.AttachList:
208 glyphs.extend(table.table.AttachList.Coverage.glyphs)
209 attachPoints.extend(table.table.AttachList.AttachPoint)
210 coverage = otTables.Coverage()
211 coverage.glyphs = glyphs
212 attachList = otTables.AttachList()
213 attachList.Coverage = coverage
214 attachList.AttachPoint = attachPoints
215 attachList.GlyphCount = len(attachPoints)
216 self.table.AttachList = attachList
217 else:
218 self.table.AttachList = None
219
220 return True
221
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400222
223class Options(object):
224
225 class UnknownOptionError(Exception):
226 pass
227
228 _drop_tables_default = ['fpgm', 'prep', 'cvt ', 'gasp']
229 drop_tables = _drop_tables_default
230
231 def __init__(self, **kwargs):
232
233 self.set(**kwargs)
234
235 def set(self, **kwargs):
236 for k,v in kwargs.iteritems():
237 if not hasattr(self, k):
238 raise self.UnknownOptionError("Unknown option '%s'" % k)
239 setattr(self, k, v)
240
241 def parse_opts(self, argv, ignore_unknown=False):
242 ret = []
243 opts = {}
244 for a in argv:
245 orig_a = a
246 if not a.startswith('--'):
247 ret.append(a)
248 continue
249 a = a[2:]
250 i = a.find('=')
251 op = '='
252 if i == -1:
253 if a.startswith("no-"):
254 k = a[3:]
255 v = False
256 else:
257 k = a
258 v = True
259 else:
260 k = a[:i]
261 if k[-1] in "-+":
262 op = k[-1]+'=' # Ops is '-=' or '+=' now.
263 k = k[:-1]
264 v = a[i+1:]
265 k = k.replace('-', '_')
266 if not hasattr(self, k):
267 if ignore_unknown == True or k in ignore_unknown:
268 ret.append(orig_a)
269 continue
270 else:
271 raise self.UnknownOptionError("Unknown option '%s'" % a)
272
273 ov = getattr(self, k)
274 if isinstance(ov, bool):
275 v = bool(v)
276 elif isinstance(ov, int):
277 v = int(v)
278 elif isinstance(ov, list):
279 vv = v.split(',')
280 if vv == ['']:
281 vv = []
282 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
283 if op == '=':
284 v = vv
285 elif op == '+=':
286 v = ov
287 v.extend(vv)
288 elif op == '-=':
289 v = ov
290 for x in vv:
291 if x in v:
292 v.remove(x)
293 else:
294 assert 0
295
296 opts[k] = v
297 self.set(**opts)
298
299 return ret
300
301
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400302class Merger:
303
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400304 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400305
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400306 if not log:
307 log = Logger()
308 if not options:
309 options = Options()
310
311 self.options = options
312 self.log = log
313
314 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400315
316 mega = ttLib.TTFont()
317
318 #
319 # Settle on a mega glyph order.
320 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400321 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400322 glyphOrders = [font.getGlyphOrder() for font in fonts]
323 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
324 # Reload fonts and set new glyph names on them.
325 # TODO Is it necessary to reload font? I think it is. At least
326 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400327 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400328 for font,glyphOrder in zip(fonts, glyphOrders):
329 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400330 mega.setGlyphOrder(megaGlyphOrder)
331
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400332 allTags = reduce(set.union, (font.keys() for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400333 allTags.remove('GlyphOrder')
334 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400335
336 if tag in self.options.drop_tables:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400337 self.log("Dropping '%s'." % tag)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400338 continue
339
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400340 clazz = ttLib.getTableClass(tag)
341
342 if not hasattr(clazz, 'merge'):
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400343 self.log("Don't know how to merge '%s', dropped." % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400344 continue
345
346 # TODO For now assume all fonts have the same tables.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400347 self.tables = [font[tag] for font in fonts]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400348 table = clazz(tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400349 if table.merge (self):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400350 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400351 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400352 else:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400353 self.log("Dropped '%s'. No need to merge explicitly." % tag)
354 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400355 del self.tables
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400356
357 return mega
358
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400359 def _mergeGlyphOrders(self, glyphOrders):
360 """Modifies passed-in glyphOrders to reflect new glyph names."""
361 # Simply append font index to the glyph name for now.
362 mega = []
363 for n,glyphOrder in enumerate(glyphOrders):
364 for i,glyphName in enumerate(glyphOrder):
365 glyphName += "#" + `n`
366 glyphOrder[i] = glyphName
367 mega.append(glyphName)
368 return mega
369
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400370
371class Logger(object):
372
373 def __init__(self, verbose=False, xml=False, timing=False):
374 self.verbose = verbose
375 self.xml = xml
376 self.timing = timing
377 self.last_time = self.start_time = time.time()
378
379 def parse_opts(self, argv):
380 argv = argv[:]
381 for v in ['verbose', 'xml', 'timing']:
382 if "--"+v in argv:
383 setattr(self, v, True)
384 argv.remove("--"+v)
385 return argv
386
387 def __call__(self, *things):
388 if not self.verbose:
389 return
390 print ' '.join(str(x) for x in things)
391
392 def lapse(self, *things):
393 if not self.timing:
394 return
395 new_time = time.time()
396 print "Took %0.3fs to %s" %(new_time - self.last_time,
397 ' '.join(str(x) for x in things))
398 self.last_time = new_time
399
400 def font(self, font, file=sys.stdout):
401 if not self.xml:
402 return
403 from fontTools.misc import xmlWriter
404 writer = xmlWriter.XMLWriter(file)
405 font.disassembleInstructions = False # Work around ttLib bug
406 for tag in font.keys():
407 writer.begintag(tag)
408 writer.newline()
409 font[tag].toXML(writer, font)
410 writer.endtag(tag)
411 writer.newline()
412
413
414__all__ = [
415 'Options',
416 'Merger',
417 'Logger',
418 'main'
419]
420
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400421def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400422
423 log = Logger()
424 args = log.parse_opts(args)
425
426 options = Options()
427 args = options.parse_opts(args)
428
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400429 if len(args) < 1:
430 print >>sys.stderr, "usage: pyftmerge font..."
431 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400432
433 merger = Merger(options=options, log=log)
434 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400435 outfile = 'merged.ttf'
436 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400437 log.lapse("compile and save font")
438
439 log.last_time = log.start_time
440 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400441
442if __name__ == "__main__":
443 main(sys.argv[1:])