/
googlefonts.py
3347 lines (2957 loc) · 126 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
from fontbakery.checkrunner import (
INFO
, WARN
, ERROR
, SKIP
, PASS
, FAIL
, Section
)
import os
from .shared_conditions import is_variable_font
from fontbakery.callable import condition, check, disable
from fontbakery.message import Message
from fontbakery.constants import(
# TODO: priority levels are not yet part of the new runner/reporters.
# How did we ever use this information?
# Check priority levels:
CRITICAL
, IMPORTANT
# , NORMAL
# , LOW
# , TRIVIAL
)
from fontbakery.fonts_spec import spec_factory
spec_imports = (
('.', ('general', 'cmap', 'head', 'os2', 'post', 'name',
'hhea', 'dsig', 'hmtx', 'gpos', 'gdef', 'kern', 'glyf',
'fvar', 'shared_conditions', 'loca')
),
)
# this is from the output of
# $ fontbakery check-specification fontbakery.specifications.googlefonts -L
expected_check_ids = [
'com.google.fonts/check/001' # Checking file is named canonically.
, 'com.google.fonts/check/002' # Checking all files are in the same directory.
, 'com.google.fonts/check/003' # Does DESCRIPTION file contain broken links?
, 'com.google.fonts/check/004' # Is this a propper HTML snippet?
, 'com.google.fonts/check/005' # DESCRIPTION.en_us.html must have more than 200 bytes.
, 'com.google.fonts/check/006' # DESCRIPTION.en_us.html must have less than 1000 bytes.
, 'com.google.fonts/check/007' # Font designer field in METADATA.pb must not be 'unknown'.
, 'com.google.fonts/check/008' # Fonts have consistent underline thickness?
, 'com.google.fonts/check/009' # Fonts have consistent PANOSE proportion?
, 'com.google.fonts/check/010' # Fonts have consistent PANOSE family type?
, 'com.google.fonts/check/011' # Fonts have equal numbers of glyphs?
, 'com.google.fonts/check/012' # Fonts have equal glyph names?
, 'com.google.fonts/check/013' # Fonts have equal unicode encodings?
, 'com.google.fonts/check/014' # Make sure all font files have the same version value.
, 'com.google.fonts/check/015' # Font has post table version 2?
, 'com.google.fonts/check/016' # Checking OS/2 fsType.
, 'com.google.fonts/check/018' # Checking OS/2 achVendID.
, 'com.google.fonts/check/019' # Substitute copyright, registered and trademark symbols in name table entries.
, 'com.google.fonts/check/020' # Checking OS/2 usWeightClass.
, 'com.google.fonts/check/028' # Check font has a license.
, 'com.google.fonts/check/029' # Check copyright namerecords match license file.
, 'com.google.fonts/check/030' # "License URL matches License text on name table?
, 'com.google.fonts/check/031' # Description strings in the name table must not contain copyright info.
, 'com.google.fonts/check/032' # Description strings in the name table must not exceed 200 characters.
, 'com.google.fonts/check/033' # Checking correctness of monospaced metadata.
, 'com.google.fonts/check/034' # Check if OS/2 xAvgCharWidth is correct.
, 'com.google.fonts/check/035' # Checking with ftxvalidator.
, 'com.google.fonts/check/036' # Checking with ots-sanitize.
, 'com.google.fonts/check/037' # Checking with Microsoft Font Validator.
, 'com.google.fonts/check/038' # FontForge validation outputs error messages?
, 'com.google.fonts/check/039' # FontForge checks.
, 'com.google.fonts/check/040' # Checking OS/2 usWinAscent & usWinDescent.
, 'com.google.fonts/check/041' # Checking Vertical Metric Linegaps.
, 'com.google.fonts/check/042' # Checking OS/2 Metrics match hhea Metrics.
, 'com.google.fonts/check/043' # Checking unitsPerEm value is reasonable.
, 'com.google.fonts/check/044' # Checking font version fields.
, 'com.google.fonts/check/045' # Does the font have a DSIG table?
, 'com.google.fonts/check/046' # Font contains the first few mandatory glyphs (.null or NULL, CR and space)?
, 'com.google.fonts/check/047' # Font contains glyphs for whitespace characters?
, 'com.google.fonts/check/048' # Font has **proper** whitespace glyph names?
, 'com.google.fonts/check/049' # Whitespace glyphs have ink?
, 'com.google.fonts/check/050' # Whitespace glyphs have coherent widths?
, 'com.google.fonts/check/052' # Font contains all required tables?
, 'com.google.fonts/check/053' # Are there unwanted tables?
, 'com.google.fonts/check/054' # Show hinting filesize impact.
, 'com.google.fonts/check/055' # Version format is correct in 'name' table?
, 'com.google.fonts/check/056' # Font has old ttfautohint applied?
, 'com.google.fonts/check/057' # Name table entries should not contain line-breaks.
, 'com.google.fonts/check/058' # Glyph names are all valid?
, 'com.google.fonts/check/059' # Font contains unique glyph names?
, 'com.google.fonts/check/061' # EPAR table present in font?
, 'com.google.fonts/check/062' # Is 'gasp' table correctly set?
, 'com.google.fonts/check/063' # Does GPOS table have kerning information?
, 'com.google.fonts/check/064' # Is there a caret position declared for every ligature?
, 'com.google.fonts/check/065' # Is there kerning info for non-ligated sequences?
, 'com.google.fonts/check/066' # Is there a "kern" table declared in the font?
, 'com.google.fonts/check/067' # Make sure family name does not begin with a digit.
, 'com.google.fonts/check/068' # Does full font name begin with the font family name?
, 'com.google.fonts/check/069' # Is there any unused data at the end of the glyf table?
, 'com.google.fonts/check/070' # Font has all expected currency sign characters?
, 'com.google.fonts/check/071' # Font follows the family naming recommendations?
, 'com.google.fonts/check/072' # Font enables smart dropout control in "prep" table instructions?
, 'com.google.fonts/check/073' # MaxAdvanceWidth is consistent with values in the Hmtx and Hhea tables?
, 'com.google.fonts/check/074' # Are there non-ASCII characters in ASCII-only NAME table entries?
, 'com.google.fonts/check/075' # Check for points out of bounds.
, 'com.google.fonts/check/076' # Check glyphs have unique unicode codepoints.
, 'com.google.fonts/check/077' # Check all glyphs have codepoints assigned.
#, 'com.google.fonts/check/078' # Check that glyph names do not exceed max length.
, 'com.google.fonts/check/079' # Monospace font has hhea.advanceWidthMax equal to each glyph's advanceWidth?
, 'com.google.fonts/check/081' # METADATA.pb: Fontfamily is listed on Google Fonts API?
, 'com.google.fonts/check/083' # METADATA.pb: check if fonts field only has unique "full_name" values.
, 'com.google.fonts/check/084' # METADATA.pb: check if fonts field only contains unique style:weight pairs.
, 'com.google.fonts/check/085' # METADATA.pb license is "APACHE2", "UFL" or "OFL"?
, 'com.google.fonts/check/086' # METADATA.pb should contain at least "menu" and "latin" subsets.
, 'com.google.fonts/check/087' # METADATA.pb subsets should be alphabetically ordered.
, 'com.google.fonts/check/088' # METADATA.pb: Copyright notice is the same in all fonts?
, 'com.google.fonts/check/089' # Check that METADATA.pb family values are all the same.
, 'com.google.fonts/check/090' # METADATA.pb: According Google Fonts standards, families should have a Regular style.
, 'com.google.fonts/check/091' # METADATA.pb: Regular should be 400.
, 'com.google.fonts/check/092' # Checks METADATA.pb font.name field matches family name declared on the name table.
, 'com.google.fonts/check/093' # Checks METADATA.pb font.post_script_name matches postscript name declared on the name table.
, 'com.google.fonts/check/094' # METADATA.pb font.full_name value matches fullname declared on the name table?
, 'com.google.fonts/check/095' # METADATA.pb font.name value should be same as the family name declared on the name table.
, 'com.google.fonts/check/096' # METADATA.pb font.full_name and font.post_script_name fields have equivalent values ?
, 'com.google.fonts/check/097' # METADATA.pb font.filename and font.post_script_name fields have equivalent values?
, 'com.google.fonts/check/098' # METADATA.pb font.name field contains font name in right format?
, 'com.google.fonts/check/099' # METADATA.pb font.full_name field contains font name in right format?
, 'com.google.fonts/check/100' # METADATA.pb font.filename field contains font name in right format?
, 'com.google.fonts/check/101' # METADATA.pb font.post_script_name field contains font name in right format?
, 'com.google.fonts/check/102' # Copyright notice on METADATA.pb matches canonical pattern?
, 'com.google.fonts/check/103' # Copyright notice on METADATA.pb does not contain Reserved Font Name?
, 'com.google.fonts/check/104' # METADATA.pb: Copyright notice shouldn't exceed 500 chars.
, 'com.google.fonts/check/105' # Filename is set canonically in METADATA.pb?
, 'com.google.fonts/check/106' # METADATA.pb font.style "italic" matches font internals?
, 'com.google.fonts/check/107' # METADATA.pb font.style "normal" matches font internals?
, 'com.google.fonts/check/108' # METADATA.pb font.name and font.full_name fields match the values declared on the name table?
, 'com.google.fonts/check/109' # METADATA.pb: Check if fontname is not camel cased.
, 'com.google.fonts/check/110' # METADATA.pb: Check font name is the same as family name.
, 'com.google.fonts/check/111' # METADATA.pb: Check that font weight has a canonical value.
, 'com.google.fonts/check/112' # Checking OS/2 usWeightClass matches weight specified at METADATA.pb.
, 'com.google.fonts/check/113' # METADATA.pb weight matches postScriptName.
, 'com.google.fonts/check/115' # METADATA.pb: Font styles are named canonically?
, 'com.google.fonts/check/116' # Is font em size (ideally) equal to 1000?
, 'com.google.fonts/check/117' # Version number has increased since previous release on Google Fonts?
, 'com.google.fonts/check/118' # Glyphs are similiar to Google Fonts version?
, 'com.google.fonts/check/119' # TTFAutohint x-height increase value is same as in previous release on Google Fonts ?
, 'com.google.fonts/check/129' # Checking OS/2 fsSelection value.
, 'com.google.fonts/check/130' # Checking post.italicAngle value.
, 'com.google.fonts/check/131' # Checking head.macStyle value.
, 'com.google.fonts/check/152' # Name table strings must not contain 'Reserved Font Name'.
, 'com.google.fonts/check/153' # Check if each glyph has the recommended amount of contours.
, 'com.google.fonts/check/154' # Check font has same encoded glyphs as version hosted on fonts.google.com
, 'com.google.fonts/check/155' # Copyright field for this font on METADATA.pb matches all copyright notice entries on the name table ?
, 'com.google.fonts/check/156' # Font has all mandatory 'name' table entries ?
, 'com.google.fonts/check/157' # Check name table: FONT_FAMILY_NAME entries.
, 'com.google.fonts/check/158' # Check name table: FONT_SUBFAMILY_NAME entries.
, 'com.google.fonts/check/159' # Check name table: FULL_FONT_NAME entries.
, 'com.google.fonts/check/160' # Check name table: POSTSCRIPT_NAME entries.
, 'com.google.fonts/check/161' # Check name table: TYPOGRAPHIC_FAMILY_NAME entries.
, 'com.google.fonts/check/162' # Check name table: TYPOGRAPHIC_SUBFAMILY_NAME entries.
, 'com.google.fonts/check/163' # Combined length of family and style must not exceed 20 characters.
, 'com.google.fonts/check/164' # Length of copyright notice must not exceed 500 characters.
, 'com.google.fonts/check/165' # Familyname must be unique according to namecheck.fontdata.com
, 'com.google.fonts/check/166' # Check for font-v versioning
, 'com.google.fonts/check/167' # The variable font 'wght' (Weight) axis coordinate must be 400 on the 'Regular' instance.
, 'com.google.fonts/check/168' # The variable font 'wdth' (Width) axis coordinate must be 100 on the 'Regular' instance.
, 'com.google.fonts/check/169' # The variable font 'slnt' (Slant) axis coordinate must be zero on the 'Regular' instance.
, 'com.google.fonts/check/170' # The variable font 'ital' (Italic) axis coordinate must be zero on the 'Regular' instance.
, 'com.google.fonts/check/171' # The variable font 'opsz' (Optical Size) axis coordinate should be between 9 and 13 on the 'Regular' instance.
, 'com.google.fonts/check/172' # The variable font 'wght' (Weight) axis coordinate must be 700 on the 'Bold'
, 'com.google.fonts/check/174' # Check a static ttf can be generated from a variable font.
, 'com.google.fonts/check/180' # Does the number of glyphs in the loca table match the maxp table?
, 'com.google.fonts/check/ttx-roundtrip' # Checking with fontTools.ttx
]
specification = spec_factory(default_section=Section("Google Fonts"))
# -------------------------------------------------------------------
@condition
def style(font):
"""Determine font style from canonical filename."""
from fontbakery.constants import STYLE_NAMES
filename = os.path.basename(font)
if '-' in filename:
stylename = os.path.splitext(filename)[0].split('-')[1]
if stylename in [name.replace(' ', '') for name in STYLE_NAMES]:
return stylename
return None
@condition
def expected_os2_weight(style):
"""The weight name and the expected OS/2 usWeightClass value inferred from
the style part of the font name
The Google Font's API which serves the fonts can only serve
the following weights values with the corresponding subfamily styles:
250, Thin
275, ExtraLight
300, Light
400, Regular
500, Medium
600, SemiBold
700, Bold
800, ExtraBold
900, Black
Thin is not set to 100 because of legacy Windows GDI issues:
https://www.adobe.com/devnet/opentype/afdko/topic_font_wt_win.html
"""
if not style:
return None
# Weight name to value mapping:
GF_API_WEIGHTS = {
"Thin": 250,
"ExtraLight": 275,
"Light": 300,
"Regular": 400,
"Medium": 500,
"SemiBold": 600,
"Bold": 700,
"ExtraBold": 800,
"Black": 900
}
if style == "Italic":
weight_name = "Regular"
elif style.endswith("Italic"):
weight_name = style.replace("Italic", "")
else:
weight_name = style
expected = GF_API_WEIGHTS[weight_name]
return weight_name, expected
@check(
id = 'com.google.fonts/check/001',
misc_metadata = {
'priority': CRITICAL
}
)
def com_google_fonts_check_001(font):
"""Checking file is named canonically.
A font's filename must be composed in the following manner:
<familyname>-<stylename>.ttf
e.g. Nunito-Regular.ttf, Oswald-BoldItalic.ttf
Variable fonts must use the "-VF" suffix such:
e.g. Roboto-VF.ttf, Barlow-VF.ttf,
Example-Roman-VF.ttf, Familyname-Italic-VF.ttf
"""
from fontbakery.constants import STYLE_NAMES
from fontTools.ttLib import TTFont
filename = os.path.basename(font)
basename = os.path.splitext(filename)[0]
# remove spaces in style names
valid_style_suffixes = [name.replace(' ', '') for name in STYLE_NAMES]
valid_varfont_suffixes = ["VF",
"Italic-VF",
"Roman-VF"]
suffix = basename.split('-')
suffix.pop(0)
suffix = '-'.join(suffix)
if ('-' in basename and
(suffix in valid_varfont_suffixes
and is_variable_font(TTFont(font)))
or (suffix in valid_style_suffixes
and not is_variable_font(TTFont(font)))):
yield PASS, f"{font} is named canonically."
else:
yield FAIL, ('Style name used in "{}" is not canonical.'
' You should rebuild the font using'
' any of the following'
' style names: "{}".').format(font,
'", "'.join(STYLE_NAMES))
@condition
def family_directory(fonts):
"""Get the path of font project directory."""
if fonts:
return os.path.dirname(fonts[0])
@condition
def descfile(family_directory):
"""Get the path of the DESCRIPTION file of a given font project."""
if family_directory:
descfilepath = os.path.join(family_directory, "DESCRIPTION.en_us.html")
if os.path.exists(descfilepath):
return descfilepath
@condition
def description(descfile):
"""Get the contents of the DESCRIPTION file of a font project."""
if not descfile:
return
import io
return io.open(descfile, "r", encoding="utf-8").read()
@check(
id = 'com.google.fonts/check/003',
conditions = ['description']
)
def com_google_fonts_check_003(description):
"""Does DESCRIPTION file contain broken links?"""
from lxml.html import HTMLParser
import defusedxml.lxml
import requests
doc = defusedxml.lxml.fromstring(description, parser=HTMLParser())
broken_links = []
for link in doc.xpath('//a/@href'):
if link.startswith("mailto:") and \
"@" in link and \
"." in link.split("@")[1]:
yield INFO, (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(("url: '{}' "
"status code: '{}'").format(link, code))
except requests.exceptions.Timeout:
yield WARN, ("Timedout while attempting to access: '{}'."
" Please verify if that's a broken link.").format(link)
except requests.exceptions.RequestException:
broken_links.append(link)
if len(broken_links) > 0:
yield FAIL, ("The following links are broken"
" in the DESCRIPTION file:"
" '{}'").format("', '".join(broken_links))
else:
yield PASS, "All links in the DESCRIPTION file look good!"
@check(
id = 'com.google.fonts/check/004',
conditions = ['descfile']
)
def com_google_fonts_check_004(descfile, description):
"""Is this a proper HTML snippet?
When packaging families for google/fonts, if there is no
DESCRIPTION.en_us.html file, the add_font.py metageneration tool will
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."""
if "<p>" not in description or "</p>" not in description:
yield FAIL, 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/005',
conditions = ['description']
)
def com_google_fonts_check_005(description):
"""DESCRIPTION.en_us.html must have more than 200 bytes."""
if len(description) <= 200:
yield FAIL, ("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/006',
conditions = ['description']
)
def com_google_fonts_check_006(description):
"""DESCRIPTION.en_us.html must have less than 1000 bytes."""
if len(description) >= 1000:
yield FAIL, ("DESCRIPTION.en_us.html must"
" have size smaller than 1000 bytes.")
else:
yield PASS, "DESCRIPTION.en_us.html is smaller than 1000 bytes."
@condition
def family_metadata(family_directory):
from fontbakery.utils import get_FamilyProto_Message
if family_directory:
pb_file = os.path.join(family_directory, "METADATA.pb")
if os.path.exists(pb_file):
return get_FamilyProto_Message(pb_file)
@check(
id = 'com.google.fonts/check/007',
conditions = ['family_metadata']
)
def com_google_fonts_check_007(family_metadata):
"""Font designer field in METADATA.pb must not be 'unknown'."""
if family_metadata.designer.lower() == 'unknown':
yield FAIL, f"Font designer field is '{family_metadata.designer}'."
else:
yield PASS, "Font designer field is not 'unknown'."
@check(
id = 'com.google.fonts/check/011',
conditions = ['is_ttf']
)
def com_google_fonts_check_011(ttFonts):
"""Fonts have equal numbers of glyphs?"""
fonts = list(ttFonts)
failed = False
max_style = None
max_count = 0
for ttFont in fonts:
fontname = ttFont.reader.file.name
stylename = style(fontname)
this_count = len(ttFont['glyf'].glyphs)
if this_count > max_count:
max_count = this_count
max_style = stylename
for ttFont in fonts:
fontname = ttFont.reader.file.name
stylename = style(fontname)
this_count = len(ttFont['glyf'].glyphs)
if this_count != max_count:
failed = True
yield FAIL, ("{} has {} glyphs while"
" {} has {} glyphs.").format(stylename,
this_count,
max_style,
max_count)
if not failed:
yield PASS, ("All font files in this family have"
" an equal total ammount of glyphs.")
@check(
id = 'com.google.fonts/check/012',
conditions = ['is_ttf']
)
def com_google_fonts_check_012(ttFonts):
"""Fonts have equal glyph names?"""
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
stylename = style(fontname)
these_ones = set(ttFont["glyf"].glyphs.keys())
for glyphname in all_glyphnames:
if glyphname not in these_ones:
failed = True
missing[glyphname].append(stylename)
else:
available[glyphname].append(stylename)
for gn in missing.keys():
if missing[gn]:
yield FAIL, ("Glyphname '{}' is defined on {}"
" but is missing on"
" {}.").format(gn,
', '.join(missing[gn]),
', '.join(available[gn]))
if not failed:
yield PASS, "All font files have identical glyph names."
@check(
id = 'com.google.fonts/check/016'
)
def com_google_fonts_check_016(ttFont):
"""Checking OS/2 fsType.
Fonts must have their fsType field set to zero.
This setting is known as Installable Embedding, meaning
that none of the DRM restrictions are enabled on the fonts.
More info available at:
https://docs.microsoft.com/en-us/typography/opentype/spec/os2#fstype
"""
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, ("OS/2 fsType is a legacy DRM-related field.\n"
"In this font it is set to {} meaning that:\n"
"{}\n"
"No such DRM restrictions can be enabled on the"
" Google Fonts collection, so the fsType field"
" must be set to zero (Installable Embedding) instead.\n"
"Fonts with this setting indicate that they may be embedded"
" and permanently installed on the remote system"
" by an application.\n\n"
" More detailed info is available at:\n"
" https://docs.microsoft.com/en-us"
"/typography/opentype/spec/os2#fstype"
"").format(value, restrictions)
else:
yield PASS, ("OS/2 fsType is properly set to zero.")
@condition
def registered_vendor_ids():
"""Get a list of vendor IDs from Microsoft's website."""
from bs4 import BeautifulSoup
from pkg_resources import resource_filename
registered_vendor_ids = {}
CACHED = resource_filename('fontbakery',
'data/fontbakery-microsoft-vendorlist.cache')
content = open(CACHED).read()
soup = BeautifulSoup(content, 'html.parser')
IDs = [chr(c + ord('a')) for c in range(ord('z') - ord('a') + 1)]
IDs.append("vendor-id-and-name-list")
for section_id in IDs:
section = soup.find('h2', {'id': section_id})
table = section.find_next_sibling('table')
if not table: continue
#print ("table: '{}'".format(table))
for row in table.findAll('tr'):
#print("ROW: '{}'".format(row))
cells = row.findAll('td')
# pad the code to make sure it is a 4 char string,
# otherwise eg "CF " will not be matched to "CF"
code = cells[0].string.strip()
code = code + (4 - len(code)) * ' '
labels = [label for label in cells[1].stripped_strings]
registered_vendor_ids[code] = labels[0]
return registered_vendor_ids
@check(
id = 'com.google.fonts/check/018',
conditions = ['registered_vendor_ids']
)
def com_google_fonts_check_018(ttFont, registered_vendor_ids):
"""Checking OS/2 achVendID."""
SUGGEST_MICROSOFT_VENDORLIST_WEBSITE = (
" You should set it to your own 4 character code,"
" and register that code with Microsoft at"
" https://www.microsoft.com"
"/typography/links/vendorlist.aspx")
vid = ttFont['OS/2'].achVendID
bad_vids = ['UKWN', 'ukwn', 'PfEd']
if vid is None:
yield FAIL, Message("not set", "OS/2 VendorID is not set." +
SUGGEST_MICROSOFT_VENDORLIST_WEBSITE)
elif vid in bad_vids:
yield FAIL, Message("bad", ("OS/2 VendorID is '{}',"
" a font editor default.").format(vid) +
SUGGEST_MICROSOFT_VENDORLIST_WEBSITE)
elif vid not in registered_vendor_ids.keys():
yield WARN, Message("unknown", ("OS/2 VendorID value '{}' is not"
" a known registered id.").format(vid) +
SUGGEST_MICROSOFT_VENDORLIST_WEBSITE)
else:
yield PASS, f"OS/2 VendorID '{vid}' looks good!"
@check(
id = 'com.google.fonts/check/019'
)
def com_google_fonts_check_019(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, ("NAMEID #{} contains symbol that should be"
" replaced by '{}'.").format(name.nameID,
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/020',
conditions=['style']
)
def com_google_fonts_check_020(font, ttFont, style):
"""Checking OS/2 usWeightClass."""
from fontbakery.specifications.shared_conditions import is_ttf
weight_name, expected_value = expected_os2_weight(style)
value = ttFont['OS/2'].usWeightClass
if value != expected_value:
if is_ttf(ttFont) and \
(weight_name == 'Thin' and value == 100) or \
(weight_name == 'ExtraLight' and value == 200):
yield WARN, ("{}:{} is OK on TTFs, but OTF files with those values"
" will cause bluring on Windows."
" GlyphsApp users must set a Instance Custom Parameter"
" for the Thin and ExtraLight styles to 250 and 275,"
" so that if OTFs are exported then it will not"
" blur on Windows.")
else:
yield FAIL, ("OS/2 usWeightClass expected value for"
" '{}' is {} but this font has"
" {}.").format(weight_name, expected_value, value)
else:
yield PASS, "OS/2 usWeightClass value looks good!"
@condition
def licenses(family_directory):
"""Get a list of paths for every license
file found in a font project."""
licenses = []
if family_directory:
for license in ['OFL.txt', 'LICENSE.txt']:
license_path = os.path.join(family_directory, license)
if os.path.exists(license_path):
licenses.append(license_path)
return licenses
@condition
def license_path(licenses):
"""Get license path."""
# return license if there is exactly one license
return licenses[0] if len(licenses) == 1 else None
@condition
def license(license_path):
"""Get license filename."""
if license_path:
return os.path.basename(license_path)
@check(
id = 'com.google.fonts/check/028'
)
def com_google_fonts_check_028(licenses):
"""Check font has a license."""
if len(licenses) > 1:
yield FAIL, Message("multiple",
("More than a single license file found."
" Please review."))
elif not licenses:
yield FAIL, Message("none",
("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/029',
conditions = ['license'],
misc_metadata = {
'priority': CRITICAL
})
def com_google_fonts_check_029(ttFont, license):
"""Check copyright namerecords match license file."""
from fontbakery.constants import (NAMEID_LICENSE_DESCRIPTION,
# NAMEID_LICENSE_INFO_URL,
PLACEHOLDER_LICENSING_TEXT,
# NAMEID_STR,
PLATID_STR)
from unidecode import unidecode
failed = 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 value != placeholder:
failed = True
yield FAIL, Message("wrong", \
("License file {} exists but"
" NameID {} (LICENSE DESCRIPTION) value"
" on platform {} ({})"
" is not specified for that."
" Value was: \"{}\""
" Must be changed to \"{}\""
"").format(license,
NAMEID_LICENSE_DESCRIPTION,
nameRecord.platformID,
PLATID_STR[nameRecord.platformID],
unidecode(value),
unidecode(placeholder)))
if not entry_found:
yield FAIL, Message("missing", \
("Font lacks NameID {} "
"(LICENSE DESCRIPTION). A proper licensing entry"
" must be set.").format(NAMEID_LICENSE_DESCRIPTION))
elif not failed:
yield PASS, "Licensing entry on name table is correctly set."
@condition
def familyname(font):
filename = os.path.basename(font)
filename_base = os.path.splitext(filename)[0]
return filename_base.split('-')[0]
@check(
id = 'com.google.fonts/check/030',
conditions = ['familyname'],
misc_metadata = {
'priority': CRITICAL
}
)
def com_google_fonts_check_030(ttFont, familyname):
""""License URL matches License text on name table?"""
from fontbakery.constants import (NAMEID_LICENSE_DESCRIPTION,
NAMEID_LICENSE_INFO_URL,
PLACEHOLDER_LICENSING_TEXT)
LEGACY_UFL_FAMILIES = ["Ubuntu", "UbuntuCondensed", "UbuntuMono"]
LICENSE_URL = {
'OFL.txt': 'http://scripts.sil.org/OFL',
'LICENSE.txt': 'http://www.apache.org/licenses/LICENSE-2.0',
'UFL.txt': 'https://www.ubuntu.com/legal/terms-and-policies/font-licence'
}
LICENSE_NAME = {
'OFL.txt': 'Open Font',
'LICENSE.txt': 'Apache',
'UFL.txt': 'Ubuntu Font License'
}
detected_license = False
for license in ['OFL.txt', 'LICENSE.txt', 'UFL.txt']:
placeholder = PLACEHOLDER_LICENSING_TEXT[license]
for nameRecord in ttFont['name'].names:
string = nameRecord.string.decode(nameRecord.getEncoding())
if nameRecord.nameID == NAMEID_LICENSE_DESCRIPTION and\
string == placeholder:
detected_license = license
break
if detected_license == "UFL.txt" and familyname not in LEGACY_UFL_FAMILIES:
yield FAIL, Message("ufl",
("The Ubuntu Font License is only acceptable on"
" the Google Fonts collection for legacy font"
" families that already adopted such license."
" New Families should use eigther Apache or"
" Open Font License."))
else:
found_good_entry = False
if detected_license:
failed = False
expected = LICENSE_URL[detected_license]
for nameRecord in ttFont['name'].names:
if nameRecord.nameID == NAMEID_LICENSE_INFO_URL:
string = nameRecord.string.decode(nameRecord.getEncoding())
if string == expected:
found_good_entry = True
else:
failed = True
yield FAIL, Message("licensing-inconsistency",
("Licensing inconsistency in name table"
" entries! NameID={} (LICENSE DESCRIPTION)"
" indicates {} licensing, but NameID={}"
" (LICENSE URL) has '{}'. Expected: '{}'"
"").format(NAMEID_LICENSE_DESCRIPTION,
LICENSE_NAME[detected_license],
NAMEID_LICENSE_INFO_URL,
string, expected))
if not found_good_entry:
yield FAIL, Message("no-license-found",
("A known license URL must be provided in the"
" NameID {} (LICENSE INFO URL) entry."
" 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."
"").format(NAMEID_LICENSE_INFO_URL))
else:
if failed:
yield FAIL, Message("bad-entries",
("Even though a valid license URL was seen in"
" NAME table, there were also bad entries."
" Please review NameIDs {} (LICENSE DESCRIPTION)"
" and {} (LICENSE INFO URL)."
"").format(NAMEID_LICENSE_DESCRIPTION,
NAMEID_LICENSE_INFO_URL))
else:
yield PASS, "Font has a valid license URL in NAME table."
@check(
id = 'com.google.fonts/check/032',
rationale = """
An old FontLab version had a bug which caused it to store
copyright notices in nameID 10 entries.
In order to detect those and distinguish them from actual
legitimate usage of this name table entry, we expect that
such strings do not exceed a reasonable length of 200 chars.
Longer strings are likely instances of the FontLab bug.
"""
)
def com_google_fonts_check_032(ttFont):
"""Description strings in the name table must not exceed 200 characters."""
from fontbakery.constants import NAMEID_DESCRIPTION
failed = False
for name in ttFont['name'].names:
if (name.nameID == NAMEID_DESCRIPTION and
len(name.string.decode(name.getEncoding())) > 200):
failed = True
break
if failed:
yield WARN, ("A few name table entries with ID={} (NAMEID_DESCRIPTION)"
" are longer than 200 characters."
" Please check whether those entries are copyright notices"
" mistakenly stored in the description string entries by"
" a bug in an old FontLab version."
" If that's the case, then such copyright notices must be"
" removed from these entries."
"").format(NAMEID_DESCRIPTION)
else:
yield PASS, "All description name records have reasonably small lengths."
@condition
def ttfautohint_stats(font):
import re
import subprocess
import tempfile
try:
hinted_size = os.stat(font).st_size
dehinted = tempfile.NamedTemporaryFile(suffix=".ttf", delete=False)
subprocess.call(["ttfautohint",
"--dehint",
font,
dehinted.name])
dehinted_size = os.stat(dehinted.name).st_size
os.unlink(dehinted.name)
except OSError:
return {"missing": True}
ttfa_cmd = ["ttfautohint",
"-V"] # print version info
ttfa_output = subprocess.check_output(ttfa_cmd,
stderr=subprocess.STDOUT)
installed_ttfa = re.search(r'ttfautohint ([^-\n]*)(-.*)?\n',
ttfa_output.decode('utf-8')).group(1)
return {
"dehinted_size": dehinted_size,
"hinted_size": hinted_size,
"version": installed_ttfa
}
TTFAUTOHINT_MISSING_MSG = (
"ttfautohint is not available!"
" You really MUST check the fonts with this tool."
" To install it, see https://github.com"
"/googlefonts/gf-docs/blob/master"
"/ProjectChecklist.md#ttfautohint")
@check(
id = 'com.google.fonts/check/054',
conditions = ['ttfautohint_stats']
)
def com_google_fonts_check_054(font, ttfautohint_stats):
"""Show hinting filesize impact.
Current implementation simply logs useful info
but there's no fail scenario for this checker."""
if "missing" in ttfautohint_stats:
yield WARN, Message("ttfa-missing",
TTFAUTOHINT_MISSING_MSG)
return
if ttfautohint_stats["dehinted_size"] == 0:
yield WARN, Message("ttfa-bug",
("ttfautohint --dehint reports that"
" \"This font has already been processed"
" with ttfautohint\"."
" This is a bug in an old version of ttfautohint."
" You'll need to upgrade it."
" See https://github.com/googlefonts/fontbakery/"
"issues/1043#issuecomment-249035069"))
return
hinted = ttfautohint_stats["hinted_size"]
dehinted = ttfautohint_stats["dehinted_size"]
increase = hinted - dehinted
change = float(hinted)/dehinted - 1
def filesize_formatting(s):
if s < 1024:
return f"{s} bytes"
elif s < 1024*1024:
return "{:.1f}kb".format(s/1024)
else:
return "{:.1f}Mb".format(s/(1024*1024))
hinted_size = filesize_formatting(hinted)
dehinted_size = filesize_formatting(dehinted)
increase = filesize_formatting(increase)
results_table = "Hinting filesize impact:\n\n"
results_table += f"| | {font} |\n"
results_table += "|:--- | ---:|\n"
results_table += f"| Dehinted Size | {dehinted_size} |\n"
results_table += f"| Hinted Size | {hinted_size} |\n"
results_table += f"| Increase | {increase} |\n"
results_table += f"| Change | {change:.1f} % |\n"
yield INFO, results_table
@check(
id = 'com.google.fonts/check/055'
)
def com_google_fonts_check_055(ttFont):
"""Version format is correct in 'name' table?"""
from fontbakery.utils import get_name_entry_strings
from fontbakery.constants import NAMEID_VERSION_STRING
import re
def is_valid_version_format(value):
return re.match(r'Version\s0*[1-9]+\.\d+', value)
failed = False
version_entries = get_name_entry_strings(ttFont, NAMEID_VERSION_STRING)
if len(version_entries) == 0:
failed = True
yield FAIL, Message("no-version-string",
("Font lacks a NAMEID_VERSION_STRING (nameID={})"
" entry").format(NAMEID_VERSION_STRING))
for ventry in version_entries:
if not is_valid_version_format(ventry):
failed = True
yield FAIL, Message("bad-version-strings",
("The NAMEID_VERSION_STRING (nameID={}) value must"
" follow the pattern \"Version X.Y\" with X.Y"
" between 1.000 and 9.999."
" Current version string is:"
" \"{}\"").format(NAMEID_VERSION_STRING,
ventry))
if not failed:
yield PASS, "Version format in NAME table entries is correct."
@check(