Skip to content

Commit

Permalink
new check: com.adobe.fonts/check/cff_deprecated_operators
Browse files Browse the repository at this point in the history
on the adobefonts profile
(PR #3033)
  • Loading branch information
josh-hadley authored Sep 10, 2020
1 parent 8b1ff94 commit 214506e
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 55 deletions.
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
Below are the most important changes from each release.
A more detailed list of changes is available in the corresponding milestones for each release in the Github issue tracker (https://github.com/googlefonts/fontbakery/milestones?state=closed).

## 0.7.31 (2020-Sep-??)
### Note-worthy code changes
- Renamed `multiprocessing.py` to `multiproc.py` to avoid conflict with Python
stdlib module of the same name in some configurations.

## 0.7.30 (2020-Aug-??)
## 0.7.30 (2020-Sept-someday)
### Note-worthy code changes
- Adopted 4-spaces indentation. We're changing our codestyle to facilitate collaboration from people who also work with the fontTools and AFDKO codebases. (issue 2997)
- All rationale text needs to have 8 indentation spaces (because this indentation on the source should not show up on the user-interface when rationale text is printed on the text terminal)
- Remove PriorityLevel class as it makes classifying checks by priority more complicated then necessary! (issue #2981)
- Use the http://fonts.google.com/metadata/fonts endpoint to determine if a font is listed in Google Fonts. (issue #2991)
- Renamed `multiprocessing.py` to `multiproc.py` to avoid conflict with Python
stdlib module of the same name in some configurations.
- Re-worked `cff.py` checks using `@condition` to avoid repeated iterations
over the glyph set.
- Added checks for deprecated CFF operator `dotsection` and deprecated use of
`endchar` operator to build accented characters (`seac`).

### New Checks
- **[com.google.fonts/check/varfont_duplicate_instance_names]**: Avoid duplicate instance names in variable fonts (issue #2986)
Expand Down
164 changes: 116 additions & 48 deletions Lib/fontbakery/profiles/cff.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from fontbakery.callable import check
from fontbakery.checkrunner import FAIL, PASS
from fontbakery.callable import check, condition
from fontbakery.checkrunner import FAIL, PASS, WARN
from fontbakery.message import Message

# used to inform get_module_profile whether and how to create a profile
Expand All @@ -9,6 +9,13 @@
('.shared_conditions', ('is_cff', 'is_cff2'))
]

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
Expand All @@ -32,6 +39,11 @@ def _traverse_subr_call_tree(info, program, depth):
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':
Expand All @@ -44,9 +56,8 @@ def _traverse_subr_call_tree(info, program, depth):
_traverse_subr_call_tree(info, sub_program, depth + 1)


def _check_call_depth(top_dict, private_dict, fd_index=0):
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))

Expand All @@ -58,7 +69,7 @@ def _check_call_depth(top_dict, private_dict, fd_index=0):
subr_bias = None

char_list = char_strings.keys()
failed = False

for glyph_name in char_list:
t2_char_string, fd_select_index = char_strings.getItemAndSelector(
glyph_name)
Expand All @@ -67,11 +78,7 @@ def _check_call_depth(top_dict, private_dict, fd_index=0):
try:
t2_char_string.decompile()
except RecursionError:
yield FAIL,\
Message("recursion-error",
f'Recursion error while decompiling'
f' glyph "{glyph_name}".')
failed = True
analysis.glyphs_recursion_errors.append(glyph_name)
continue
info = dict()
info['subrs'] = subrs
Expand All @@ -83,70 +90,131 @@ def _check_call_depth(top_dict, private_dict, fd_index=0):
program = t2_char_string.program.copy()
_traverse_subr_call_tree(info, program, depth)
max_depth = info['max_depth']

if max_depth > 10:
yield FAIL,\
Message("max-depth",
f'Subroutine call depth exceeded'
f' maximum of 10 for glyph "{glyph_name}".')
failed = True
return failed
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)

@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

@check(
id = 'com.adobe.fonts/check/cff_call_depth',
conditions = ['is_cff'],
conditions = ['ttFont', 'is_cff', 'cff_analysis'],
rationale = """
Per "The Type 2 Charstring Format, Technical Note #5177", the "Subr nesting, stack limit" is 10.
"""
)
def com_adobe_fonts_check_cff_call_depth(ttFont):
def com_adobe_fonts_check_cff_call_depth(cff_analysis):
"""Is the CFF subr/gsubr call depth > 10?"""

any_failures = False
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
failed = yield from \
_check_call_depth(top_dict, private_dict, fd_index)
any_failures = any_failures or failed
else:
if hasattr(top_dict, 'Private'):
private_dict = top_dict.Private
else:
private_dict = None
failed = yield from _check_call_depth(top_dict, private_dict)
any_failures = any_failures or failed
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 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.'


@check(
id = 'com.adobe.fonts/check/cff2_call_depth',
conditions = ['is_cff2'],
conditions = ['ttFont', 'is_cff2', 'cff_analysis'],
rationale = """
Per "The CFF2 CharString Format", the "Subr nesting, stack limit" is 10.
"""
)
def com_adobe_fonts_check_cff2_call_depth(ttFont):
def com_adobe_fonts_check_cff2_call_depth(cff_analysis):
"""Is the CFF2 subr/gsubr call depth > 10?"""

any_failures = False
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
failed = yield from \
_check_call_depth(top_dict, private_dict, fd_index)
any_failures = any_failures or failed
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 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.'


@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 useage as a FAIL. There are no known ill effects of using
dotsection, so that check is a WARN.
"""
)
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" operator to build accented characters (seac).')

if not any_failures:
yield PASS, 'No deprecated CFF operators used.'
1 change: 1 addition & 0 deletions Lib/fontbakery/profiles/opentype.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
'com.google.fonts/check/loca/maxp_num_glyphs',
'com.adobe.fonts/check/cff2_call_depth',
'com.adobe.fonts/check/cff_call_depth',
'com.adobe.fonts/check/cff_deprecated_operators',
'com.google.fonts/check/font_version',
'com.google.fonts/check/post_table_version',
'com.google.fonts/check/monospace',
Expand Down
Binary file not shown.
Binary file not shown.
19 changes: 18 additions & 1 deletion tests/profiles/cff_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_check_cff_call_depth():
# in this font, glyphs D & E exceed max call depth,
# and glyph F calls a subroutine that calls itself
font = TEST_FILE('subr_test_fonts/subr_test_font_infinite_recursion.otf')

assert_results_contain(check(font),
FAIL, 'max-depth',
'- Subroutine call depth exceeded'
Expand Down Expand Up @@ -62,3 +62,20 @@ def test_check_cff2_call_depth():
assert_results_contain(check(font),
FAIL, 'recursion-error',
'Recursion error while decompiling glyph "F".')


def test_check_cff_deprecated_operators():
check = TestingContext(cff_profile,
"com.adobe.fonts/check/cff_deprecated_operators")

# this font uses the deprecated 'dotsection' operator
font = TEST_FILE('deprecated_operators/cff1_dotsection.otf')
assert_results_contain(check(font),
WARN, 'deprecated-operator-dotsection',
'Glyph "i" uses deprecated "dotsection" operator.')

# this font uses the 'endchar' operator in a manner that is deprecated ("seac")
font = TEST_FILE('deprecated_operators/cff1_endchar_seac.otf')
assert_results_contain(check(font),
FAIL, 'deprecated-operation-endchar-seac',
'Glyph "Agrave" has deprecated use of "endchar" operator to build accented characters (seac).')

0 comments on commit 214506e

Please sign in to comment.