from fontbakery.callable import check, condition
from fontbakery.status import FAIL, PASS, WARN
from fontbakery.message import Message
# used to inform get_module_profile whether and how to create a profile
from fontbakery.fonts_profile import profile_factory # noqa:F401 pylint:disable=W0611
profile_imports = ((".", ("shared_conditions",)),)
[docs]class CFFAnalysis:
def __init__(self):
self.glyphs_dotsection = []
self.glyphs_endchar_seac = []
self.glyphs_exceed_max = []
self.glyphs_recursion_errors = []
def _get_subr_bias(count):
if count < 1240:
bias = 107
elif count < 33900:
bias = 1131
else:
bias = 32768
return bias
def _traverse_subr_call_tree(info, program, depth):
global_subrs = info["global_subrs"]
subrs = info["subrs"]
gsubr_bias = info["gsubr_bias"]
subr_bias = info["subr_bias"]
if depth > info["max_depth"]:
info["max_depth"] = depth
# once we exceed the max depth we can stop going deeper
if depth > 10:
return
if (
len(program) >= 5
and program[-1] == "endchar"
and all([isinstance(a, int) for a in program[-5:-1]])
):
info["saw_endchar_seac"] = True
if "ignore" in program: # decompiler expresses 'dotsection' as 'ignore'
info["saw_dotsection"] = True
while program:
x = program.pop()
if x == "callgsubr":
y = int(program.pop()) + gsubr_bias
sub_program = global_subrs[y].program.copy()
_traverse_subr_call_tree(info, sub_program, depth + 1)
elif x == "callsubr":
y = int(program.pop()) + subr_bias
sub_program = subrs[y].program.copy()
_traverse_subr_call_tree(info, sub_program, depth + 1)
def _analyze_cff(analysis, top_dict, private_dict, fd_index=0):
char_strings = top_dict.CharStrings
global_subrs = top_dict.GlobalSubrs
gsubr_bias = _get_subr_bias(len(global_subrs))
if private_dict is not None and hasattr(private_dict, "Subrs"):
subrs = private_dict.Subrs
subr_bias = _get_subr_bias(len(subrs))
else:
subrs = None
subr_bias = None
char_list = char_strings.keys()
for glyph_name in char_list:
t2_char_string, fd_select_index = char_strings.getItemAndSelector(glyph_name)
if fd_select_index is not None and fd_select_index != fd_index:
continue
try:
t2_char_string.decompile()
except RecursionError:
analysis.glyphs_recursion_errors.append(glyph_name)
continue
info = {}
info["subrs"] = subrs
info["global_subrs"] = global_subrs
info["gsubr_bias"] = gsubr_bias
info["subr_bias"] = subr_bias
info["max_depth"] = 0
depth = 0
program = t2_char_string.program.copy()
_traverse_subr_call_tree(info, program, depth)
max_depth = info["max_depth"]
if max_depth > 10:
analysis.glyphs_exceed_max.append(glyph_name)
if info.get("saw_endchar_seac"):
analysis.glyphs_endchar_seac.append(glyph_name)
if info.get("saw_dotsection"):
analysis.glyphs_dotsection.append(glyph_name)
[docs]@condition
def cff_analysis(ttFont):
analysis = CFFAnalysis()
if "CFF " in ttFont:
cff = ttFont["CFF "].cff
for top_dict in cff.topDictIndex:
if hasattr(top_dict, "FDArray"):
for fd_index, font_dict in enumerate(top_dict.FDArray):
if hasattr(font_dict, "Private"):
private_dict = font_dict.Private
else:
private_dict = None
_analyze_cff(analysis, top_dict, private_dict, fd_index)
else:
if hasattr(top_dict, "Private"):
private_dict = top_dict.Private
else:
private_dict = None
_analyze_cff(analysis, top_dict, private_dict)
elif "CFF2" in ttFont:
cff = ttFont["CFF2"].cff
for top_dict in cff.topDictIndex:
for fd_index, font_dict in enumerate(top_dict.FDArray):
if hasattr(font_dict, "Private"):
private_dict = font_dict.Private
else:
private_dict = None
_analyze_cff(analysis, top_dict, private_dict, fd_index)
return analysis
[docs]@check(
id="com.adobe.fonts/check/cff_call_depth",
conditions=["ttFont", "is_cff", "cff_analysis"],
rationale="""
Per "The Type 2 Charstring Format, Technical Note #5177",
the "Subr nesting, stack limit" is 10.
""",
proposal="https://github.com/fonttools/fontbakery/pull/2425",
)
def com_adobe_fonts_check_cff_call_depth(cff_analysis):
"""Is the CFF subr/gsubr call depth > 10?"""
any_failures = False
if cff_analysis.glyphs_exceed_max or cff_analysis.glyphs_recursion_errors:
any_failures = True
for gn in cff_analysis.glyphs_exceed_max:
yield FAIL, Message(
"max-depth",
f"Subroutine call depth exceeded" f' maximum of 10 for glyph "{gn}".',
)
for gn in cff_analysis.glyphs_recursion_errors:
yield FAIL, Message(
"recursion-error", f'Recursion error while decompiling glyph "{gn}".'
)
if not any_failures:
yield PASS, "Maximum call depth not exceeded."
[docs]@check(
id="com.adobe.fonts/check/cff2_call_depth",
conditions=["ttFont", "is_cff2", "cff_analysis"],
rationale="""
Per "The CFF2 CharString Format", the "Subr nesting, stack limit" is 10.
""",
proposal="https://github.com/fonttools/fontbakery/pull/2425",
)
def com_adobe_fonts_check_cff2_call_depth(cff_analysis):
"""Is the CFF2 subr/gsubr call depth > 10?"""
any_failures = False
if cff_analysis.glyphs_exceed_max or cff_analysis.glyphs_recursion_errors:
any_failures = True
for gn in cff_analysis.glyphs_exceed_max:
yield FAIL, Message(
"max-depth",
f"Subroutine call depth exceeded" f' maximum of 10 for glyph "{gn}".',
)
for gn in cff_analysis.glyphs_recursion_errors:
yield FAIL, Message(
"recursion-error", f'Recursion error while decompiling glyph "{gn}".'
)
if not any_failures:
yield PASS, "Maximum call depth not exceeded."
[docs]@check(
id="com.adobe.fonts/check/cff_deprecated_operators",
conditions=["ttFont", "is_cff", "cff_analysis"],
rationale="""
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 usage as a FAIL.
There are no known ill effects of using dotsection, so that check is a WARN.
""",
proposal="https://github.com/fonttools/fontbakery/pull/3033",
)
def com_adobe_fonts_check_cff_deprecated_operators(cff_analysis):
"""Does the font use deprecated CFF operators or operations?"""
any_failures = False
if cff_analysis.glyphs_dotsection or cff_analysis.glyphs_endchar_seac:
any_failures = True
for gn in cff_analysis.glyphs_dotsection:
yield WARN, Message(
"deprecated-operator-dotsection",
f'Glyph "{gn}" uses deprecated "dotsection" operator.',
)
for gn in cff_analysis.glyphs_endchar_seac:
yield FAIL, Message(
"deprecated-operation-endchar-seac",
f'Glyph "{gn}" has deprecated use of "endchar"'
f" operator to build accented characters (seac).",
)
if not any_failures:
yield PASS, "No deprecated CFF operators used."