blob: b576b595bb27b861d47ca3bb50458bacb481e94a [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 Esfahbod6942b222013-09-20 16:57:28 -040034 allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
35 for key in allKeys:
Behdad Esfahbod3235a042013-09-19 20:57:33 -040036 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040037 return True
38
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040039@_add_method(ttLib.getTableClass('head'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040040def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040041 # TODO Check that unitsPerEm are the same.
42 # TODO Use bitwise ops for flags, macStyle, fontDirectionHint
43 minMembers = ['xMin', 'yMin']
44 # Negate some members
45 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040046 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040047 setattr(table, key, -getattr(table, key))
48 # Get max over members
Behdad Esfahbod6942b222013-09-20 16:57:28 -040049 allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
50 for key in allKeys:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040051 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040052 # Negate them back
53 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040054 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040055 setattr(table, key, -getattr(table, key))
56 setattr(self, key, -getattr(self, key))
57 return True
58
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040059@_add_method(ttLib.getTableClass('hhea'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040060def merge(self, m):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040061 # TODO Check that ascent, descent, slope, etc are the same.
62 minMembers = ['descent', 'minLeftSideBearing', 'minRightSideBearing']
63 # Negate some members
64 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040065 for table in m.tables:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040066 setattr(table, key, -getattr(table, key))
67 # Get max over members
Behdad Esfahbod6942b222013-09-20 16:57:28 -040068 allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
69 for key in allKeys:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040070 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040071 # Negate them back
72 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040073 for table in m.tables:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -040074 setattr(table, key, -getattr(table, key))
75 setattr(self, key, -getattr(self, key))
76 return True
77
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040078@_add_method(ttLib.getTableClass('OS/2'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040079def merge(self, m):
Behdad Esfahbod71294de2013-09-19 19:43:17 -040080 # TODO Check that weight/width/subscript/superscript/etc are the same.
81 # TODO Bitwise ops for UnicodeRange/CodePageRange.
82 # TODO Pretty much all fields generated here have bogus values.
83 # Get max over members
Behdad Esfahbod6942b222013-09-20 16:57:28 -040084 allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
85 for key in allKeys:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040086 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbod71294de2013-09-19 19:43:17 -040087 return True
88
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -040089@_add_method(ttLib.getTableClass('post'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040090def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040091 # TODO Check that italicAngle, underlinePosition, underlineThickness are the same.
92 minMembers = ['underlinePosition', 'minMemType42', 'minMemType1']
93 # Negate some members
94 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -040095 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -040096 setattr(table, key, -getattr(table, key))
97 # Get max over members
Behdad Esfahbod6942b222013-09-20 16:57:28 -040098 allKeys = reduce(set.union, (vars(table).keys() for table in m.tables), set())
99 if 'mapping' in allKeys:
100 allKeys.remove('mapping')
101 allKeys.remove('extraNames')
102 for key in allKeys:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400103 setattr(self, key, max(getattr(table, key) for table in m.tables))
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400104 # Negate them back
105 for key in minMembers:
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400106 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400107 setattr(table, key, -getattr(table, key))
108 setattr(self, key, -getattr(self, key))
109 self.mapping = {}
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400110 for table in m.tables:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400111 if hasattr(table, 'mapping'):
112 self.mapping.update(table.mapping)
113 self.extraNames = []
114 return True
115
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400116@_add_method(ttLib.getTableClass('vmtx'),
117 ttLib.getTableClass('hmtx'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400118def merge(self, m):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400119 self.metrics = {}
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400120 for table in m.tables:
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400121 self.metrics.update(table.metrics)
122 return True
123
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400124@_add_method(ttLib.getTableClass('loca'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400125def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400126 return True # Will be computed automatically
127
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400128@_add_method(ttLib.getTableClass('glyf'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400129def merge(self, m):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400130 self.glyphs = {}
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400131 for table in m.tables:
Behdad Esfahbod43650332013-09-20 16:33:33 -0400132 for g in table.glyphs.values():
133 # Drop hints for now, since we don't remap
134 # functions / CVT values.
135 g.removeHinting()
136 # Expand composite glyphs to load their
137 # composite glyph names.
138 if g.isComposite():
139 g.expand(table)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400140 self.glyphs.update(table.glyphs)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400141 return True
142
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400143@_add_method(ttLib.getTableClass('prep'),
144 ttLib.getTableClass('fpgm'),
145 ttLib.getTableClass('cvt '))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400146def merge(self, m):
Behdad Esfahbod60eb8042013-09-20 16:34:58 -0400147 return False # TODO We don't merge hinting data currently.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400148
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400149@_add_method(ttLib.getTableClass('cmap'))
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400150def merge(self, m):
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400151 # TODO Handle format=14.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400152 cmapTables = [t for table in m.tables for t in table.tables
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400153 if t.platformID == 3 and t.platEncID in [1, 10]]
154 # TODO Better handle format-4 and format-12 coexisting in same font.
155 # TODO Insert both a format-4 and format-12 if needed.
Behdad Esfahbodbe4ecc72013-09-19 20:37:01 -0400156 module = ttLib.getTableModule('cmap')
Behdad Esfahbod71294de2013-09-19 19:43:17 -0400157 assert all(t.format in [4, 12] for t in cmapTables)
158 format = max(t.format for t in cmapTables)
159 cmapTable = module.cmap_classes[format](format)
160 cmapTable.cmap = {}
161 cmapTable.platformID = 3
162 cmapTable.platEncID = max(t.platEncID for t in cmapTables)
163 cmapTable.language = 0
164 for table in cmapTables:
165 # TODO handle duplicates.
166 cmapTable.cmap.update(table.cmap)
167 self.tableVersion = 0
168 self.tables = [cmapTable]
169 self.numSubTables = len(self.tables)
170 return True
171
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400172@_add_method(ttLib.getTableClass('GDEF'))
173def merge(self, m):
174 self.table = otTables.GDEF()
Behdad Esfahbod60eb8042013-09-20 16:34:58 -0400175 self.table.Version = 1.0 # TODO version 1.2...
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400176
177 if any(t.table.LigCaretList for t in m.tables):
178 glyphs = []
179 ligGlyphs = []
180 for table in m.tables:
181 if table.table.LigCaretList:
182 glyphs.extend(table.table.LigCaretList.Coverage.glyphs)
183 ligGlyphs.extend(table.table.LigCaretList.LigGlyph)
184 coverage = otTables.Coverage()
185 coverage.glyphs = glyphs
186 ligCaretList = otTables.LigCaretList()
187 ligCaretList.Coverage = coverage
188 ligCaretList.LigGlyph = ligGlyphs
189 ligCaretList.GlyphCount = len(ligGlyphs)
190 self.table.LigCaretList = ligCaretList
191 else:
192 self.table.LigCaretList = None
193
194 if any(t.table.MarkAttachClassDef for t in m.tables):
195 classDefs = {}
196 for table in m.tables:
197 if table.table.MarkAttachClassDef:
198 classDefs.update(table.table.MarkAttachClassDef.classDefs)
199 self.table.MarkAttachClassDef = otTables.MarkAttachClassDef()
200 self.table.MarkAttachClassDef.classDefs = classDefs
201 else:
202 self.table.MarkAttachClassDef = None
203
204 if any(t.table.GlyphClassDef for t in m.tables):
205 classDefs = {}
206 for table in m.tables:
207 if table.table.GlyphClassDef:
208 classDefs.update(table.table.GlyphClassDef.classDefs)
209 self.table.GlyphClassDef = otTables.GlyphClassDef()
210 self.table.GlyphClassDef.classDefs = classDefs
211 else:
212 self.table.GlyphClassDef = None
213
214 if any(t.table.AttachList for t in m.tables):
215 glyphs = []
216 attachPoints = []
217 for table in m.tables:
218 if table.table.AttachList:
219 glyphs.extend(table.table.AttachList.Coverage.glyphs)
220 attachPoints.extend(table.table.AttachList.AttachPoint)
221 coverage = otTables.Coverage()
222 coverage.glyphs = glyphs
223 attachList = otTables.AttachList()
224 attachList.Coverage = coverage
225 attachList.AttachPoint = attachPoints
226 attachList.GlyphCount = len(attachPoints)
227 self.table.AttachList = attachList
228 else:
229 self.table.AttachList = None
230
231 return True
232
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400233
234class Options(object):
235
236 class UnknownOptionError(Exception):
237 pass
238
239 _drop_tables_default = ['fpgm', 'prep', 'cvt ', 'gasp']
240 drop_tables = _drop_tables_default
241
242 def __init__(self, **kwargs):
243
244 self.set(**kwargs)
245
246 def set(self, **kwargs):
247 for k,v in kwargs.iteritems():
248 if not hasattr(self, k):
249 raise self.UnknownOptionError("Unknown option '%s'" % k)
250 setattr(self, k, v)
251
252 def parse_opts(self, argv, ignore_unknown=False):
253 ret = []
254 opts = {}
255 for a in argv:
256 orig_a = a
257 if not a.startswith('--'):
258 ret.append(a)
259 continue
260 a = a[2:]
261 i = a.find('=')
262 op = '='
263 if i == -1:
264 if a.startswith("no-"):
265 k = a[3:]
266 v = False
267 else:
268 k = a
269 v = True
270 else:
271 k = a[:i]
272 if k[-1] in "-+":
273 op = k[-1]+'=' # Ops is '-=' or '+=' now.
274 k = k[:-1]
275 v = a[i+1:]
276 k = k.replace('-', '_')
277 if not hasattr(self, k):
278 if ignore_unknown == True or k in ignore_unknown:
279 ret.append(orig_a)
280 continue
281 else:
282 raise self.UnknownOptionError("Unknown option '%s'" % a)
283
284 ov = getattr(self, k)
285 if isinstance(ov, bool):
286 v = bool(v)
287 elif isinstance(ov, int):
288 v = int(v)
289 elif isinstance(ov, list):
290 vv = v.split(',')
291 if vv == ['']:
292 vv = []
293 vv = [int(x, 0) if len(x) and x[0] in "0123456789" else x for x in vv]
294 if op == '=':
295 v = vv
296 elif op == '+=':
297 v = ov
298 v.extend(vv)
299 elif op == '-=':
300 v = ov
301 for x in vv:
302 if x in v:
303 v.remove(x)
304 else:
305 assert 0
306
307 opts[k] = v
308 self.set(**opts)
309
310 return ret
311
312
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400313class Merger:
314
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400315 def __init__(self, options=None, log=None):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400316
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400317 if not log:
318 log = Logger()
319 if not options:
320 options = Options()
321
322 self.options = options
323 self.log = log
324
325 def merge(self, fontfiles):
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400326
327 mega = ttLib.TTFont()
328
329 #
330 # Settle on a mega glyph order.
331 #
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400332 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400333 glyphOrders = [font.getGlyphOrder() for font in fonts]
334 megaGlyphOrder = self._mergeGlyphOrders(glyphOrders)
335 # Reload fonts and set new glyph names on them.
336 # TODO Is it necessary to reload font? I think it is. At least
337 # it's safer, in case tables were loaded to provide glyph names.
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400338 fonts = [ttLib.TTFont(fontfile) for fontfile in fontfiles]
Behdad Esfahbod3235a042013-09-19 20:57:33 -0400339 for font,glyphOrder in zip(fonts, glyphOrders):
340 font.setGlyphOrder(glyphOrder)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400341 mega.setGlyphOrder(megaGlyphOrder)
342
Behdad Esfahbodc14ab482013-09-19 21:22:54 -0400343 allTags = reduce(set.union, (font.keys() for font in fonts), set())
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400344 allTags.remove('GlyphOrder')
345 for tag in allTags:
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400346
347 if tag in self.options.drop_tables:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400348 self.log("Dropping '%s'." % tag)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400349 continue
350
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400351 clazz = ttLib.getTableClass(tag)
352
353 if not hasattr(clazz, 'merge'):
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400354 self.log("Don't know how to merge '%s', dropped." % tag)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400355 continue
356
357 # TODO For now assume all fonts have the same tables.
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400358 self.tables = [font[tag] for font in fonts]
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400359 table = clazz(tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400360 if table.merge (self):
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400361 mega[tag] = table
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400362 self.log("Merged '%s'." % tag)
Behdad Esfahbod65f19d82013-09-18 21:02:41 -0400363 else:
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400364 self.log("Dropped '%s'. No need to merge explicitly." % tag)
365 self.log.lapse("merge '%s'" % tag)
Behdad Esfahbod0bf4f562013-09-19 20:21:04 -0400366 del self.tables
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400367
368 return mega
369
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400370 def _mergeGlyphOrders(self, glyphOrders):
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400371 """Modifies passed-in glyphOrders to reflect new glyph names.
372 Returns glyphOrder for the merged font."""
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400373 # Simply append font index to the glyph name for now.
Behdad Esfahbodc2e27fd2013-09-20 16:25:48 -0400374 # TODO Even this simplistic numbering can result in conflicts.
375 # But then again, we have to improve this soon anyway.
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400376 mega = []
377 for n,glyphOrder in enumerate(glyphOrders):
378 for i,glyphName in enumerate(glyphOrder):
379 glyphName += "#" + `n`
380 glyphOrder[i] = glyphName
381 mega.append(glyphName)
382 return mega
383
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400384
385class Logger(object):
386
387 def __init__(self, verbose=False, xml=False, timing=False):
388 self.verbose = verbose
389 self.xml = xml
390 self.timing = timing
391 self.last_time = self.start_time = time.time()
392
393 def parse_opts(self, argv):
394 argv = argv[:]
395 for v in ['verbose', 'xml', 'timing']:
396 if "--"+v in argv:
397 setattr(self, v, True)
398 argv.remove("--"+v)
399 return argv
400
401 def __call__(self, *things):
402 if not self.verbose:
403 return
404 print ' '.join(str(x) for x in things)
405
406 def lapse(self, *things):
407 if not self.timing:
408 return
409 new_time = time.time()
410 print "Took %0.3fs to %s" %(new_time - self.last_time,
411 ' '.join(str(x) for x in things))
412 self.last_time = new_time
413
414 def font(self, font, file=sys.stdout):
415 if not self.xml:
416 return
417 from fontTools.misc import xmlWriter
418 writer = xmlWriter.XMLWriter(file)
419 font.disassembleInstructions = False # Work around ttLib bug
420 for tag in font.keys():
421 writer.begintag(tag)
422 writer.newline()
423 font[tag].toXML(writer, font)
424 writer.endtag(tag)
425 writer.newline()
426
427
428__all__ = [
429 'Options',
430 'Merger',
431 'Logger',
432 'main'
433]
434
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400435def main(args):
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400436
437 log = Logger()
438 args = log.parse_opts(args)
439
440 options = Options()
441 args = options.parse_opts(args)
442
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400443 if len(args) < 1:
444 print >>sys.stderr, "usage: pyftmerge font..."
445 sys.exit(1)
Behdad Esfahbodf2d59822013-09-19 16:16:39 -0400446
447 merger = Merger(options=options, log=log)
448 font = merger.merge(args)
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400449 outfile = 'merged.ttf'
450 font.save(outfile)
Behdad Esfahbodb640f742013-09-19 20:12:56 -0400451 log.lapse("compile and save font")
452
453 log.last_time = log.start_time
454 log.lapse("make one with everything(TOTAL TIME)")
Behdad Esfahbod45d2f382013-09-18 20:47:53 -0400455
456if __name__ == "__main__":
457 main(sys.argv[1:])