1from fontbakery.callable import check, condition
2from fontbakery.status import FAIL, PASS, WARN
3from fontbakery.message import Message
4
5# used to inform get_module_profile whether and how to create a profile
6from fontbakery.fonts_profile import profile_factory # NOQA pylint: disable=unused-import
7
8profile_imports = [
9    ('.shared_conditions', ('is_cff', 'is_cff2'))
10]
11
12class CFFAnalysis:
13    def __init__(self):
14        self.glyphs_dotsection = []
15        self.glyphs_endchar_seac = []
16        self.glyphs_exceed_max = []
17        self.glyphs_recursion_errors = []
18
19def _get_subr_bias(count):
20    if count < 1240:
21        bias = 107
22    elif count < 33900:
23        bias = 1131
24    else:
25        bias = 32768
26    return bias
27
28
29def _traverse_subr_call_tree(info, program, depth):
30    global_subrs = info['global_subrs']
31    subrs = info['subrs']
32    gsubr_bias = info['gsubr_bias']
33    subr_bias = info['subr_bias']
34
35    if depth > info['max_depth']:
36        info['max_depth'] = depth
37
38    # once we exceed the max depth we can stop going deeper
39    if depth > 10:
40        return
41
42    if len(program) >=5 and program[-1] == 'endchar' and all([isinstance(a, int) for a in program[-5:-1]]):
43        info['saw_endchar_seac'] = True
44    if 'ignore' in program:  # decompiler expresses 'dotsection' as 'ignore'
45        info['saw_dotsection'] = True
46
47    while program:
48        x = program.pop()
49        if x == 'callgsubr':
50            y = int(program.pop()) + gsubr_bias
51            sub_program = global_subrs[y].program.copy()
52            _traverse_subr_call_tree(info, sub_program, depth + 1)
53        elif x == 'callsubr':
54            y = int(program.pop()) + subr_bias
55            sub_program = subrs[y].program.copy()
56            _traverse_subr_call_tree(info, sub_program, depth + 1)
57
58
59def _analyze_cff(analysis, top_dict, private_dict, fd_index=0):
60    char_strings = top_dict.CharStrings
61    global_subrs = top_dict.GlobalSubrs
62    gsubr_bias = _get_subr_bias(len(global_subrs))
63
64    if private_dict is not None and hasattr(private_dict, 'Subrs'):
65        subrs = private_dict.Subrs
66        subr_bias = _get_subr_bias(len(subrs))
67    else:
68        subrs = None
69        subr_bias = None
70
71    char_list = char_strings.keys()
72
73    for glyph_name in char_list:
74        t2_char_string, fd_select_index = char_strings.getItemAndSelector(
75            glyph_name)
76        if fd_select_index is not None and fd_select_index != fd_index:
77            continue
78        try:
79            t2_char_string.decompile()
80        except RecursionError:
81            analysis.glyphs_recursion_errors.append(glyph_name)
82            continue
83        info = dict()
84        info['subrs'] = subrs
85        info['global_subrs'] = global_subrs
86        info['gsubr_bias'] = gsubr_bias
87        info['subr_bias'] = subr_bias
88        info['max_depth'] = 0
89        depth = 0
90        program = t2_char_string.program.copy()
91        _traverse_subr_call_tree(info, program, depth)
92        max_depth = info['max_depth']
93
94        if max_depth > 10:
95            analysis.glyphs_exceed_max.append(glyph_name)
96        if info.get('saw_endchar_seac'):
97            analysis.glyphs_endchar_seac.append(glyph_name)
98        if info.get('saw_dotsection'):
99            analysis.glyphs_dotsection.append(glyph_name)
100
101@condition
102def cff_analysis(ttFont):
103
104    analysis = CFFAnalysis()
105
106    if 'CFF ' in ttFont:
107        cff = ttFont['CFF '].cff
108
109        for top_dict in cff.topDictIndex:
110            if hasattr(top_dict, 'FDArray'):
111                for fd_index, font_dict in enumerate(top_dict.FDArray):
112                    if hasattr(font_dict, 'Private'):
113                        private_dict = font_dict.Private
114                    else:
115                        private_dict = None
116                    _analyze_cff(analysis, top_dict, private_dict, fd_index)
117            else:
118                if hasattr(top_dict, 'Private'):
119                    private_dict = top_dict.Private
120                else:
121                    private_dict = None
122                _analyze_cff(analysis, top_dict, private_dict)
123
124    elif 'CFF2' in ttFont:
125        cff = ttFont['CFF2'].cff
126
127        for top_dict in cff.topDictIndex:
128            for fd_index, font_dict in enumerate(top_dict.FDArray):
129                if hasattr(font_dict, 'Private'):
130                    private_dict = font_dict.Private
131                else:
132                    private_dict = None
133                _analyze_cff(analysis, top_dict, private_dict, fd_index)
134
135    return analysis
136
137@check(
138    id = 'com.adobe.fonts/check/cff_call_depth',
139    conditions = ['ttFont',
140                  'is_cff',
141                  'cff_analysis'],
142    rationale = """
143        Per "The Type 2 Charstring Format, Technical Note #5177", the "Subr nesting, stack limit" is 10.
144    """,
145    proposal = 'https://github.com/googlefonts/fontbakery/pull/2425'
146)
147def com_adobe_fonts_check_cff_call_depth(cff_analysis):
148    """Is the CFF subr/gsubr call depth > 10?"""
149
150    any_failures = False
151
152    if cff_analysis.glyphs_exceed_max or cff_analysis.glyphs_recursion_errors:
153        any_failures = True
154        for gn in cff_analysis.glyphs_exceed_max:
155            yield FAIL, \
156                  Message('max-depth',
157                          f'Subroutine call depth exceeded maximum of 10 for glyph "{gn}".')
158        for gn in cff_analysis.glyphs_recursion_errors:
159            yield FAIL, \
160                  Message('recursion-error',
161                          f'Recursion error while decompiling glyph "{gn}".')
162
163    if not any_failures:
164        yield PASS, 'Maximum call depth not exceeded.'
165
166
167@check(
168    id = 'com.adobe.fonts/check/cff2_call_depth',
169    conditions = ['ttFont', 'is_cff2', 'cff_analysis'],
170    rationale = """
171        Per "The CFF2 CharString Format", the "Subr nesting, stack limit" is 10.
172    """,
173    proposal = 'https://github.com/googlefonts/fontbakery/pull/2425'
174)
175def com_adobe_fonts_check_cff2_call_depth(cff_analysis):
176    """Is the CFF2 subr/gsubr call depth > 10?"""
177
178    any_failures = False
179
180    if cff_analysis.glyphs_exceed_max or cff_analysis.glyphs_recursion_errors:
181        any_failures = True
182        for gn in cff_analysis.glyphs_exceed_max:
183            yield FAIL, \
184                  Message('max-depth',
185                          f'Subroutine call depth exceeded maximum of 10 for glyph "{gn}".')
186        for gn in cff_analysis.glyphs_recursion_errors:
187            yield FAIL, \
188                  Message('recursion-error',
189                          f'Recursion error while decompiling glyph "{gn}".')
190
191    if not any_failures:
192        yield PASS, 'Maximum call depth not exceeded.'
193
194
195@check(
196    id = 'com.adobe.fonts/check/cff_deprecated_operators',
197    conditions = ['ttFont',
198                  'is_cff',
199                  'cff_analysis'],
200    rationale = """
201        The 'dotsection' operator and the use of 'endchar' to build accented characters from the Adobe Standard Encoding Character Set ("seac") are deprecated in CFF. Adobe recommends repairing any fonts that use these, especially endchar-as-seac, because a rendering issue was discovered in Microsoft Word with a font that makes use of this operation. The check treats that useage as a FAIL. There are no known ill effects of using dotsection, so that check is a WARN.
202    """,
203    proposal = 'https://github.com/googlefonts/fontbakery/pull/3033'
204)
205def com_adobe_fonts_check_cff_deprecated_operators(cff_analysis):
206    """Does the font use deprecated CFF operators or operations?"""
207    any_failures = False
208
209    if cff_analysis.glyphs_dotsection or cff_analysis.glyphs_endchar_seac:
210        any_failures = True
211        for gn in cff_analysis.glyphs_dotsection:
212            yield WARN, \
213                  Message('deprecated-operator-dotsection',
214                          f'Glyph "{gn}" uses deprecated "dotsection" operator.')
215        for gn in cff_analysis.glyphs_endchar_seac:
216            yield FAIL, \
217                  Message('deprecated-operation-endchar-seac',
218                          f'Glyph "{gn}" has deprecated use of "endchar" operator to build accented characters (seac).')
219
220    if not any_failures:
221        yield PASS, 'No deprecated CFF operators used.'
222