-
Notifications
You must be signed in to change notification settings - Fork 105
/
Copy pathgooglefonts.py
4423 lines (3921 loc) · 169 KB
/
googlefonts.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import os
from fontbakery.profiles.universal import UNIVERSAL_PROFILE_CHECKS
from fontbakery.checkrunner import Section, INFO, WARN, ERROR, SKIP, PASS, FAIL
from fontbakery.callable import check, disable
from fontbakery.message import Message
from fontbakery.fonts_profile import profile_factory
from fontbakery.constants import (PriorityLevel,
NameID,
PlatformID,
WindowsEncodingID,
WindowsLanguageID,
MacintoshEncodingID,
MacintoshLanguageID)
from .googlefonts_conditions import * # pylint: disable=wildcard-import,unused-wildcard-import
profile_imports = ('fontbakery.profiles.universal',)
profile = profile_factory(default_section=Section("Google Fonts"))
METADATA_CHECKS = [
'com.google.fonts/check/metadata/parses',
'com.google.fonts/check/metadata/unknown_designer',
'com.google.fonts/check/metadata/multiple_designers',
'com.google.fonts/check/metadata/designer_values',
'com.google.fonts/check/metadata/listed_on_gfonts',
'com.google.fonts/check/metadata/unique_full_name_values',
'com.google.fonts/check/metadata/unique_weight_style_pairs',
'com.google.fonts/check/metadata/license',
'com.google.fonts/check/metadata/menu_and_latin',
'com.google.fonts/check/metadata/subsets_order',
'com.google.fonts/check/metadata/copyright',
'com.google.fonts/check/metadata/familyname',
'com.google.fonts/check/metadata/has_regular',
'com.google.fonts/check/metadata/regular_is_400',
'com.google.fonts/check/metadata/nameid/family_name',
'com.google.fonts/check/metadata/nameid/post_script_name',
'com.google.fonts/check/metadata/nameid/full_name',
'com.google.fonts/check/metadata/nameid/family_and_full_names', # FIXME! This seems redundant!
'com.google.fonts/check/metadata/nameid/copyright',
'com.google.fonts/check/metadata/nameid/font_name', # FIXME! This looks suspiciously similar to com.google.fonts/check/metadata/nameid/family_name
'com.google.fonts/check/metadata/match_fullname_postscript',
'com.google.fonts/check/metadata/match_filename_postscript',
'com.google.fonts/check/metadata/match_weight_postscript',
'com.google.fonts/check/metadata/valid_name_values',
'com.google.fonts/check/metadata/valid_full_name_values',
'com.google.fonts/check/metadata/valid_filename_values',
'com.google.fonts/check/metadata/valid_post_script_name_values',
'com.google.fonts/check/metadata/valid_copyright',
'com.google.fonts/check/metadata/reserved_font_name',
'com.google.fonts/check/metadata/copyright_max_length',
'com.google.fonts/check/metadata/filenames',
'com.google.fonts/check/metadata/italic_style',
'com.google.fonts/check/metadata/normal_style',
'com.google.fonts/check/metadata/fontname_not_camel_cased',
'com.google.fonts/check/metadata/match_name_familyname',
'com.google.fonts/check/metadata/canonical_weight_value',
'com.google.fonts/check/metadata/os2_weightclass',
'com.google.fonts/check/metadata/canonical_style_names',
'com.google.fonts/check/metadata/broken_links',
'com.google.fonts/check/metadata/undeclared_fonts'
]
DESCRIPTION_CHECKS = [
'com.google.fonts/check/description/broken_links',
'com.google.fonts/check/description/valid_html',
'com.google.fonts/check/description/min_length',
'com.google.fonts/check/description/max_length',
'com.google.fonts/check/description/git_url',
'com.google.fonts/check/description/variable_font'
]
FAMILY_CHECKS = [
# 'com.google.fonts/check/family/equal_numbers_of_glyphs',
# 'com.google.fonts/check/family/equal_glyph_names',
'com.google.fonts/check/family/has_license',
'com.google.fonts/check/family/control_chars',
'com.google.fonts/check/family/tnum_horizontal_metrics',
]
NAME_TABLE_CHECKS = [
'com.google.fonts/check/name/unwanted_chars',
'com.google.fonts/check/name/license',
'com.google.fonts/check/name/license_url',
'com.google.fonts/check/name/family_and_style_max_length',
'com.google.fonts/check/name/line_breaks',
'com.google.fonts/check/name/rfn',
]
REPO_CHECKS = [
'com.google.fonts/check/repo/dirname_matches_nameid_1',
'com.google.fonts/check/repo/vf_has_static_fonts',
'com.google.fonts/check/license/OFL_copyright'
]
FONT_FILE_CHECKS = [
'com.google.fonts/check/glyph_coverage',
'com.google.fonts/check/canonical_filename',
'com.google.fonts/check/usweightclass',
'com.google.fonts/check/fstype',
'com.google.fonts/check/vendor_id',
'com.google.fonts/check/ligature_carets',
'com.google.fonts/check/production_glyphs_similarity',
'com.google.fonts/check/fontv',
#DISABLED: 'com.google.fonts/check/production_encoded_glyphs',
'com.google.fonts/check/varfont/generate_static',
'com.google.fonts/check/kerning_for_non_ligated_sequences',
'com.google.fonts/check/name/description_max_length',
'com.google.fonts/check/fvar_name_entries',
'com.google.fonts/check/version_bump',
'com.google.fonts/check/epar',
'com.google.fonts/check/font_copyright',
'com.google.fonts/check/italic_angle',
'com.google.fonts/check/has_ttfautohint_params',
'com.google.fonts/check/name/version_format',
'com.google.fonts/check/name/familyname_first_char',
'com.google.fonts/check/hinting_impact',
'com.google.fonts/check/varfont/has_HVAR',
'com.google.fonts/check/name/typographicfamilyname',
'com.google.fonts/check/name/subfamilyname',
'com.google.fonts/check/name/typographicsubfamilyname',
'com.google.fonts/check/gasp',
'com.google.fonts/check/name/familyname',
'com.google.fonts/check/name/mandatory_entries',
'com.google.fonts/check/name/copyright_length',
'com.google.fonts/check/fontdata_namecheck',
'com.google.fonts/check/name/ascii_only_entries',
'com.google.fonts/check/varfont_has_instances',
'com.google.fonts/check/varfont_weight_instances',
'com.google.fonts/check/old_ttfautohint',
'com.google.fonts/check/vttclean',
'com.google.fonts/check/name/postscriptname',
'com.google.fonts/check/aat',
'com.google.fonts/check/name/fullfontname',
'com.google.fonts/check/mac_style',
'com.google.fonts/check/fsselection',
'com.google.fonts/check/smart_dropout',
'com.google.fonts/check/integer_ppem_if_hinted',
'com.google.fonts/check/unitsperem_strict',
'com.google.fonts/check/contour_count',
'com.google.fonts/check/vertical_metrics_regressions',
'com.google.fonts/check/varfont_instance_coordinates',
'com.google.fonts/check/varfont_instance_names',
'com.google.fonts/check/varfont/consistent_axes',
'com.google.fonts/check/varfont/unsupported_axes'
]
GOOGLEFONTS_PROFILE_CHECKS = \
UNIVERSAL_PROFILE_CHECKS + \
METADATA_CHECKS + \
DESCRIPTION_CHECKS + \
FAMILY_CHECKS + \
NAME_TABLE_CHECKS + \
REPO_CHECKS + \
FONT_FILE_CHECKS
@check(
id = 'com.google.fonts/check/canonical_filename',
rationale = """
A font's filename must be composed in the following manner:
<familyname>-<stylename>.ttf
- Nunito-Regular.ttf,
- Oswald-BoldItalic.ttf
Variable fonts must list the axis tags in alphabetical order in square brackets and separated by commas:
- Roboto[wdth,wght].ttf
- Familyname-Italic[wght].ttf
""",
misc_metadata = {
'priority': PriorityLevel.CRITICAL
}
)
def com_google_fonts_check_canonical_filename(font):
"""Checking file is named canonically."""
from fontTools.ttLib import TTFont
from .shared_conditions import is_variable_font
from .googlefonts_conditions import canonical_stylename
from fontbakery.utils import suffix
from fontbakery.constants import (STATIC_STYLE_NAMES,
MacStyle)
def variable_font_filename(ttFont):
from fontbakery.utils import get_name_entry_strings
familyname = get_name_entry_strings(ttFont, NameID.FONT_FAMILY_NAME)[0]
typo_familynames = get_name_entry_strings(ttFont, NameID.TYPOGRAPHIC_FAMILY_NAME)
familyname = typo_familynames[0] if typo_familynames else familyname
familyname = "".join(familyname.split(' ')) #remove spaces
if bool(ttFont["head"].macStyle & MacStyle.ITALIC):
familyname+="-Italic"
tags = ttFont["fvar"].axes
tags = list(map(lambda t: t.axisTag, tags))
tags.sort()
tags = "[{}]".format(",".join(tags))
return f"{familyname}{tags}.ttf"
failed = False
if "_" in os.path.basename(font):
failed = True
yield FAIL,\
Message("invalid-char",
f'font filename "{font}" is invalid.'
f' It must not contain underscore characters!')
return
ttFont = TTFont(font)
if is_variable_font(ttFont):
if suffix(font) in STATIC_STYLE_NAMES:
failed = True
yield FAIL,\
Message("varfont-with-static-filename",
"This is a variable font, but it is using"
" a naming scheme typical of a static font.")
expected = variable_font_filename(ttFont)
font_filename = os.path.basename(font)
if font_filename != expected:
failed = True
yield FAIL,\
Message("bad-varfont-filename",
f"The file '{font_filename}' must be renamed"
f" to '{expected}' according to the"
f" Google Fonts naming policy for variable fonts.")
else:
if not canonical_stylename(font):
failed = True
style_names = '", "'.join(STATIC_STYLE_NAMES)
yield FAIL,\
Message("bad-static-filename",
f'Style name used in "{font}" is not canonical.'
f' You should rebuild the font using'
f' any of the following'
f' style names: "{style_names}".')
if not failed:
yield PASS, f"{font} is named canonically."
@check(
id = 'com.google.fonts/check/description/broken_links',
conditions = ['description'],
rationale = """
The snippet of HTML in the DESCRIPTION.en_us.html file is added to the font family webpage on the Google Fonts website. For that reason, all hyperlinks in it must be properly working.
"""
)
def com_google_fonts_check_description_broken_links(description):
"""Does DESCRIPTION file contain broken links?"""
import requests
from lxml import etree
doc = etree.fromstring("<html>" + description + "</html>")
broken_links = []
for a_href in doc.iterfind('.//a[@href]'):
link = a_href.get("href")
if link.startswith("mailto:") and \
"@" in link and \
"." in link.split("@")[1]:
yield INFO,\
Message("email",
f"Found an email address: {link}")
continue
try:
response = requests.head(link, allow_redirects=True, timeout=10)
code = response.status_code
if code != requests.codes.ok:
broken_links.append(f"{link} (status code: {code})")
except requests.exceptions.Timeout:
yield WARN,\
Message("timeout",
f"Timedout while attempting to access: '{link}'."
f" Please verify if that's a broken link.")
except requests.exceptions.RequestException:
broken_links.append(link)
if len(broken_links) > 0:
broken_links_list = '\n\t'.join(broken_links)
yield FAIL,\
Message("broken-links",
f"The following links are broken"
f" in the DESCRIPTION file:\n\t"
f"{broken_links_list}")
else:
yield PASS, "All links in the DESCRIPTION file look good!"
@check(
id = 'com.google.fonts/check/description/git_url',
conditions = ['description'],
rationale = """
The contents of the DESCRIPTION.en-us.html file are displayed on the Google Fonts website in the about section of each font family specimen page.
Since all of the Google Fonts collection is composed of libre-licensed fonts, this check enforces a policy that there must be a hypertext link in that page directing users to the repository where the font project files are made available.
Such hosting is typically done on sites like Github, Gitlab, GNU Savannah or any other git-based version control service.
"""
)
def com_google_fonts_check_description_git_url(description):
"""Does DESCRIPTION file contain a upstream Git repo URL?"""
from lxml import etree
doc = etree.fromstring("<html>" + description + "</html>")
git_urls = []
for a_href in doc.iterfind('.//a[@href]'):
link = a_href.get("href")
if "://git" in link:
git_urls.append(link)
yield INFO,\
Message("url-found",
f"Found a git repo URL: {link}")
if len(git_urls) > 0:
yield PASS, "Looks great!"
else:
yield FAIL,\
Message("lacks-git-url",
"Please host your font project on a public Git repo"
" (such as GitHub or GitLab) and place a link"
" in the DESCRIPTION.en_us.html file.")
@check(
id = 'com.google.fonts/check/description/variable_font',
conditions = ['is_variable_font',
'description'],
rationale = """
Families with variable fonts do not always mention that in their descriptions. Therefore, this check ensures that a standard boilerplate sentence is present in the DESCRIPTION.en_us.html files for all those families which are available as variable fonts.
"""
)
def com_google_fonts_check_description_variable_font(description):
"""Does DESCRIPTION file mention when a family
is available as variable font?"""
if "variable font" not in description.lower():
yield FAIL,\
Message("should-mention-varfonts",
"Please mention in the DESCRIPTION.en-us.html"
" that the family is a variable font. This check"
" expects the words 'variable font' to be present"
" in the text e.g 'This font is now available as"
" a variable font.'")
else:
yield PASS, "Looks good!"
@check(
id = 'com.google.fonts/check/description/valid_html',
conditions = ['descfile'],
rationale = """
When packaging families for being pushed to the `google/fonts` git repo, if there is no DESCRIPTION.en_us.html file, some older versions of the `add_font.py` tool insert a dummy description file which contains invalid html.
This file needs to either be replaced with an existing description file or edited by hand.
"""
)
def com_google_fonts_check_description_valid_html(descfile, description):
"""Is this a proper HTML snippet?"""
if "<p>" not in description or "</p>" not in description:
yield FAIL,\
Message("bad-html",
f"{descfile} does not look like a propper HTML snippet.")
else:
yield PASS, f"{descfile} is a propper HTML file."
@check(
id = 'com.google.fonts/check/description/min_length',
conditions = ['description']
)
def com_google_fonts_check_description_min_length(description):
"""DESCRIPTION.en_us.html must have more than 200 bytes."""
if len(description) <= 200:
yield FAIL,\
Message("too-short",
"DESCRIPTION.en_us.html must"
" have size larger than 200 bytes.")
else:
yield PASS, "DESCRIPTION.en_us.html is larger than 200 bytes."
@check(
id = 'com.google.fonts/check/description/max_length',
conditions = ['description']
)
def com_google_fonts_check_description_max_length(description):
"""DESCRIPTION.en_us.html must have less than 1000 bytes."""
if len(description) >= 1000:
yield FAIL,\
Message("too-long",
"DESCRIPTION.en_us.html must"
" have size smaller than 1000 bytes.")
else:
yield PASS, "DESCRIPTION.en_us.html is smaller than 1000 bytes."
@check(
id = 'com.google.fonts/check/metadata/parses',
conditions = ['family_directory'],
rationale = """
The purpose of this check is to ensure that the METADATA.pb file is not malformed.
"""
)
def com_google_fonts_check_metadata_parses(family_directory):
"""Check METADATA.pb parse correctly."""
from google.protobuf import text_format
from fontbakery.utils import get_FamilyProto_Message
try:
pb_file = os.path.join(family_directory, "METADATA.pb")
get_FamilyProto_Message(pb_file)
yield PASS, "METADATA.pb parsed successfuly."
except text_format.ParseError as e:
yield FAIL,\
Message("parsing-error",
f"Family metadata at {family_directory} failed to parse.\n"
f"TRACEBACK:\n{e}")
except FileNotFoundError:
yield SKIP, f"Font family at '{family_directory}' lacks a METADATA.pb file."
@check(
id = 'com.google.fonts/check/metadata/unknown_designer',
conditions = ['family_metadata']
)
def com_google_fonts_check_metadata_unknown_designer(family_metadata):
"""Font designer field in METADATA.pb must not be 'unknown'."""
if family_metadata.designer.lower() == 'unknown':
yield FAIL,\
Message("unknown-designer",
f"Font designer field is '{family_metadata.designer}'.")
else:
yield PASS, "Font designer field is not 'unknown'."
@check(
id = 'com.google.fonts/check/metadata/multiple_designers',
conditions = ['family_metadata'],
rationale = """
For a while the string "Multiple designers" was used as a placeholder on METADATA.pb files. We should replace all those instances with actual designer names so that proper credits are displayed on the Google Fonts family specimen pages.
If there's more than a single designer, the designer names must be separated by commas.
"""
)
def com_google_fonts_check_metadata_multiple_designers(family_metadata):
"""Font designer field in METADATA.pb must not contain 'Multiple designers'."""
if 'multiple designer' in family_metadata.designer.lower():
yield FAIL,\
Message("multiple-designers",
f"Font designer field is '{family_metadata.designer}'."
f" Please add an explicit comma-separated list of designer names.")
else:
yield PASS, "Looks good."
@check(
id = 'com.google.fonts/check/metadata/designer_values',
conditions = ['family_metadata'],
rationale = """
We must use commas instead of forward slashes because the server-side code at the fonts.google.com directory will segment the string on the commas into a list of names and display the first item in the list as the "principal designer" while the remaining names are identified as "contributors".
See eg https://fonts.google.com/specimen/Rubik
"""
)
def com_google_fonts_check_metadata_designer_values(family_metadata):
"""Multiple values in font designer field in
METADATA.pb must be separated by commas."""
if '/' in family_metadata.designer:
yield FAIL,\
Message("slash",
f"Font designer field contains a forward slash"
f" '{family_metadata.designer}'."
f" Please use commas to separate multiple names instead.")
else:
yield PASS, "Looks good."
@check(
id = 'com.google.fonts/check/metadata/broken_links',
conditions = ['family_metadata']
)
def com_google_fonts_check_metadata_broken_links(family_metadata):
"""Does METADATA.pb copyright field contain broken links?"""
import requests
broken_links = []
for font_metadata in family_metadata.fonts:
copyright = font_metadata.copyright
if "mailto:" in copyright:
yield INFO,\
Message("email",
f"Found an email address: {copyright}")
continue
if "http" in copyright:
link = "http" + copyright.split("http")[1]
for endchar in [' ', ')']:
if endchar in link:
link = link.split(endchar)[0]
try:
response = requests.head(link, allow_redirects=True, timeout=10)
code = response.status_code
if code != requests.codes.ok:
broken_links.append(("{} (status code: {})").format(link, code))
except requests.exceptions.Timeout:
yield WARN,\
Message("timeout",
f"Timed out while attempting to access: '{link}'."
f" Please verify if that's a broken link.")
except requests.exceptions.RequestException:
broken_links.append(link)
if len(broken_links) > 0:
broken_links_list = '\n\t'.join(broken_links)
yield FAIL,\
Message("broken-links",
f"The following links are broken"
f" in the METADATA.pb file:\n\t"
f"{broken_links_list}")
else:
yield PASS, "All links in the METADATA.pb file look good!"
@check(
id = 'com.google.fonts/check/metadata/undeclared_fonts',
conditions = ['family_metadata'],
rationale = """
The set of font binaries available, except the ones on a "static" subdir, must match exactly those declared on the METADATA.pb file.
Also, to avoid confusion, we expect that font files (other than statics) are not placed on subdirectories.
"""
)
def com_google_fonts_check_metadata_undeclared_fonts(family_metadata, family_directory):
"""Ensure METADATA.pb lists all font binaries."""
pb_binaries = []
for font_metadata in family_metadata.fonts:
pb_binaries.append(font_metadata.filename)
passed = True
binaries = []
for entry in os.listdir(family_directory):
if entry != "static" and os.path.isdir(os.path.join(family_directory, entry)):
for filename in os.listdir(os.path.join(family_directory, entry)):
if filename[-4:] in [".ttf", ".otf"]:
path = os.path.join(family_directory, entry, filename)
passed = False
yield WARN,\
Message("font-on-subdir",
f'The file "{path}" is a font binary'
f' in a subdirectory.\n'
f'Please keep all font files (except VF statics) directly'
f' on the root directory side-by-side'
f' with its corresponding METADATA.pb file.')
else:
# Note: This does not include any font binaries placed in a "static" subdir!
if entry[-4:] in [".ttf", ".otf"]:
binaries.append(entry)
for filename in set(pb_binaries) - set(binaries):
passed = False
yield FAIL,\
Message("file-missing",
f'The file "{filename}" declared on METADATA.pb'
f' is not available in this directory.')
for filename in set(binaries) - set(pb_binaries):
passed = False
yield FAIL,\
Message("file-not-declared",
f'The file "{filename}" is not declared on METADATA.pb')
if passed:
yield PASS, "OK"
@disable # TODO: re-enable after addressing issue #1998
@check(
id = 'com.google.fonts/check/family/equal_numbers_of_glyphs',
conditions = ['are_ttf',
'stylenames_are_canonical']
)
def com_google_fonts_check_family_equal_numbers_of_glyphs(ttFonts):
"""Fonts have equal numbers of glyphs?"""
from .googlefonts_conditions import canonical_stylename
# ttFonts is an iterator, so here we make a list from it
# because we'll have to iterate twice in this check implementation:
the_ttFonts = list(ttFonts)
failed = False
max_stylename = None
max_count = 0
max_glyphs = None
for ttFont in the_ttFonts:
fontname = ttFont.reader.file.name
stylename = canonical_stylename(fontname)
this_count = len(ttFont['glyf'].glyphs)
if this_count > max_count:
max_count = this_count
max_stylename = stylename
max_glyphs = set(ttFont['glyf'].glyphs)
for ttFont in the_ttFonts:
fontname = ttFont.reader.file.name
stylename = canonical_stylename(fontname)
these_glyphs = set(ttFont['glyf'].glyphs)
this_count = len(these_glyphs)
if this_count != max_count:
failed = True
all_glyphs = max_glyphs.union(these_glyphs)
common_glyphs = max_glyphs.intersection(these_glyphs)
diff = all_glyphs - common_glyphs
diff_count = len(diff)
if diff_count < 10:
diff = ", ".join(diff)
else:
diff = ", ".join(list(diff)[:10]) + " (and more)"
yield FAIL,\
Message("glyph-count-diverges",
f"{stylename} has {this_count} glyphs while"
f" {max_stylename} has {max_count} glyphs."
f" There are {diff_count} different glyphs"
f" among them: {diff}")
if not failed:
yield PASS, ("All font files in this family have"
" an equal total ammount of glyphs.")
@disable # TODO: re-enable after addressing issue #1998
@check(
id = 'com.google.fonts/check/family/equal_glyph_names',
conditions = ['are_ttf']
)
def com_google_fonts_check_family_equal_glyph_names(ttFonts):
"""Fonts have equal glyph names?"""
from .googlefonts_conditions import style
fonts = list(ttFonts)
all_glyphnames = set()
for ttFont in fonts:
all_glyphnames |= set(ttFont["glyf"].glyphs.keys())
missing = {}
available = {}
for glyphname in all_glyphnames:
missing[glyphname] = []
available[glyphname] = []
failed = False
for ttFont in fonts:
fontname = ttFont.reader.file.name
these_ones = set(ttFont["glyf"].glyphs.keys())
for glyphname in all_glyphnames:
if glyphname not in these_ones:
failed = True
missing[glyphname].append(fontname)
else:
available[glyphname].append(fontname)
for gn in missing.keys():
if missing[gn]:
available_styles = [style(k) for k in available[gn]]
missing_styles = [style(k) for k in missing[gn]]
if None not in available_styles + missing_styles:
# if possible, use stylenames in the log messages.
avail = ', '.join(available_styles)
miss = ', '.join(missing_styles)
else:
# otherwise, print filenames:
avail = ', '.join(available[gn])
miss = ', '.join(missing[gn])
yield FAIL,\
Message("missing-glyph",
f"Glyphname '{gn}' is defined on {avail}"
f" but is missing on {miss}.")
if not failed:
yield PASS, "All font files have identical glyph names."
@check(
id = 'com.google.fonts/check/fstype',
rationale = """
The fsType in the OS/2 table is a legacy DRM-related field. Fonts in the Google Fonts collection must have it set to zero (also known as "Installable Embedding"). This setting indicates that the fonts can be embedded in documents and permanently installed by applications on remote systems.
More detailed info is available at:
https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fstype
"""
)
def com_google_fonts_check_fstype(ttFont):
"""Checking OS/2 fsType does not impose restrictions."""
value = ttFont['OS/2'].fsType
if value != 0:
FSTYPE_RESTRICTIONS = {
0x0002: ("* The font must not be modified, embedded or exchanged in"
" any manner without first obtaining permission of"
" the legal owner."),
0x0004: ("The font may be embedded, and temporarily loaded on the"
" remote system, but documents that use it must"
" not be editable."),
0x0008: ("The font may be embedded but must only be installed"
" temporarily on other systems."),
0x0100: ("The font may not be subsetted prior to embedding."),
0x0200: ("Only bitmaps contained in the font may be embedded."
" No outline data may be embedded.")
}
restrictions = ""
for bit_mask in FSTYPE_RESTRICTIONS.keys():
if value & bit_mask:
restrictions += FSTYPE_RESTRICTIONS[bit_mask]
if value & 0b1111110011110001:
restrictions += ("* There are reserved bits set,"
" which indicates an invalid setting.")
yield FAIL,\
Message("drm",
f"In this font fsType is set to {value} meaning that:\n"
f"{restrictions}\n"
f"\n"
f"No such DRM restrictions can be enabled on the"
f" Google Fonts collection, so the fsType field"
f" must be set to zero (Installable Embedding) instead.")
else:
yield PASS, "OS/2 fsType is properly set to zero."
@check(
id = 'com.google.fonts/check/vendor_id',
conditions = ['registered_vendor_ids'],
rationale = """
Microsoft keeps a list of font vendors and their respective contact info. This list is updated regularly and is indexed by a 4-char "Vendor ID" which is stored in the achVendID field of the OS/2 table.
Registering your ID is not mandatory, but it is a good practice since some applications may display the type designer / type foundry contact info on some dialog and also because that info will be visible on Microsoft's website:
https://docs.microsoft.com/en-us/typography/vendors/
This check verifies whether or not a given font's vendor ID is registered in that list or if it has some of the default values used by the most common font editors.
Each new FontBakery release includes a cached copy of that list of vendor IDs. If you registered recently, you're safe to ignore warnings emitted by this check, since your ID will soon be included in one of our upcoming releases.
"""
)
def com_google_fonts_check_vendor_id(ttFont, registered_vendor_ids):
"""Checking OS/2 achVendID."""
SUGGEST_MICROSOFT_VENDORLIST_WEBSITE = (
"If you registered it recently, then it's safe to ignore this warning message."
" Otherwise, you should set it to your own unique 4 character code,"
" and register it with Microsoft at"
" https://www.microsoft.com/typography/links/vendorlist.aspx\n")
vid = ttFont['OS/2'].achVendID
bad_vids = ['UKWN', 'ukwn', 'PfEd']
if vid is None:
yield WARN,\
Message("not set",
f"OS/2 VendorID is not set."
f" {SUGGEST_MICROSOFT_VENDORLIST_WEBSITE}")
elif vid in bad_vids:
yield WARN,\
Message("bad",
f"OS/2 VendorID is '{vid}', a font editor default."
f" {SUGGEST_MICROSOFT_VENDORLIST_WEBSITE}")
elif vid not in registered_vendor_ids.keys():
yield WARN,\
Message("unknown",
f"OS/2 VendorID value '{vid}' is not yet recognized."
f" {SUGGEST_MICROSOFT_VENDORLIST_WEBSITE}")
else:
yield PASS, f"OS/2 VendorID '{vid}' looks good!"
@check(
id = 'com.google.fonts/check/glyph_coverage',
rationale = """
Google Fonts expects that fonts in its collection support at least the minimal set of characters defined in the `GF-latin-core` glyph-set.
"""
)
def com_google_fonts_check_glyph_coverage(ttFont):
"""Check `Google Fonts Latin Core` glyph coverage."""
from fontbakery.utils import pretty_print_list
from fontbakery.constants import GF_latin_core
font_codepoints = set()
for table in ttFont['cmap'].tables:
if (table.platformID == PlatformID.WINDOWS and
table.platEncID == WindowsEncodingID.UNICODE_BMP):
font_codepoints.update(table.cmap.keys())
required_codepoints = set(GF_latin_core.keys())
diff = required_codepoints - font_codepoints
if bool(diff):
missing = ['0x%04X (%s)' % (c, GF_latin_core[c][1]) for c in sorted(diff)]
yield FAIL,\
Message("missing-codepoints",
f"Missing required codepoints:"
f" {pretty_print_list(missing, shorten=4)}")
if len(missing) > 4:
missing_list = "\n\t\t".join(missing)
yield INFO,\
Message("missing-codepoints-verbose",
f"Here's the full list of required codepoints"
f" still missing:\n\t\t{missing_list}")
else:
yield PASS, "OK"
@check(
id = 'com.google.fonts/check/name/unwanted_chars'
)
def com_google_fonts_check_name_unwanted_chars(ttFont):
"""Substitute copyright, registered and trademark
symbols in name table entries."""
failed = False
replacement_map = [("\u00a9", '(c)'),
("\u00ae", '(r)'),
("\u2122", '(tm)')]
for name in ttFont['name'].names:
string = str(name.string, encoding=name.getEncoding())
for mark, ascii_repl in replacement_map:
new_string = string.replace(mark, ascii_repl)
if string != new_string:
yield FAIL,\
Message("unwanted-chars",
f"NAMEID #{name.nameID} contains symbols that"
f" should be replaced by '{ascii_repl}'.")
failed = True
if not failed:
yield PASS, ("No need to substitute copyright, registered and"
" trademark symbols in name table entries of this font.")
@check(
id = 'com.google.fonts/check/usweightclass',
conditions=['expected_style']
)
def com_google_fonts_check_usweightclass(ttFont, expected_style):
"""Checking OS/2 usWeightClass."""
from fontbakery.profiles.shared_conditions import is_ttf
expected_value = expected_style.usWeightClass
weight_name = expected_style.name
value = ttFont['OS/2'].usWeightClass
if value != expected_value:
if is_ttf(ttFont) and \
(weight_name in ['Thin', 'Thin Italic'] and value == 100) or \
(weight_name in ['ExtraLight', 'ExtraLight Italic'] and value == 200):
yield WARN,\
Message("blur-on-windows",
f"{weight_name}:{value} is OK on TTFs, but"
f" OTF files with those values will cause"
f" bluring on Windows."
f" GlyphsApp users must set an Instance"
f" Custom Parameter for the Thin and ExtraLight"
f" styles to 250 and 275, so that if OTFs are"
f" exported then it will not blur on Windows.")
else:
yield FAIL,\
Message("bad-value",
f"OS/2 usWeightClass expected value for"
f" '{weight_name}' is {expected_value} but"
f" this font has {value}.\n"
f" GlyphsApp users should set a Custom Parameter"
f" for 'Axis Location' in each master to ensure"
f" that the information is accurately built into"
f" variable fonts.")
else:
yield PASS, "OS/2 usWeightClass value looks good!"
@check(
id = 'com.google.fonts/check/family/has_license',
conditions=['gfonts_repo_structure'],
)
def com_google_fonts_check_family_has_license(licenses):
"""Check font has a license."""
from fontbakery.utils import pretty_print_list
if len(licenses) > 1:
filenames = [os.path.basename(f) for f in licenses]
yield FAIL,\
Message("multiple",
f"More than a single license file found:"
f" {pretty_print_list(filenames)}")
elif not licenses:
yield FAIL,\
Message("no-license",
"No license file was found."
" Please add an OFL.txt or a LICENSE.txt file."
" If you are running fontbakery on a Google Fonts"
" upstream repo, which is fine, just make sure"
" there is a temporary license file in the same folder.")
else:
yield PASS, "Found license at '{}'".format(licenses[0])
@check(
id = 'com.google.fonts/check/license/OFL_copyright',
conditions = ['license_contents'],
rationale = """
An OFL.txt file's first line should be the font copyright e.g:
"Copyright 2019 The Montserrat Project Authors (https://github.com/julietaula/montserrat)"
""",
misc_metadata = {
'request': 'https://github.com/googlefonts/fontbakery/issues/2764'
})
def com_google_fonts_check_license_OFL_copyright(license_contents):
"""Check license file has good copyright string."""
import re
string = license_contents.strip().split('\n')[0].lower()
does_match = re.search(r'copyright [0-9]{4} the .* project authors \([^\@]*\)', string)
if does_match:
yield PASS, "looks good"
else:
yield FAIL, (f'First line in license file does not match expected format:'
f' "{string}"')
@check(
id = 'com.google.fonts/check/name/license',
conditions = ['license'],
rationale = """
A known licensing description must be provided in the NameID 14 (LICENSE DESCRIPTION) entries of the name table.
The source of truth for this check (to determine which license is in use) is a file placed side-by-side to your font project including the licensing terms.
Depending on the chosen license, one of the following string snippets is expected to be found on the NameID 13 (LICENSE DESCRIPTION) entries of the name table:
- "This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is available with a FAQ at: https://scripts.sil.org/OFL"
- "Licensed under the Apache License, Version 2.0"
- "Licensed under the Ubuntu Font Licence 1.0."
Currently accepted licenses are Apache or Open Font License.
For a small set of legacy families the Ubuntu Font License may be acceptable as well.
When in doubt, please choose OFL for new font projects.
""",
misc_metadata = {
'priority': PriorityLevel.CRITICAL
})
def com_google_fonts_check_name_license(ttFont, license):
"""Check copyright namerecords match license file."""
from fontbakery.constants import PLACEHOLDER_LICENSING_TEXT
failed = False
http_warn = False
placeholder = PLACEHOLDER_LICENSING_TEXT[license]
entry_found = False
for i, nameRecord in enumerate(ttFont["name"].names):
if nameRecord.nameID == NameID.LICENSE_DESCRIPTION:
entry_found = True
value = nameRecord.toUnicode()
if "http://" in value:
yield WARN,\
Message("http-in-description",
f'Please consider using HTTPS URLs at name table entry [plat={nameRecord.platformID}, enc={nameRecord.platEncID}, name={nameRecord.nameID}]')
value = "https://".join(value.split("http://"))
http_warn = True
if value != placeholder:
failed = True
yield FAIL,\
Message("wrong", \
f'License file {license} exists but'
f' NameID {NameID.LICENSE_DESCRIPTION}'
f' (LICENSE DESCRIPTION) value on platform'
f' {nameRecord.platformID}'
f' ({PlatformID(nameRecord.platformID).name})'
f' is not specified for that.'
f' Value was: "{value}"'
f' Must be changed to "{placeholder}"')
if http_warn:
yield WARN,\
Message("http",
"For now we're still accepting http URLs,"
" but you should consider using https instead.\n")
if not entry_found:
yield FAIL,\
Message("missing", \
f"Font lacks NameID {NameID.LICENSE_DESCRIPTION}"
f" (LICENSE DESCRIPTION). A proper licensing"
f" entry must be set.")
elif not failed:
yield PASS, "Licensing entry on name table is correctly set."
@check(
id = 'com.google.fonts/check/name/license_url',
rationale = """
A known license URL must be provided in the NameID 14 (LICENSE INFO URL) entry of the name table.