Skip to content

Commit 4fe626b

Browse files
committed
Weighted average check
1 parent 0633a88 commit 4fe626b

File tree

2 files changed

+104
-2
lines changed

2 files changed

+104
-2
lines changed

profile-universal/src/checks/os2.rs

+100
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use read_fonts::{
33
tables::{head::MacStyle, os2::SelectionFlags},
44
TableProvider,
55
};
6+
use skrifa::{GlyphId, MetadataProvider};
67

78
#[check(
89
id = "opentype/fsselection",
@@ -160,3 +161,102 @@ fn check_vendor_id(f: &Testable, context: &Context) -> CheckFnResult {
160161
))
161162
}
162163
}
164+
165+
const AVG_CHAR_WEIGHTS: [(char, u32); 27] = [
166+
('a', 64),
167+
('b', 14),
168+
('c', 27),
169+
('d', 35),
170+
('e', 100),
171+
('f', 20),
172+
('g', 14),
173+
('h', 42),
174+
('i', 63),
175+
('j', 3),
176+
('k', 6),
177+
('l', 35),
178+
('m', 20),
179+
('n', 56),
180+
('o', 56),
181+
('p', 17),
182+
('q', 4),
183+
('r', 49),
184+
('s', 56),
185+
('t', 71),
186+
('u', 31),
187+
('v', 10),
188+
('w', 18),
189+
('x', 3),
190+
('y', 18),
191+
('z', 2),
192+
(' ', 166),
193+
];
194+
195+
#[check(
196+
id = "opentype/xavgcharwidth",
197+
proposal = "https://github.com/fonttools/fontbakery/issues/4829",
198+
title = "Checking OS/2 fsSelection value.",
199+
rationale = "
200+
The OS/2.xAvgCharWidth field is used to calculate the width of a string of
201+
characters. It is the average width of all non-zero width glyphs in the font.
202+
203+
This check ensures that the value is correct. A failure here may indicate
204+
a bug in the font compiler, rather than something that the designer can
205+
do anything about.
206+
"
207+
)]
208+
fn xavgcharwidth(f: &Testable, _context: &Context) -> CheckFnResult {
209+
let font = testfont!(f);
210+
let os2 = font.font().os2()?;
211+
let hmtx = font.font().hmtx()?;
212+
let charmap = font.font().charmap();
213+
let (rule, expected) = if os2.version() >= 3 {
214+
let advances = hmtx
215+
.h_metrics()
216+
.iter()
217+
.map(|metric| metric.advance.get() as u32)
218+
.filter(|&w| w > 0)
219+
.collect::<Vec<_>>();
220+
(
221+
"the average of the widths of all glyphs in the font",
222+
advances.iter().sum::<u32>() / advances.len() as u32,
223+
)
224+
} else {
225+
let ids: Vec<Option<GlyphId>> = AVG_CHAR_WEIGHTS
226+
.iter()
227+
.map(|(c, _)| charmap.map(*c))
228+
.collect();
229+
if ids.iter().any(|id| id.is_none()) {
230+
return Err(CheckError::Error(
231+
"Missing glyph in font for average character width calculation".to_string(),
232+
));
233+
}
234+
#[allow(clippy::unwrap_used)] // We know all the characters are in the font
235+
let advances = ids
236+
.iter()
237+
.zip(AVG_CHAR_WEIGHTS.iter())
238+
.map(|(id, (_, w))| hmtx.advance(id.unwrap()).unwrap_or(0) as u32 * w)
239+
.collect::<Vec<_>>();
240+
(
241+
"the weighted average of the widths of the latin lowercase glyphs in the font",
242+
advances.iter().sum::<u32>() / advances.len() as u32,
243+
)
244+
};
245+
let actual = os2.x_avg_char_width();
246+
let difference = (expected as i16 - actual).abs();
247+
Ok(match difference {
248+
0|1 => Status::just_one_pass(),
249+
2|3|4|5|6|7|8|9|20 => Status::just_one_info(
250+
"xAvgCharWidth-close",
251+
&format!("OS/2 xAvgCharWidth is {} but it should be {} which corresponds to {}. These are similar values, which may be a symptom of the slightly different calculation of the xAvgCharWidth value in font editors. There's further discussion on this at https://github.com/fonttools/fontbakery/issues/1622",
252+
actual, expected, rule
253+
)
254+
),
255+
_ => Status::just_one_warn(
256+
"xAvgCharWidth-wrong",
257+
&format!("OS/2 xAvgCharWidth is {} but it should be {} which corresponds to {}. This may indicate a problem with the font editor or the font compiler.",
258+
actual, expected, rule
259+
)
260+
)
261+
} )
262+
}

profile-universal/src/lib.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ impl fontspector_checkapi::Plugin for Universal {
3131
cr.register_check(checks::layout::layout_valid_language_tags);
3232
cr.register_check(checks::layout::layout_valid_script_tags);
3333
cr.register_check(checks::name::check_name_no_copyright_on_description);
34+
cr.register_check(checks::name::check_name_match_familyname_fullfont);
3435
cr.register_check(checks::post::post_table_version);
3536
cr.register_check(checks::post::underline_thickness);
3637
cr.register_check(checks::stat::stat_axis_record);
@@ -39,6 +40,7 @@ impl fontspector_checkapi::Plugin for Universal {
3940
cr.register_check(checks::os2::fsselection);
4041
cr.register_check(checks::os2::panose_familytype);
4142
cr.register_check(checks::os2::check_vendor_id);
43+
cr.register_check(checks::os2::xavgcharwidth);
4244

4345
let opentype_profile = Profile::from_toml(
4446
r#"
@@ -76,6 +78,8 @@ impl fontspector_checkapi::Plugin for Universal {
7678
"opentype/weight_class_fvar",
7779
"opentype/vendor_id",
7880
"opentype/name/match_familyname_fullfont",
81+
"opentype/varfont/valid_default_instance_nameids",
82+
"opentype/xavgcharwidth",
7983
8084
# Checks left to port
8185
"opentype/cff2_call_depth",
@@ -120,8 +124,6 @@ impl fontspector_checkapi::Plugin for Universal {
120124
"opentype/name/postscript_vs_cff",
121125
"opentype/postscript_name",
122126
"opentype/slant_direction",
123-
"opentype/varfont/valid_default_instance_nameids",
124-
"opentype/xavgcharwidth",
125127
]
126128
"#,
127129
)

0 commit comments

Comments
 (0)