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