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