From a326e75cc67c2a66de474dedb04bc63eeaa0ed6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felipe=20Corr=C3=AAa=20da=20Silva=20Sanches?= Date: Wed, 19 Jun 2024 17:48:11 -0300 Subject: [PATCH] New check: CFF top dict in ASCII range. Checks if all strings in a font's CFF table top dict fit in the range of ASCII values. EXPERIMENTAL - com.adobe.fonts/check/cff_ascii_strings Added to the Universal profile. (issue #4619) --- CHANGELOG.md | 3 ++ Lib/fontbakery/checks/opentype/cff.py | 47 +++++++++++++++++- Lib/fontbakery/profiles/adobefonts.py | 1 + Lib/fontbakery/profiles/opentype.py | 3 +- .../unicode-decode-err-cff.otf | Bin 0 -> 1780 bytes tests/checks/opentype/cff_test.py | 40 +++++++++++++++ 6 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 data/test/unicode-decode-err/unicode-decode-err-cff.otf diff --git a/CHANGELOG.md b/CHANGELOG.md index 57a241233f..5d73bfeb07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ A more detailed list of changes is available in the corresponding milestones for ## Upcoming release: 0.12.8 (2024-Jun-??) ### New checks +#### Added to the OpenType profile + - **EXPERIMENTAL - [com.adobe.fonts/check/cff_ascii_strings]:** Checks if all strings in a font's CFF table top dict fit in the range of ASCII values (issue #4619) + #### Added to the Universal profile - **EXPERIMENTAL - [com.google.fonts/check/gsub/smallcaps_before_ligatures]:** Ensure 'smcp' (small caps) lookups are defined before ligature lookups in the 'GSUB' table (issue #3020) diff --git a/Lib/fontbakery/checks/opentype/cff.py b/Lib/fontbakery/checks/opentype/cff.py index dc7cf05503..2971974cd8 100644 --- a/Lib/fontbakery/checks/opentype/cff.py +++ b/Lib/fontbakery/checks/opentype/cff.py @@ -10,6 +10,7 @@ def __init__(self): self.glyphs_endchar_seac = [] self.glyphs_exceed_max = [] self.glyphs_recursion_errors = [] + self.string_not_ascii = [] def _get_subr_bias(count): @@ -61,6 +62,14 @@ def _analyze_cff(analysis, top_dict, private_dict, fd_index=0): global_subrs = top_dict.GlobalSubrs gsubr_bias = _get_subr_bias(len(global_subrs)) + if hasattr(top_dict, "rawDict"): + raw_dict = top_dict.rawDict + for key in ["Notice", "Copyright", "FontName", "FullName", "FamilyName"]: + for char in raw_dict.get(key, ""): + if ord(char) > 0x7F: + analysis.string_not_ascii.append((key, raw_dict[key])) + break + if private_dict is not None and hasattr(private_dict, "Subrs"): subrs = private_dict.Subrs subr_bias = _get_subr_bias(len(subrs)) @@ -109,7 +118,11 @@ def cff_analysis(font): ttFont = TTFont(font.file) # Use our own copy here since we are decompiling if "CFF " in ttFont: - cff = ttFont["CFF "].cff + try: + cff = ttFont["CFF "].cff + except UnicodeDecodeError: + analysis.string_not_ascii = None + return analysis for top_dict in cff.topDictIndex: if hasattr(top_dict, "FDArray"): @@ -218,3 +231,35 @@ def com_adobe_fonts_check_cff_deprecated_operators(cff_analysis): f'Glyph "{gn}" has deprecated use of "endchar"' f" operator to build accented characters (seac).", ) + + +@check( + id="com.adobe.fonts/check/cff_ascii_strings", + conditions=["ttFont", "is_cff", "cff_analysis"], + rationale=""" + All CFF Table top dict string chars should fit into the ASCII range. + """, + proposal="https://github.com/fonttools/fontbakery/issues/4619", + experimental="Since 2024/Jun/20", +) +def com_adobe_fonts_check_cff_ascii_strings(cff_analysis): + """Does the font's CFF table top dict strings fit into the ASCII range?""" + + if cff_analysis.string_not_ascii is None: + yield FAIL, Message( + "cff-unable-to-decode", + "Unable to decode CFF table, possibly due to out" + " of ASCII range strings. Please check table strings.", + ) + elif cff_analysis.string_not_ascii: + detailed_info = "" + for key, string in cff_analysis.string_not_ascii: + detailed_info += ( + f"\n\n\t - {key}: {string.encode('latin-1').decode('utf-8')}" + ) + + yield FAIL, Message( + "cff-string-not-in-ascii-range", + f"The following CFF TopDict strings" + f" are not in the ASCII range: {detailed_info}", + ) diff --git a/Lib/fontbakery/profiles/adobefonts.py b/Lib/fontbakery/profiles/adobefonts.py index 37e5d1316c..d2cd469159 100644 --- a/Lib/fontbakery/profiles/adobefonts.py +++ b/Lib/fontbakery/profiles/adobefonts.py @@ -15,6 +15,7 @@ "com.adobe.fonts/check/cff2_call_depth", "com.adobe.fonts/check/cff_call_depth", "com.adobe.fonts/check/cff_deprecated_operators", + "com.adobe.fonts/check/cff_ascii_strings", ], "fontwerk": [ "com.fontwerk/check/inconsistencies_between_fvar_stat", # IS_OVERRIDDEN diff --git a/Lib/fontbakery/profiles/opentype.py b/Lib/fontbakery/profiles/opentype.py index d793d1ddd4..1eac3326eb 100644 --- a/Lib/fontbakery/profiles/opentype.py +++ b/Lib/fontbakery/profiles/opentype.py @@ -23,9 +23,10 @@ "com.google.fonts/check/varfont/wdth_valid_range", "com.google.fonts/check/varfont/stat_axis_record_for_each_axis", "com.google.fonts/check/loca/maxp_num_glyphs", - "com.adobe.fonts/check/cff2_call_depth", + "com.adobe.fonts/check/cff_ascii_strings", "com.adobe.fonts/check/cff_call_depth", "com.adobe.fonts/check/cff_deprecated_operators", + "com.adobe.fonts/check/cff2_call_depth", "com.google.fonts/check/font_version", "com.google.fonts/check/post_table_version", "com.google.fonts/check/monospace", diff --git a/data/test/unicode-decode-err/unicode-decode-err-cff.otf b/data/test/unicode-decode-err/unicode-decode-err-cff.otf new file mode 100644 index 0000000000000000000000000000000000000000..9232ef7c2fe0fc6fc718f1e90d143660f0ddf165 GIT binary patch literal 1780 zcma)6ZD?Cn7=G_Z(zK0nZ7s~N^sLI(%{6J`HoCUfewjL*OKsZWhPd7)cTH^Xz1hue z)B3|!5Pv8gPNWh1u~FPW9jF!Yk0LS|6QMc~9qdQzkWGKMfjI6Rlg8)V#9F6_=j5FC z`#H~ha_@}|4#udGW=W!^o@kVP-hOj zqgB|i!``4NQyG%`p~@W3t1FuNL{+i_HZa7^86#^I&Qk=TZxLq#$s!4Yupw^zIhxNb zDvbN;ox91oM{w@-lM@u3I;qnyU7WFhxe@i)=c~>+`>V+#2p;vD%hw~JNK@RHFcT_E zXPLq*Q%S0tVvaH6FzZShBPxqeXDwCBvORjjFf)d!SZb1`b!MehHl(My6}R9-xtJJkmkTMJLnvWr;jPGyLK2E4A;Q zI{98*&4~{(C&!-698rTc!=tB;oDK$@4yVV?3k7;|mR7E;NLQVn+E2Z2+HFU#cpmf~ z4SVdNn{9vh1ZMKibNS=>kMp19k74)T)beGw_J<6sPUS!lD2Pc^kF^mIq);qMptilj5@)p zUc6fH0_cKJg`k@wTo(CT$G{B`SX|!WGUZZ61RODWcD>7l6$*tPiq-BeRy;IfzqD}Z z_V()}Nk1WvzF!VHTwZ!0{o88m>P4R+orB5ms5pQoU0A(FZTa9b%<;A#p=de>Wft|)ICD` zU9MU1=Pdb6{)L@_&vHA}(G9~5^H|8e>D@R{1Di3_^Q^;=Q@l49VJEV7(N4_WkpG`q P@kz$RQCI@bzl=WsmPV12 literal 0 HcmV?d00001 diff --git a/tests/checks/opentype/cff_test.py b/tests/checks/opentype/cff_test.py index 045d73e20e..0c6670ad19 100644 --- a/tests/checks/opentype/cff_test.py +++ b/tests/checks/opentype/cff_test.py @@ -1,3 +1,5 @@ +from fontTools.ttLib import TTFont + from fontbakery.codetesting import ( assert_PASS, assert_results_contain, @@ -98,3 +100,41 @@ def test_check_cff_deprecated_operators(): " to build accented characters (seac)." ), ) + + +def test_check_cff_strings(): + check = CheckTester("com.adobe.fonts/check/cff_ascii_strings") + + ttFont = TTFont(TEST_FILE("source-sans-pro/OTF/SourceSansPro-Regular.otf")) + + # check that a healthy CFF font passes: + assert_PASS(check(ttFont)) + + # FIXME: The condition cff_analysis creates a new ttFont object + # and, consequently, discards the modifications we made + # here prior to invoking the check. + # + # # put an out of range char into FullName field: + # rawDict = ttFont["CFF "].cff.topDictIndex[0].rawDict + # rawDict["FullName"] = "S\u00F2urceSansPro-Regular" + # assert_results_contain( + # check(ttFont), + # FAIL, + # "cff-string-not-in-ascii-range", + # ( + # "The following CFF TopDict strings are not in the ASCII range:" + # f"- FullName: {rawDict['FullName']}" + # ), + # ) + + # Out-of-ascii-range char in the FontName field will cause decode issues: + ttFont = TTFont(TEST_FILE("unicode-decode-err/unicode-decode-err-cff.otf")) + assert_results_contain( + check(ttFont), + FAIL, + "cff-unable-to-decode", + ( + "Unable to decode CFF table, possibly due to out " + "of ASCII range strings. Please check table strings." + ), + )