xref: /aosp_15_r20/external/fonttools/Lib/fontTools/subset/cff.py (revision e1fe3e4ad2793916b15cccdc4a7da52a7e1dd0e9)
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
7
8
9class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler):
10    def __init__(self, components, localSubrs, globalSubrs):
11        psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs)
12        self.components = components
13
14    def op_endchar(self, index):
15        args = self.popall()
16        if len(args) >= 4:
17            from fontTools.encodings.StandardEncoding import StandardEncoding
18
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)
26
27
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
34
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]
42
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
49
50
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"]
69
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
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
98
99        glyphs = s.glyphs.union(s.glyphs_emptied)
100
101        # Load all glyphs
102        for g in font.charset:
103            if g not in glyphs:
104                continue
105            c, _ = cs.getItemAndSelector(g)
106
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)
126
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)
131
132    return True  # any(cff[fontname].numGlyphs for fontname in cff.keys())
133
134
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            )
147
148
149@_add_method(psCharStrings.T2CharString)
150def drop_hints(self):
151    hints = self._hints
152
153    if hints.deletions:
154        p = self.program
155        for idx in reversed(hints.deletions):
156            del p[idx - 2 : idx]
157
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)
172
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
182
183    assert len(self.program)
184
185    del self._hints
186
187
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()
196
197    def op_callsubr(self, index):
198        self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
199        psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
200
201    def op_callgsubr(self, index):
202        self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
203        psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
204
205
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 = []
230
231        pass
232
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
241
242    def execute(self, charString):
243        old_hints = charString._hints if hasattr(charString, "_hints") else None
244        charString._hints = self.Hints()
245
246        psCharStrings.T2WidthExtractor.execute(self, charString)
247
248        hints = charString._hints
249
250        if hints.has_hint or hints.has_hintmask:
251            self._css.add(charString)
252
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)
262
263        if old_hints:
264            assert hints.__dict__ == old_hints.__dict__
265
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)
270
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)
275
276    def op_hstem(self, index):
277        psCharStrings.T2WidthExtractor.op_hstem(self, index)
278        self.processHint(index)
279
280    def op_vstem(self, index):
281        psCharStrings.T2WidthExtractor.op_vstem(self, index)
282        self.processHint(index)
283
284    def op_hstemhm(self, index):
285        psCharStrings.T2WidthExtractor.op_hstemhm(self, index)
286        self.processHint(index)
287
288    def op_vstemhm(self, index):
289        psCharStrings.T2WidthExtractor.op_vstemhm(self, index)
290        self.processHint(index)
291
292    def op_hintmask(self, index):
293        rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index)
294        self.processHintmask(index)
295        return rv
296
297    def op_cntrmask(self, index):
298        rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index)
299        self.processHintmask(index)
300        return rv
301
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
318
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
325
326    def processSubr(self, index, subr):
327        cs = self.callingStack[-1]
328        hints = cs._hints
329        subr_hints = subr._hints
330
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
339
340        if hints.status != 2:
341            if subr_hints.has_hint:
342                hints.has_hint = True
343
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
349
350        elif subr_hints.status == 0:
351            hints.deletions.append(index)
352
353        hints.status = max(hints.status, subr_hints.status)
354
355
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
362
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
371
372    # Desubroutinize if asked for
373    if options.desubroutinize:
374        cff.desubroutinize()
375
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
382
383
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
389
390
391@deprecateFunction(
392    "use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning
393)
394@_add_method(ttLib.getTableClass("CFF "))
395def desubroutinize(self):
396    self.cff.desubroutinize()
397
398
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
438
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()
465
466
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
474
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)
481
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)
491
492        subrs = set(subrs)  # Remove duplicates
493
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)
501
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)
507
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
517
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
523
524            for subr in subrs.items:
525                subr.subset_subroutines(local_subrs, font.GlobalSubrs)
526
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)
533
534        # Cleanup
535        for subrs in all_subrs:
536            del subrs._used, subrs._old_bias, subrs._new_bias
537