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