• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from fontTools.misc import psCharStrings
2from fontTools import ttLib
3from fontTools.pens.basePen import NullPen
4from fontTools.misc.fixedTools import otRound
5from fontTools.varLib.varStore import VarStoreInstancer
6
7def _add_method(*clazzes):
8	"""Returns a decorator function that adds a new method to one or
9	more classes."""
10	def wrapper(method):
11		done = []
12		for clazz in clazzes:
13			if clazz in done: continue # Support multiple names of a clazz
14			done.append(clazz)
15			assert clazz.__name__ != 'DefaultTable', \
16					'Oops, table class not found.'
17			assert not hasattr(clazz, method.__name__), \
18					"Oops, class '%s' has method '%s'." % (clazz.__name__,
19									       method.__name__)
20			setattr(clazz, method.__name__, method)
21		return None
22	return wrapper
23
24def _uniq_sort(l):
25	return sorted(set(l))
26
27class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler):
28
29	def __init__(self, components, localSubrs, globalSubrs):
30		psCharStrings.SimpleT2Decompiler.__init__(self,
31							  localSubrs,
32							  globalSubrs)
33		self.components = components
34
35	def op_endchar(self, index):
36		args = self.popall()
37		if len(args) >= 4:
38			from fontTools.encodings.StandardEncoding import StandardEncoding
39			# endchar can do seac accent bulding; The T2 spec says it's deprecated,
40			# but recent software that shall remain nameless does output it.
41			adx, ady, bchar, achar = args[-4:]
42			baseGlyph = StandardEncoding[bchar]
43			accentGlyph = StandardEncoding[achar]
44			self.components.add(baseGlyph)
45			self.components.add(accentGlyph)
46
47@_add_method(ttLib.getTableClass('CFF '))
48def closure_glyphs(self, s):
49	cff = self.cff
50	assert len(cff) == 1
51	font = cff[cff.keys()[0]]
52	glyphSet = font.CharStrings
53
54	decompose = s.glyphs
55	while decompose:
56		components = set()
57		for g in decompose:
58			if g not in glyphSet:
59				continue
60			gl = glyphSet[g]
61
62			subrs = getattr(gl.private, "Subrs", [])
63			decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs)
64			decompiler.execute(gl)
65		components -= s.glyphs
66		s.glyphs.update(components)
67		decompose = components
68
69def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False):
70	c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName)
71	if isCFF2 or ignoreWidth:
72		# CFF2 charstrings have no widths nor 'endchar' operators
73		c.decompile()
74		c.program = [] if isCFF2 else ['endchar']
75	else:
76		if hasattr(font, 'FDArray') and font.FDArray is not None:
77			private = font.FDArray[fdSelectIndex].Private
78		else:
79			private = font.Private
80		dfltWdX = private.defaultWidthX
81		nmnlWdX = private.nominalWidthX
82		pen = NullPen()
83		c.draw(pen)  # this will set the charstring's width
84		if c.width != dfltWdX:
85			c.program = [c.width - nmnlWdX, 'endchar']
86		else:
87			c.program = ['endchar']
88
89@_add_method(ttLib.getTableClass('CFF '))
90def prune_pre_subset(self, font, options):
91	cff = self.cff
92	# CFF table must have one font only
93	cff.fontNames = cff.fontNames[:1]
94
95	if options.notdef_glyph and not options.notdef_outline:
96		isCFF2 = cff.major > 1
97		for fontname in cff.keys():
98			font = cff[fontname]
99			_empty_charstring(font, ".notdef", isCFF2=isCFF2)
100
101	# Clear useless Encoding
102	for fontname in cff.keys():
103		font = cff[fontname]
104		# https://github.com/fonttools/fonttools/issues/620
105		font.Encoding = "StandardEncoding"
106
107	return True # bool(cff.fontNames)
108
109@_add_method(ttLib.getTableClass('CFF '))
110def subset_glyphs(self, s):
111	cff = self.cff
112	for fontname in cff.keys():
113		font = cff[fontname]
114		cs = font.CharStrings
115
116		if s.options.retain_gids:
117			isCFF2 = cff.major > 1
118			for g in s.glyphs_emptied:
119				_empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True)
120		else:
121			# Load all glyphs
122			for g in font.charset:
123				if g not in s.glyphs: continue
124				c, _ = cs.getItemAndSelector(g)
125
126			if cs.charStringsAreIndexed:
127				indices = [i for i,g in enumerate(font.charset) if g in s.glyphs]
128				csi = cs.charStringsIndex
129				csi.items = [csi.items[i] for i in indices]
130				del csi.file, csi.offsets
131				if hasattr(font, "FDSelect"):
132					sel = font.FDSelect
133					# XXX We want to set sel.format to None, such that the
134					# most compact format is selected. However, OTS was
135					# broken and couldn't parse a FDSelect format 0 that
136					# happened before CharStrings. As such, always force
137					# format 3 until we fix cffLib to always generate
138					# FDSelect after CharStrings.
139					# https://github.com/khaledhosny/ots/pull/31
140					#sel.format = None
141					sel.format = 3
142					sel.gidArray = [sel.gidArray[i] for i in indices]
143				cs.charStrings = {g:indices.index(v)
144						  for g,v in cs.charStrings.items()
145						  if g in s.glyphs}
146			else:
147				cs.charStrings = {g:v
148						  for g,v in cs.charStrings.items()
149						  if g in s.glyphs}
150			font.charset = [g for g in font.charset if g in s.glyphs]
151			font.numGlyphs = len(font.charset)
152
153	return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
154
155@_add_method(psCharStrings.T2CharString)
156def subset_subroutines(self, subrs, gsubrs):
157	p = self.program
158	for i in range(1, len(p)):
159		if p[i] == 'callsubr':
160			assert isinstance(p[i-1], int)
161			p[i-1] = subrs._used.index(p[i-1] + subrs._old_bias) - subrs._new_bias
162		elif p[i] == 'callgsubr':
163			assert isinstance(p[i-1], int)
164			p[i-1] = gsubrs._used.index(p[i-1] + gsubrs._old_bias) - gsubrs._new_bias
165
166@_add_method(psCharStrings.T2CharString)
167def drop_hints(self):
168	hints = self._hints
169
170	if hints.deletions:
171		p = self.program
172		for idx in reversed(hints.deletions):
173			del p[idx-2:idx]
174
175	if hints.has_hint:
176		assert not hints.deletions or hints.last_hint <= hints.deletions[0]
177		self.program = self.program[hints.last_hint:]
178		if not self.program:
179			# TODO CFF2 no need for endchar.
180			self.program.append('endchar')
181		if hasattr(self, 'width'):
182			# Insert width back if needed
183			if self.width != self.private.defaultWidthX:
184				# For CFF2 charstrings, this should never happen
185				assert self.private.defaultWidthX is not None, "CFF2 CharStrings must not have an initial width value"
186				self.program.insert(0, self.width - self.private.nominalWidthX)
187
188	if hints.has_hintmask:
189		i = 0
190		p = self.program
191		while i < len(p):
192			if p[i] in ['hintmask', 'cntrmask']:
193				assert i + 1 <= len(p)
194				del p[i:i+2]
195				continue
196			i += 1
197
198	assert len(self.program)
199
200	del self._hints
201
202class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
203
204	def __init__(self, localSubrs, globalSubrs, private):
205		psCharStrings.SimpleT2Decompiler.__init__(self,
206							  localSubrs,
207							  globalSubrs,
208							  private)
209		for subrs in [localSubrs, globalSubrs]:
210			if subrs and not hasattr(subrs, "_used"):
211				subrs._used = set()
212
213	def op_callsubr(self, index):
214		self.localSubrs._used.add(self.operandStack[-1]+self.localBias)
215		psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
216
217	def op_callgsubr(self, index):
218		self.globalSubrs._used.add(self.operandStack[-1]+self.globalBias)
219		psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
220
221class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor):
222
223	class Hints(object):
224		def __init__(self):
225			# Whether calling this charstring produces any hint stems
226			# Note that if a charstring starts with hintmask, it will
227			# have has_hint set to True, because it *might* produce an
228			# implicit vstem if called under certain conditions.
229			self.has_hint = False
230			# Index to start at to drop all hints
231			self.last_hint = 0
232			# Index up to which we know more hints are possible.
233			# Only relevant if status is 0 or 1.
234			self.last_checked = 0
235			# The status means:
236			# 0: after dropping hints, this charstring is empty
237			# 1: after dropping hints, there may be more hints
238			#	continuing after this, or there might be
239			#	other things.  Not clear yet.
240			# 2: no more hints possible after this charstring
241			self.status = 0
242			# Has hintmask instructions; not recursive
243			self.has_hintmask = False
244			# List of indices of calls to empty subroutines to remove.
245			self.deletions = []
246		pass
247
248	def __init__(self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None):
249		self._css = css
250		psCharStrings.T2WidthExtractor.__init__(
251			self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX)
252		self.private = private
253
254	def execute(self, charString):
255		old_hints = charString._hints if hasattr(charString, '_hints') else None
256		charString._hints = self.Hints()
257
258		psCharStrings.T2WidthExtractor.execute(self, charString)
259
260		hints = charString._hints
261
262		if hints.has_hint or hints.has_hintmask:
263			self._css.add(charString)
264
265		if hints.status != 2:
266			# Check from last_check, make sure we didn't have any operators.
267			for i in range(hints.last_checked, len(charString.program) - 1):
268				if isinstance(charString.program[i], str):
269					hints.status = 2
270					break
271				else:
272					hints.status = 1 # There's *something* here
273			hints.last_checked = len(charString.program)
274
275		if old_hints:
276			assert hints.__dict__ == old_hints.__dict__
277
278	def op_callsubr(self, index):
279		subr = self.localSubrs[self.operandStack[-1]+self.localBias]
280		psCharStrings.T2WidthExtractor.op_callsubr(self, index)
281		self.processSubr(index, subr)
282
283	def op_callgsubr(self, index):
284		subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
285		psCharStrings.T2WidthExtractor.op_callgsubr(self, index)
286		self.processSubr(index, subr)
287
288	def op_hstem(self, index):
289		psCharStrings.T2WidthExtractor.op_hstem(self, index)
290		self.processHint(index)
291	def op_vstem(self, index):
292		psCharStrings.T2WidthExtractor.op_vstem(self, index)
293		self.processHint(index)
294	def op_hstemhm(self, index):
295		psCharStrings.T2WidthExtractor.op_hstemhm(self, index)
296		self.processHint(index)
297	def op_vstemhm(self, index):
298		psCharStrings.T2WidthExtractor.op_vstemhm(self, index)
299		self.processHint(index)
300	def op_hintmask(self, index):
301		rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index)
302		self.processHintmask(index)
303		return rv
304	def op_cntrmask(self, index):
305		rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index)
306		self.processHintmask(index)
307		return rv
308
309	def processHintmask(self, index):
310		cs = self.callingStack[-1]
311		hints = cs._hints
312		hints.has_hintmask = True
313		if hints.status != 2:
314			# Check from last_check, see if we may be an implicit vstem
315			for i in range(hints.last_checked, index - 1):
316				if isinstance(cs.program[i], str):
317					hints.status = 2
318					break
319			else:
320				# We are an implicit vstem
321				hints.has_hint = True
322				hints.last_hint = index + 1
323				hints.status = 0
324		hints.last_checked = index + 1
325
326	def processHint(self, index):
327		cs = self.callingStack[-1]
328		hints = cs._hints
329		hints.has_hint = True
330		hints.last_hint = index
331		hints.last_checked = index
332
333	def processSubr(self, index, subr):
334		cs = self.callingStack[-1]
335		hints = cs._hints
336		subr_hints = subr._hints
337
338		# Check from last_check, make sure we didn't have
339		# any operators.
340		if hints.status != 2:
341			for i in range(hints.last_checked, index - 1):
342				if isinstance(cs.program[i], str):
343					hints.status = 2
344					break
345			hints.last_checked = index
346
347		if hints.status != 2:
348			if subr_hints.has_hint:
349				hints.has_hint = True
350
351				# Decide where to chop off from
352				if subr_hints.status == 0:
353					hints.last_hint = index
354				else:
355					hints.last_hint = index - 2  # Leave the subr call in
356
357		elif subr_hints.status == 0:
358			hints.deletions.append(index)
359
360		hints.status = max(hints.status, subr_hints.status)
361
362class StopHintCountEvent(Exception):
363	pass
364
365
366
367
368class _DesubroutinizingT2Decompiler(psCharStrings.SimpleT2Decompiler):
369	stop_hintcount_ops = ("op_hstem", "op_vstem", "op_rmoveto", "op_hmoveto",
370							"op_vmoveto")
371
372	def __init__(self, localSubrs, globalSubrs, private=None):
373		psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs,
374												private)
375
376	def execute(self, charString):
377		self.need_hintcount = True  # until proven otherwise
378		for op_name in self.stop_hintcount_ops:
379			setattr(self, op_name, self.stop_hint_count)
380
381		if hasattr(charString, '_desubroutinized'):
382			if self.need_hintcount and self.callingStack:
383				try:
384					psCharStrings.SimpleT2Decompiler.execute(self, charString)
385				except StopHintCountEvent:
386					del self.callingStack[-1]
387			return
388
389		charString._patches = []
390		psCharStrings.SimpleT2Decompiler.execute(self, charString)
391		desubroutinized = charString.program[:]
392		for idx, expansion in reversed(charString._patches):
393			assert idx >= 2
394			assert desubroutinized[idx - 1] in ['callsubr', 'callgsubr'], desubroutinized[idx - 1]
395			assert type(desubroutinized[idx - 2]) == int
396			if expansion[-1] == 'return':
397				expansion = expansion[:-1]
398			desubroutinized[idx-2:idx] = expansion
399		if not self.private.in_cff2:
400			if 'endchar' in desubroutinized:
401				# Cut off after first endchar
402				desubroutinized = desubroutinized[:desubroutinized.index('endchar') + 1]
403			else:
404				if not len(desubroutinized) or desubroutinized[-1] != 'return':
405					desubroutinized.append('return')
406
407		charString._desubroutinized = desubroutinized
408		del charString._patches
409
410	def op_callsubr(self, index):
411		subr = self.localSubrs[self.operandStack[-1]+self.localBias]
412		psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
413		self.processSubr(index, subr)
414
415	def op_callgsubr(self, index):
416		subr = self.globalSubrs[self.operandStack[-1]+self.globalBias]
417		psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
418		self.processSubr(index, subr)
419
420	def stop_hint_count(self, *args):
421		self.need_hintcount = False
422		for op_name in self.stop_hintcount_ops:
423			setattr(self, op_name, None)
424		cs = self.callingStack[-1]
425		if hasattr(cs, '_desubroutinized'):
426			raise StopHintCountEvent()
427
428	def op_hintmask(self, index):
429		psCharStrings.SimpleT2Decompiler.op_hintmask(self, index)
430		if self.need_hintcount:
431			self.stop_hint_count()
432
433	def processSubr(self, index, subr):
434		cs = self.callingStack[-1]
435		if not hasattr(cs, '_desubroutinized'):
436			cs._patches.append((index, subr._desubroutinized))
437
438
439@_add_method(ttLib.getTableClass('CFF '))
440def prune_post_subset(self, ttfFont, options):
441	cff = self.cff
442	for fontname in cff.keys():
443		font = cff[fontname]
444		cs = font.CharStrings
445
446		# Drop unused FontDictionaries
447		if hasattr(font, "FDSelect"):
448			sel = font.FDSelect
449			indices = _uniq_sort(sel.gidArray)
450			sel.gidArray = [indices.index (ss) for ss in sel.gidArray]
451			arr = font.FDArray
452			arr.items = [arr[i] for i in indices]
453			del arr.file, arr.offsets
454
455	# Desubroutinize if asked for
456	if options.desubroutinize:
457		self.desubroutinize()
458
459	# Drop hints if not needed
460	if not options.hinting:
461		self.remove_hints()
462	elif not options.desubroutinize:
463		self.remove_unused_subroutines()
464	return True
465
466
467def _delete_empty_subrs(private_dict):
468	if hasattr(private_dict, 'Subrs') and not private_dict.Subrs:
469		if 'Subrs' in private_dict.rawDict:
470			del private_dict.rawDict['Subrs']
471		del private_dict.Subrs
472
473@_add_method(ttLib.getTableClass('CFF '))
474def desubroutinize(self):
475	cff = self.cff
476	for fontname in cff.keys():
477		font = cff[fontname]
478		cs = font.CharStrings
479		for g in font.charset:
480			c, _ = cs.getItemAndSelector(g)
481			c.decompile()
482			subrs = getattr(c.private, "Subrs", [])
483			decompiler = _DesubroutinizingT2Decompiler(subrs, c.globalSubrs, c.private)
484			decompiler.execute(c)
485			c.program = c._desubroutinized
486			del c._desubroutinized
487		# Delete all the local subrs
488		if hasattr(font, 'FDArray'):
489			for fd in font.FDArray:
490				pd = fd.Private
491				if hasattr(pd, 'Subrs'):
492					del pd.Subrs
493				if 'Subrs' in pd.rawDict:
494					del pd.rawDict['Subrs']
495		else:
496			pd = font.Private
497			if hasattr(pd, 'Subrs'):
498				del pd.Subrs
499			if 'Subrs' in pd.rawDict:
500				del pd.rawDict['Subrs']
501	# as well as the global subrs
502	cff.GlobalSubrs.clear()
503
504
505@_add_method(ttLib.getTableClass('CFF '))
506def remove_hints(self):
507	cff = self.cff
508	for fontname in cff.keys():
509		font = cff[fontname]
510		cs = font.CharStrings
511		# This can be tricky, but doesn't have to. What we do is:
512		#
513		# - Run all used glyph charstrings and recurse into subroutines,
514		# - For each charstring (including subroutines), if it has any
515		#   of the hint stem operators, we mark it as such.
516		#   Upon returning, for each charstring we note all the
517		#   subroutine calls it makes that (recursively) contain a stem,
518		# - Dropping hinting then consists of the following two ops:
519		#   * Drop the piece of the program in each charstring before the
520		#     last call to a stem op or a stem-calling subroutine,
521		#   * Drop all hintmask operations.
522		# - It's trickier... A hintmask right after hints and a few numbers
523		#    will act as an implicit vstemhm. As such, we track whether
524		#    we have seen any non-hint operators so far and do the right
525		#    thing, recursively... Good luck understanding that :(
526		css = set()
527		for g in font.charset:
528			c, _ = cs.getItemAndSelector(g)
529			c.decompile()
530			subrs = getattr(c.private, "Subrs", [])
531			decompiler = _DehintingT2Decompiler(css, subrs, c.globalSubrs,
532								c.private.nominalWidthX,
533								c.private.defaultWidthX,
534								c.private)
535			decompiler.execute(c)
536			c.width = decompiler.width
537		for charstring in css:
538			charstring.drop_hints()
539		del css
540
541		# Drop font-wide hinting values
542		all_privs = []
543		if hasattr(font, 'FDArray'):
544			all_privs.extend(fd.Private for fd in font.FDArray)
545		else:
546			all_privs.append(font.Private)
547		for priv in all_privs:
548			for k in ['BlueValues', 'OtherBlues',
549				  'FamilyBlues', 'FamilyOtherBlues',
550				  'BlueScale', 'BlueShift', 'BlueFuzz',
551				  'StemSnapH', 'StemSnapV', 'StdHW', 'StdVW',
552				  'ForceBold', 'LanguageGroup', 'ExpansionFactor']:
553				if hasattr(priv, k):
554					setattr(priv, k, None)
555	self.remove_unused_subroutines()
556
557
558@_add_method(ttLib.getTableClass('CFF '))
559def remove_unused_subroutines(self):
560	cff = self.cff
561	for fontname in cff.keys():
562		font = cff[fontname]
563		cs = font.CharStrings
564		# Renumber subroutines to remove unused ones
565
566		# Mark all used subroutines
567		for g in font.charset:
568			c, _ = cs.getItemAndSelector(g)
569			subrs = getattr(c.private, "Subrs", [])
570			decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
571			decompiler.execute(c)
572
573		all_subrs = [font.GlobalSubrs]
574		if hasattr(font, 'FDArray'):
575			all_subrs.extend(fd.Private.Subrs for fd in font.FDArray if hasattr(fd.Private, 'Subrs') and fd.Private.Subrs)
576		elif hasattr(font.Private, 'Subrs') and font.Private.Subrs:
577			all_subrs.append(font.Private.Subrs)
578
579		subrs = set(subrs) # Remove duplicates
580
581		# Prepare
582		for subrs in all_subrs:
583			if not hasattr(subrs, '_used'):
584				subrs._used = set()
585			subrs._used = _uniq_sort(subrs._used)
586			subrs._old_bias = psCharStrings.calcSubrBias(subrs)
587			subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
588
589		# Renumber glyph charstrings
590		for g in font.charset:
591			c, _ = cs.getItemAndSelector(g)
592			subrs = getattr(c.private, "Subrs", [])
593			c.subset_subroutines (subrs, font.GlobalSubrs)
594
595		# Renumber subroutines themselves
596		for subrs in all_subrs:
597			if subrs == font.GlobalSubrs:
598				if not hasattr(font, 'FDArray') and hasattr(font.Private, 'Subrs'):
599					local_subrs = font.Private.Subrs
600				else:
601					local_subrs = []
602			else:
603				local_subrs = subrs
604
605			subrs.items = [subrs.items[i] for i in subrs._used]
606			if hasattr(subrs, 'file'):
607				del subrs.file
608			if hasattr(subrs, 'offsets'):
609				del subrs.offsets
610
611			for subr in subrs.items:
612				subr.subset_subroutines (local_subrs, font.GlobalSubrs)
613
614		# Delete local SubrsIndex if empty
615		if hasattr(font, 'FDArray'):
616			for fd in font.FDArray:
617				_delete_empty_subrs(fd.Private)
618		else:
619			_delete_empty_subrs(font.Private)
620
621		# Cleanup
622		for subrs in all_subrs:
623			del subrs._used, subrs._old_bias, subrs._new_bias
624