Skip to content

Commit 9512dfd

Browse files
committed
Several more checks
1 parent 35db31f commit 9512dfd

File tree

7 files changed

+432
-7
lines changed

7 files changed

+432
-7
lines changed

profile-universal/Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ read-fonts = {workspace = true}
1414
write-fonts = {workspace = true}
1515
font-types = {workspace = true}
1616
skrifa = {workspace = true}
17-
itertools = "0.13.0"
17+
itertools = "0.13.0"
18+
unicode-properties = "0.1.3"

profile-universal/src/checks/gdef.rs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use fontspector_checkapi::{constants::GlyphClass, prelude::*, testfont, FileTypeConvert};
2+
use read_fonts::TableProvider;
3+
use skrifa::MetadataProvider;
4+
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
5+
6+
fn is_nonspacing_mark(c: char) -> bool {
7+
matches!(
8+
c.general_category(),
9+
GeneralCategory::NonspacingMark | GeneralCategory::EnclosingMark
10+
)
11+
}
12+
13+
#[check(
14+
id = "opentype/gdef_spacing_marks",
15+
rationale = "
16+
Glyphs in the GDEF mark glyph class should be non-spacing.
17+
18+
Spacing glyphs in the GDEF mark glyph class may have incorrect anchor
19+
positioning that was only intended for building composite glyphs during design.
20+
",
21+
proposal = "https://github.com/fonttools/fontbakery/issues/2877",
22+
title = "Check glyphs in mark glyph class are non-spacing."
23+
)]
24+
fn gdef_spacing_marks(f: &Testable, context: &Context) -> CheckFnResult {
25+
let font = testfont!(f);
26+
let hmtx = font.font().hmtx()?;
27+
let gdef = font
28+
.font()
29+
.gdef()
30+
.map_err(|_| CheckError::skip("no-gdef", "GDEF table unreadable or not present"))?;
31+
let glyph_classdef = gdef.glyph_class_def().ok_or_else(|| {
32+
CheckError::skip("no-glyph-class-def", "GDEF table has no GlyphClassDef")
33+
})??;
34+
let nonspacing_mark_glyphs = bullet_list(
35+
context,
36+
glyph_classdef
37+
.iter()
38+
.filter(|(glyph, class)| *class == 3 && hmtx.advance(*glyph).unwrap_or(0) > 0)
39+
.map(|(glyph, _)| {
40+
#[allow(clippy::unwrap_used)] // synthesis=true means this is infallible
41+
font.glyph_name_for_id(glyph, true).unwrap()
42+
}),
43+
);
44+
if !nonspacing_mark_glyphs.is_empty() {
45+
return Ok(Status::just_one_fail("spacing-mark-glyphs", &format!(
46+
"The following glyphs seem to be spacing (because they have width > 0 on the hmtx table) so they may be in the GDEF mark glyph class by mistake, or they should have zero width instead:\n{}",
47+
nonspacing_mark_glyphs
48+
)));
49+
}
50+
51+
Ok(Status::just_one_pass())
52+
}
53+
54+
#[check(
55+
id = "opentype/gdef_mark_chars",
56+
rationale = "Mark characters should be in the GDEF mark glyph class.",
57+
proposal = "https://github.com/fonttools/fontbakery/issues/2877",
58+
title = "Check mark characters are in GDEF mark glyph class."
59+
)]
60+
fn gdef_mark_chars(f: &Testable, context: &Context) -> CheckFnResult {
61+
let font = testfont!(f);
62+
let mark_chars_not_in_gdef_mark = bullet_list(
63+
context,
64+
font.font()
65+
.charmap()
66+
.mappings()
67+
.filter(|(u, gid)| {
68+
char::from_u32(*u).map_or(false, is_nonspacing_mark)
69+
&& font.gdef_class(*gid) != Some(GlyphClass::Mark)
70+
})
71+
.map(|(u, gid)| {
72+
#[allow(clippy::unwrap_used)] // synthesis=true means this is infallible
73+
let name = font.glyph_name_for_id(gid, true).unwrap();
74+
format!("U+{:04X} ({})", u, name)
75+
}),
76+
);
77+
if !mark_chars_not_in_gdef_mark.is_empty() {
78+
return Ok(Status::just_one_fail(
79+
"mark-chars",
80+
&format!(
81+
"The following mark characters should be in the GDEF mark glyph class:\n{}",
82+
mark_chars_not_in_gdef_mark
83+
),
84+
));
85+
}
86+
87+
Ok(Status::just_one_pass())
88+
}

profile-universal/src/checks/kern.rs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Fontations doesn't yet support kern table
2+
3+
// use fontspector_checkapi::{prelude::*, skip, testfont, FileTypeConvert};
4+
// use read_fonts::TableProvider;
5+
6+
// #[check(
7+
// id = "opentype/kern_table",
8+
// rationale = "
9+
// Even though all fonts should have their kerning implemented in the GPOS table,
10+
// there may be kerning info at the kern table as well.
11+
12+
// Some applications such as MS PowerPoint require kerning info on the kern table.
13+
// More specifically, they require a format 0 kern subtable from a kern table
14+
// version 0 with only glyphs defined in the cmap table, which is the only one
15+
// that Windows understands (and which is also the simplest and more limited
16+
// of all the kern subtables).
17+
18+
// Google Fonts ingests fonts made for download and use on desktops, and does
19+
// all web font optimizations in the serving pipeline (using libre libraries
20+
// that anyone can replicate.)
21+
22+
// Ideally, TTFs intended for desktop users (and thus the ones intended for
23+
// Google Fonts) should have both KERN and GPOS tables.
24+
25+
// Given all of the above, we currently treat kerning on a v0 kern table
26+
// as a good-to-have (but optional) feature.
27+
// ",
28+
// proposal = "https://github.com/fonttools/fontbakery/issues/1675",
29+
// title = "Is there a usable 'kern' table declared in the font?"
30+
// )]
31+
// fn kern_table(f: &Testable, _context: &Context) -> CheckFnResult {
32+
// let font = testfont!(f);
33+
// if !font.has_table(b"kern") {
34+
// return Ok(Status::just_one_pass());
35+
// }
36+
// let kern = font.font().kern().ok();
37+
// Ok(Status::just_one_pass())
38+
// }

profile-universal/src/checks/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ pub mod arabic_spacing_symbols;
22
pub mod bold_italic_unique;
33
pub mod code_pages;
44
pub mod fvar;
5+
pub mod gdef;
56
pub mod glyf;
67
pub mod glyphnames;
78
pub mod head;
89
pub mod hhea;
10+
pub mod kern;
911
pub mod layout;
1012
pub mod name;
1113
pub mod name_trailing_spaces;

profile-universal/src/checks/name.rs

+77
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::collections::HashMap;
33
use font_types::NameId;
44
use fontspector_checkapi::{prelude::*, skip, testfont, FileTypeConvert};
55
use read_fonts::TableProvider;
6+
use skrifa::MetadataProvider;
67

78
#[check(
89
id = "opentype/name/empty_records",
@@ -283,6 +284,82 @@ fn name_italic_names(t: &Testable, _context: &Context) -> CheckFnResult {
283284
return_result(problems)
284285
}
285286

287+
#[check(
288+
id = "opentype/family/consistent_family_name",
289+
rationale = r#"
290+
Per the OpenType spec:
291+
292+
* "...many existing applications that use this pair of names assume that a
293+
Font Family name is shared by at most four fonts that form a font
294+
style-linking group"
295+
296+
* "For extended typographic families that includes fonts other than the
297+
four basic styles(regular, italic, bold, bold italic), it is strongly
298+
recommended that name IDs 16 and 17 be used in fonts to create an
299+
extended, typographic grouping."
300+
301+
* "If name ID 16 is absent, then name ID 1 is considered to be the
302+
typographic family name."
303+
304+
https://learn.microsoft.com/en-us/typography/opentype/spec/name
305+
306+
Fonts within a font family all must have consistent names
307+
in the Typographic Family name (nameID 16)
308+
or Font Family name (nameID 1), depending on which it uses.
309+
310+
Inconsistent font/typographic family names across fonts in a family
311+
can result in unexpected behaviors, such as broken style linking.
312+
"#,
313+
proposal = "https://github.com/fonttools/fontbakery/issues/4112",
314+
title = "Verify that family names in the name table are consistent across all fonts in the family. Checks Typographic Family name (nameID 16) if present, otherwise uses Font Family name (nameID 1)",
315+
implementation = "all"
316+
)]
317+
fn consistent_family_name(c: &TestableCollection, context: &Context) -> CheckFnResult {
318+
let fonts = TTF.from_collection(c);
319+
let mut problems = vec![];
320+
let mut family_names = HashMap::new();
321+
for font in fonts {
322+
let family_name = font
323+
.font()
324+
.localized_strings(NameId::TYPOGRAPHIC_FAMILY_NAME)
325+
.english_or_first()
326+
.or_else(|| {
327+
font.font()
328+
.localized_strings(NameId::FAMILY_NAME)
329+
.english_or_first()
330+
})
331+
.ok_or_else(|| {
332+
CheckError::Error(format!(
333+
"Font {} is missing a Family Name entry",
334+
font.filename.to_string_lossy()
335+
))
336+
})?
337+
.chars()
338+
.collect::<String>();
339+
family_names
340+
.entry(family_name)
341+
.or_insert_with(Vec::new)
342+
.push(font.filename.to_string_lossy().to_string());
343+
}
344+
if family_names.len() > 1 {
345+
let report = bullet_list(
346+
context,
347+
family_names
348+
.iter()
349+
.map(|(name, fonts)| format!("{} (found in fonts {})", name, fonts.join(", "))),
350+
);
351+
problems.push(Status::fail(
352+
"inconsistent-family-name",
353+
&format!(
354+
"{} different family names were found:\n\n{}",
355+
family_names.len(),
356+
report
357+
),
358+
));
359+
}
360+
return_result(problems)
361+
}
362+
286363
#[cfg(test)]
287364
#[allow(clippy::unwrap_used)]
288365
mod tests {

0 commit comments

Comments
 (0)