blob: c2e6b1ed3ac60dfd711be35c1fbbe19f22b7e5f0 [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 Esfahbod43650332013-09-20 16:33:33 -0400128 for g in table.glyphs.values():
129 # Drop hints for now, since we don't remap
130 # functions / CVT values.
131 g.removeHinting()
132 # Expand composite glyphs to load their
133 # composite glyph names.
134 if g.isComposite():
135 g.expand(table)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400136 self.glyphs.update(table.glyphs)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400137 return True
138
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400139@_add_method(ttLib.getTableClass('prep'),
140 ttLib.getTableClass('fpgm'),
141 ttLib.getTableClass('cvt '))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400142def merge(self, m):
Behdad Esfahbod60eb8042013-09-20 16:34:58 -0400143 return False # TODO We don't merge hinting data currently.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400144
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400145@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400146def merge(self, m):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400147 # TODO Handle format=14.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400148 cmapTables = [t for table in m.tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400149 if t.platformID == 3 and t.platEncID in [1, 10]]
150 # TODO Better handle format-4 and format-12 coexisting in same font.
151 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400152 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400153 assert all(t.format in [4, 12] for t in cmapTables)
154 format = max(t.format for t in cmapTables)
155 cmapTable = module.cmap_classes[format](format)
156 cmapTable.cmap = {}
157 cmapTable.platformID = 3
158 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
159 cmapTable.language = 0
160 for table in cmapTables:
161 # TODO handle duplicates.
162 cmapTable.cmap.update(table.cmap)
163 self.tableVersion = 0
164 self.tables = [cmapTable]
165 self.numSubTables = len(self.tables)
166 return True
167
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400168@_add_method(ttLib.getTableClass('GDEF'))
169def merge(self, m):
170 self.table = otTables.GDEF()
Behdad Esfahbod60eb8042013-09-20 16:34:58 -0400171 self.table.Version = 1.0 # TODO version 1.2...
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400172
173 if any(t.table.LigCaretList for t in m.tables):
174 glyphs = []
175 ligGlyphs = []
176 for table in m.tables:
177 if table.table.LigCaretList:
178 glyphs.extend(table.table.LigCaretList.Coverage.glyphs)
179 ligGlyphs.extend(table.table.LigCaretList.LigGlyph)
180 coverage = otTables.Coverage()
181 coverage.glyphs = glyphs
182 ligCaretList = otTables.LigCaretList()
183 ligCaretList.Coverage = coverage
184 ligCaretList.LigGlyph = ligGlyphs
185 ligCaretList.GlyphCount = len(ligGlyphs)
186 self.table.LigCaretList = ligCaretList
187 else:
188 self.table.LigCaretList = None
189
190 if any(t.table.MarkAttachClassDef for t in m.tables):
191 classDefs = {}
192 for table in m.tables:
193 if table.table.MarkAttachClassDef:
194 classDefs.update(table.table.MarkAttachClassDef.classDefs)
195 self.table.MarkAttachClassDef = otTables.MarkAttachClassDef()
196 self.table.MarkAttachClassDef.classDefs = classDefs
197 else:
198 self.table.MarkAttachClassDef = None
199
200 if any(t.table.GlyphClassDef for t in m.tables):
201 classDefs = {}
202 for table in m.tables:
203 if table.table.GlyphClassDef:
204 classDefs.update(table.table.GlyphClassDef.classDefs)
205 self.table.GlyphClassDef = otTables.GlyphClassDef()
206 self.table.GlyphClassDef.classDefs = classDefs
207 else:
208 self.table.GlyphClassDef = None
209
210 if any(t.table.AttachList for t in m.tables):
211 glyphs = []
212 attachPoints = []
213 for table in m.tables:
214 if table.table.AttachList:
215 glyphs.extend(table.table.AttachList.Coverage.glyphs)
216 attachPoints.extend(table.table.AttachList.AttachPoint)
217 coverage = otTables.Coverage()
218 coverage.glyphs = glyphs
219 attachList = otTables.AttachList()
220 attachList.Coverage = coverage
221 attachList.AttachPoint = attachPoints
222 attachList.GlyphCount = len(attachPoints)
223 self.table.AttachList = attachList
224 else:
225 self.table.AttachList = None
226
227 return True
228
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400229
230class Options(object):
231
232 class UnknownOptionError(Exception):
233 pass
234
235 _drop_tables_default = ['fpgm', 'prep', 'cvt ', 'gasp']
236 drop_tables = _drop_tables_default
237
238 def __init__(self, **kwargs):
239
240 self.set(**kwargs)
241
242 def set(self, **kwargs):
243 for k,v in kwargs.iteritems():
244 if not hasattr(self, k):
245 raise self.UnknownOptionError("Unknown option '%s'" % k)
246 setattr(self, k, v)
247
248 def parse_opts(self, argv, ignore_unknown=False):
249 ret = []
250 opts = {}
251 for a in argv:
252 orig_a = a
253 if not a.startswith('--'):
254 ret.append(a)
255 continue
256 a = a[2:]
257 i = a.find('=')
258 op = '='
259 if i == -1:
260 if a.startswith("no-"):
261 k = a[3:]
262 v = False
263 else:
264 k = a
265 v = True
266 else:
267 k = a[:i]
268 if k[-1] in "-+":
269 op = k[-1]+'=' # Ops is '-=' or '+=' now.
270 k = k[:-1]
271 v = a[i+1:]
272 k = k.replace('-', '_')
273 if not hasattr(self, k):
274 if ignore_unknown == True or k in ignore_unknown:
275 ret.append(orig_a)
276 continue
277 else:
278 raise self.UnknownOptionError("Unknown option '%s'" % a)
279
280 ov = getattr(self, k)
281 if isinstance(ov, bool):
282 v = bool(v)
283 elif isinstance(ov, int):
284 v = int(v)
285 elif isinstance(ov, list):
286 vv = v.split(',')
287 if vv == ['']:
288 vv = []
289 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
290 if op == '=':
291 v = vv
292 elif op == '+=':
293 v = ov
294 v.extend(vv)
295 elif op == '-=':
296 v = ov
297 for x in vv:
298 if x in v:
299 v.remove(x)
300 else:
301 assert 0
302
303 opts[k] = v
304 self.set(**opts)
305
306 return ret
307
308
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400309class Merger:
310
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400311 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400312
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400313 if not log:
314 log = Logger()
315 if not options:
316 options = Options()
317
318 self.options = options
319 self.log = log
320
321 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400322
323 mega = ttLib.TTFont()
324
325 #
326 # Settle on a mega glyph order.
327 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400328 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400329 glyphOrders = [font.getGlyphOrder() for font in fonts]
330 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
331 # Reload fonts and set new glyph names on them.
332 # TODO Is it necessary to reload font? I think it is. At least
333 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400334 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400335 for font,glyphOrder in zip(fonts, glyphOrders):
336 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400337 mega.setGlyphOrder(megaGlyphOrder)
338
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400339 allTags = reduce(set.union, (font.keys() for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400340 allTags.remove('GlyphOrder')
341 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400342
343 if tag in self.options.drop_tables:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400344 self.log("Dropping '%s'." % tag)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400345 continue
346
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400347 clazz = ttLib.getTableClass(tag)
348
349 if not hasattr(clazz, 'merge'):
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400350 self.log("Don't know how to merge '%s', dropped." % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400351 continue
352
353 # TODO For now assume all fonts have the same tables.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400354 self.tables = [font[tag] for font in fonts]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400355 table = clazz(tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400356 if table.merge (self):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400357 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400358 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400359 else:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400360 self.log("Dropped '%s'. No need to merge explicitly." % tag)
361 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400362 del self.tables
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400363
364 return mega
365
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400366 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400367 """Modifies passed-in glyphOrders to reflect new glyph names.
368 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400369 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400370 # TODO Even this simplistic numbering can result in conflicts.
371 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400372 mega = []
373 for n,glyphOrder in enumerate(glyphOrders):
374 for i,glyphName in enumerate(glyphOrder):
375 glyphName += "#" + `n`
376 glyphOrder[i] = glyphName
377 mega.append(glyphName)
378 return mega
379
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400380
381class Logger(object):
382
383 def __init__(self, verbose=False, xml=False, timing=False):
384 self.verbose = verbose
385 self.xml = xml
386 self.timing = timing
387 self.last_time = self.start_time = time.time()
388
389 def parse_opts(self, argv):
390 argv = argv[:]
391 for v in ['verbose', 'xml', 'timing']:
392 if "--"+v in argv:
393 setattr(self, v, True)
394 argv.remove("--"+v)
395 return argv
396
397 def __call__(self, *things):
398 if not self.verbose:
399 return
400 print ' '.join(str(x) for x in things)
401
402 def lapse(self, *things):
403 if not self.timing:
404 return
405 new_time = time.time()
406 print "Took %0.3fs to %s" %(new_time - self.last_time,
407 ' '.join(str(x) for x in things))
408 self.last_time = new_time
409
410 def font(self, font, file=sys.stdout):
411 if not self.xml:
412 return
413 from fontTools.misc import xmlWriter
414 writer = xmlWriter.XMLWriter(file)
415 font.disassembleInstructions = False # Work around ttLib bug
416 for tag in font.keys():
417 writer.begintag(tag)
418 writer.newline()
419 font[tag].toXML(writer, font)
420 writer.endtag(tag)
421 writer.newline()
422
423
424__all__ = [
425 'Options',
426 'Merger',
427 'Logger',
428 'main'
429]
430
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400431def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400432
433 log = Logger()
434 args = log.parse_opts(args)
435
436 options = Options()
437 args = options.parse_opts(args)
438
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400439 if len(args) < 1:
440 print >>sys.stderr, "usage: pyftmerge font..."
441 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400442
443 merger = Merger(options=options, log=log)
444 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400445 outfile = 'merged.ttf'
446 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400447 log.lapse("compile and save font")
448
449 log.last_time = log.start_time
450 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400451
452if __name__ == "__main__":
453 main(sys.argv[1:])