• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2013 Google, Inc. All Rights Reserved.
2#
3# Google Author(s): Behdad Esfahbod, Roozbeh Pournader
4
5from fontTools import ttLib, cffLib
6from fontTools.ttLib.tables.DefaultTable import DefaultTable
7from fontTools.merge.base import add_method, mergeObjects
8from fontTools.merge.cmap import computeMegaCmap
9from fontTools.merge.util import *
10import logging
11
12
13log = logging.getLogger("fontTools.merge")
14
15
16ttLib.getTableClass('maxp').mergeMap = {
17	'*': max,
18	'tableTag': equal,
19	'tableVersion': equal,
20	'numGlyphs': sum,
21	'maxStorage': first,
22	'maxFunctionDefs': first,
23	'maxInstructionDefs': first,
24	# TODO When we correctly merge hinting data, update these values:
25	# maxFunctionDefs, maxInstructionDefs, maxSizeOfInstructions
26}
27
28headFlagsMergeBitMap = {
29	'size': 16,
30	'*': bitwise_or,
31	1: bitwise_and, # Baseline at y = 0
32	2: bitwise_and, # lsb at x = 0
33	3: bitwise_and, # Force ppem to integer values. FIXME?
34	5: bitwise_and, # Font is vertical
35	6: lambda bit: 0, # Always set to zero
36	11: bitwise_and, # Font data is 'lossless'
37	13: bitwise_and, # Optimized for ClearType
38	14: bitwise_and, # Last resort font. FIXME? equal or first may be better
39	15: lambda bit: 0, # Always set to zero
40}
41
42ttLib.getTableClass('head').mergeMap = {
43	'tableTag': equal,
44	'tableVersion': max,
45	'fontRevision': max,
46	'checkSumAdjustment': lambda lst: 0, # We need *something* here
47	'magicNumber': equal,
48	'flags': mergeBits(headFlagsMergeBitMap),
49	'unitsPerEm': equal,
50	'created': current_time,
51	'modified': current_time,
52	'xMin': min,
53	'yMin': min,
54	'xMax': max,
55	'yMax': max,
56	'macStyle': first,
57	'lowestRecPPEM': max,
58	'fontDirectionHint': lambda lst: 2,
59	'indexToLocFormat': first,
60	'glyphDataFormat': equal,
61}
62
63ttLib.getTableClass('hhea').mergeMap = {
64	'*': equal,
65	'tableTag': equal,
66	'tableVersion': max,
67	'ascent': max,
68	'descent': min,
69	'lineGap': max,
70	'advanceWidthMax': max,
71	'minLeftSideBearing': min,
72	'minRightSideBearing': min,
73	'xMaxExtent': max,
74	'caretSlopeRise': first,
75	'caretSlopeRun': first,
76	'caretOffset': first,
77	'numberOfHMetrics': recalculate,
78}
79
80ttLib.getTableClass('vhea').mergeMap = {
81	'*': equal,
82	'tableTag': equal,
83	'tableVersion': max,
84	'ascent': max,
85	'descent': min,
86	'lineGap': max,
87	'advanceHeightMax': max,
88	'minTopSideBearing': min,
89	'minBottomSideBearing': min,
90	'yMaxExtent': max,
91	'caretSlopeRise': first,
92	'caretSlopeRun': first,
93	'caretOffset': first,
94	'numberOfVMetrics': recalculate,
95}
96
97os2FsTypeMergeBitMap = {
98	'size': 16,
99	'*': lambda bit: 0,
100	1: bitwise_or, # no embedding permitted
101	2: bitwise_and, # allow previewing and printing documents
102	3: bitwise_and, # allow editing documents
103	8: bitwise_or, # no subsetting permitted
104	9: bitwise_or, # no embedding of outlines permitted
105}
106
107def mergeOs2FsType(lst):
108	lst = list(lst)
109	if all(item == 0 for item in lst):
110		return 0
111
112	# Compute least restrictive logic for each fsType value
113	for i in range(len(lst)):
114		# unset bit 1 (no embedding permitted) if either bit 2 or 3 is set
115		if lst[i] & 0x000C:
116			lst[i] &= ~0x0002
117		# set bit 2 (allow previewing) if bit 3 is set (allow editing)
118		elif lst[i] & 0x0008:
119			lst[i] |= 0x0004
120		# set bits 2 and 3 if everything is allowed
121		elif lst[i] == 0:
122			lst[i] = 0x000C
123
124	fsType = mergeBits(os2FsTypeMergeBitMap)(lst)
125	# unset bits 2 and 3 if bit 1 is set (some font is "no embedding")
126	if fsType & 0x0002:
127		fsType &= ~0x000C
128	return fsType
129
130
131ttLib.getTableClass('OS/2').mergeMap = {
132	'*': first,
133	'tableTag': equal,
134	'version': max,
135	'xAvgCharWidth': avg_int, # Apparently fontTools doesn't recalc this
136	'fsType': mergeOs2FsType, # Will be overwritten
137	'panose': first, # FIXME: should really be the first Latin font
138	'ulUnicodeRange1': bitwise_or,
139	'ulUnicodeRange2': bitwise_or,
140	'ulUnicodeRange3': bitwise_or,
141	'ulUnicodeRange4': bitwise_or,
142	'fsFirstCharIndex': min,
143	'fsLastCharIndex': max,
144	'sTypoAscender': max,
145	'sTypoDescender': min,
146	'sTypoLineGap': max,
147	'usWinAscent': max,
148	'usWinDescent': max,
149	# Version 1
150	'ulCodePageRange1': onlyExisting(bitwise_or),
151	'ulCodePageRange2': onlyExisting(bitwise_or),
152	# Version 2, 3, 4
153	'sxHeight': onlyExisting(max),
154	'sCapHeight': onlyExisting(max),
155	'usDefaultChar': onlyExisting(first),
156	'usBreakChar': onlyExisting(first),
157	'usMaxContext': onlyExisting(max),
158	# version 5
159	'usLowerOpticalPointSize': onlyExisting(min),
160	'usUpperOpticalPointSize': onlyExisting(max),
161}
162
163@add_method(ttLib.getTableClass('OS/2'))
164def merge(self, m, tables):
165	DefaultTable.merge(self, m, tables)
166	if self.version < 2:
167		# bits 8 and 9 are reserved and should be set to zero
168		self.fsType &= ~0x0300
169	if self.version >= 3:
170		# Only one of bits 1, 2, and 3 may be set. We already take
171		# care of bit 1 implications in mergeOs2FsType. So unset
172		# bit 2 if bit 3 is already set.
173		if self.fsType & 0x0008:
174			self.fsType &= ~0x0004
175	return self
176
177ttLib.getTableClass('post').mergeMap = {
178	'*': first,
179	'tableTag': equal,
180	'formatType': max,
181	'isFixedPitch': min,
182	'minMemType42': max,
183	'maxMemType42': lambda lst: 0,
184	'minMemType1': max,
185	'maxMemType1': lambda lst: 0,
186	'mapping': onlyExisting(sumDicts),
187	'extraNames': lambda lst: [],
188}
189
190ttLib.getTableClass('vmtx').mergeMap = ttLib.getTableClass('hmtx').mergeMap = {
191	'tableTag': equal,
192	'metrics': sumDicts,
193}
194
195ttLib.getTableClass('name').mergeMap = {
196	'tableTag': equal,
197	'names': first, # FIXME? Does mixing name records make sense?
198}
199
200ttLib.getTableClass('loca').mergeMap = {
201	'*': recalculate,
202	'tableTag': equal,
203}
204
205ttLib.getTableClass('glyf').mergeMap = {
206	'tableTag': equal,
207	'glyphs': sumDicts,
208	'glyphOrder': sumLists,
209}
210
211@add_method(ttLib.getTableClass('glyf'))
212def merge(self, m, tables):
213	for i,table in enumerate(tables):
214		for g in table.glyphs.values():
215			if i:
216				# Drop hints for all but first font, since
217				# we don't map functions / CVT values.
218				g.removeHinting()
219			# Expand composite glyphs to load their
220			# composite glyph names.
221			if g.isComposite():
222				g.expand(table)
223	return DefaultTable.merge(self, m, tables)
224
225ttLib.getTableClass('prep').mergeMap = lambda self, lst: first(lst)
226ttLib.getTableClass('fpgm').mergeMap = lambda self, lst: first(lst)
227ttLib.getTableClass('cvt ').mergeMap = lambda self, lst: first(lst)
228ttLib.getTableClass('gasp').mergeMap = lambda self, lst: first(lst) # FIXME? Appears irreconcilable
229
230@add_method(ttLib.getTableClass('CFF '))
231def merge(self, m, tables):
232
233	if any(hasattr(table, "FDSelect") for table in tables):
234		raise NotImplementedError(
235			"Merging CID-keyed CFF tables is not supported yet"
236		)
237
238	for table in tables:
239		table.cff.desubroutinize()
240
241	newcff = tables[0]
242	newfont = newcff.cff[0]
243	private = newfont.Private
244	storedNamesStrings = []
245	glyphOrderStrings = []
246	glyphOrder = set(newfont.getGlyphOrder())
247
248	for name in newfont.strings.strings:
249		if name not in glyphOrder:
250			storedNamesStrings.append(name)
251		else:
252			glyphOrderStrings.append(name)
253
254	chrset = list(newfont.charset)
255	newcs = newfont.CharStrings
256	log.debug("FONT 0 CharStrings: %d.", len(newcs))
257
258	for i, table in enumerate(tables[1:], start=1):
259		font = table.cff[0]
260		font.Private = private
261		fontGlyphOrder = set(font.getGlyphOrder())
262		for name in font.strings.strings:
263			if name in fontGlyphOrder:
264				glyphOrderStrings.append(name)
265		cs = font.CharStrings
266		gs = table.cff.GlobalSubrs
267		log.debug("Font %d CharStrings: %d.", i, len(cs))
268		chrset.extend(font.charset)
269		if newcs.charStringsAreIndexed:
270			for i, name in enumerate(cs.charStrings, start=len(newcs)):
271				newcs.charStrings[name] = i
272				newcs.charStringsIndex.items.append(None)
273		for name in cs.charStrings:
274			newcs[name] = cs[name]
275
276	newfont.charset = chrset
277	newfont.numGlyphs = len(chrset)
278	newfont.strings.strings = glyphOrderStrings + storedNamesStrings
279
280	return newcff
281
282@add_method(ttLib.getTableClass('cmap'))
283def merge(self, m, tables):
284
285	# TODO Handle format=14.
286	if not hasattr(m, 'cmap'):
287		computeMegaCmap(m, tables)
288	cmap = m.cmap
289
290	cmapBmpOnly = {uni: gid for uni,gid in cmap.items() if uni <= 0xFFFF}
291	self.tables = []
292	module = ttLib.getTableModule('cmap')
293	if len(cmapBmpOnly) != len(cmap):
294		# format-12 required.
295		cmapTable = module.cmap_classes[12](12)
296		cmapTable.platformID = 3
297		cmapTable.platEncID = 10
298		cmapTable.language = 0
299		cmapTable.cmap = cmap
300		self.tables.append(cmapTable)
301	# always create format-4
302	cmapTable = module.cmap_classes[4](4)
303	cmapTable.platformID = 3
304	cmapTable.platEncID = 1
305	cmapTable.language = 0
306	cmapTable.cmap = cmapBmpOnly
307	# ordered by platform then encoding
308	self.tables.insert(0, cmapTable)
309	self.tableVersion = 0
310	self.numSubTables = len(self.tables)
311	return self
312