Skip to content

Commit f11d58a

Browse files
committed
Port another seven opentype checks
1 parent c4cff3b commit f11d58a

File tree

3 files changed

+266
-7
lines changed

3 files changed

+266
-7
lines changed

fontspector-checkapi/src/font.rs

+10
Original file line numberDiff line numberDiff line change
@@ -218,4 +218,14 @@ impl TestFont<'_> {
218218
(instance_name, coords.collect())
219219
})
220220
}
221+
222+
pub fn axis_ranges(&self) -> impl Iterator<Item = (String, f32, f32, f32)> + '_ {
223+
self.font().axes().iter().map(|axis| {
224+
let tag = axis.tag().to_string();
225+
let min = axis.min_value();
226+
let max = axis.max_value();
227+
let def = axis.default_value();
228+
(tag, min, def, max)
229+
})
230+
}
221231
}

profile-universal/src/checks/fvar.rs

+243
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::collections::HashSet;
2+
3+
use font_types::NameId;
14
use fontspector_checkapi::{prelude::*, skip, testfont, FileTypeConvert};
25
use skrifa::MetadataProvider;
36

@@ -8,6 +11,8 @@ const REGULAR_COORDINATE_EXPECTATIONS: [(&str, f32); 4] = [
811
("ital", 0.0),
912
];
1013

14+
const REGISTERED_AXIS_TAGS: [&str; 5] = ["ital", "opsz", "slnt", "wdth", "wght"];
15+
1116
#[check(
1217
id = "opentype/fvar/regular_coords_correct",
1318
title = "Axes and named instances fall within correct ranges?",
@@ -137,3 +142,241 @@ fn axis_ranges_correct(t: &Testable, _context: &Context) -> CheckFnResult {
137142
}
138143
return_result(problems)
139144
}
145+
146+
#[check(
147+
id = "opentype/varfont/distinct_instance_records",
148+
title = "Validates that all of the instance records in a given font have distinct data",
149+
rationale = "According to the 'fvar' documentation in OpenType spec v1.9
150+
https://docs.microsoft.com/en-us/typography/opentype/spec/fvar
151+
152+
All of the instance records in a font should have distinct coordinates
153+
and distinct subfamilyNameID and postScriptName ID values. If two or more
154+
records share the same coordinates, the same nameID values or the same
155+
postScriptNameID values, then all but the first can be ignored.",
156+
proposal = "https://github.com/fonttools/fontbakery/issues/3706"
157+
)]
158+
fn distinct_instance_records(t: &Testable, _context: &Context) -> CheckFnResult {
159+
let f = testfont!(t);
160+
skip!(!f.is_variable_font(), "not-variable", "Not a variable font");
161+
162+
let mut problems = vec![];
163+
let mut unique_records = HashSet::new();
164+
// We want to get at subfamily and postscript name IDs, so we use the lower-level
165+
// Skrifa API here.
166+
for instance in f.font().named_instances().iter() {
167+
let loc = instance.location();
168+
let coords: Vec<_> = loc.coords().to_vec();
169+
let subfamily_name_id = instance.subfamily_name_id();
170+
let postscript_name_id = instance.postscript_name_id();
171+
let instance_data = (coords.clone(), subfamily_name_id, postscript_name_id);
172+
if unique_records.contains(&instance_data) {
173+
let subfamily = f
174+
.get_name_entry_strings(subfamily_name_id)
175+
.next()
176+
.unwrap_or_else(|| format!("ID {}", subfamily_name_id));
177+
problems.push(Status::warn(
178+
"duplicate-instance",
179+
&format!(
180+
"Instance {} with coordinates {:?} is duplicated",
181+
subfamily, coords
182+
),
183+
));
184+
} else {
185+
unique_records.insert(instance_data);
186+
}
187+
}
188+
return_result(problems)
189+
}
190+
191+
#[check(
192+
id = "opentype/varfont/family_axis_ranges",
193+
title = "Check that family axis ranges are identical",
194+
rationale = "Between members of a family (such as Roman & Italic), the ranges of variable axes must be identical.",
195+
proposal = "https://github.com/fonttools/fontbakery/issues/4445",
196+
implementation = "all"
197+
)]
198+
fn family_axis_ranges(c: &TestableCollection, context: &Context) -> CheckFnResult {
199+
let mut fonts = TTF.from_collection(c);
200+
fonts.retain(|f| f.is_variable_font());
201+
skip!(
202+
fonts.len() < 2,
203+
"not-enough-fonts",
204+
"Not enough variable fonts to compare"
205+
);
206+
let values: Vec<_> = fonts
207+
.iter()
208+
.map(|f| {
209+
let label = f
210+
.filename
211+
.file_name()
212+
.map(|x| x.to_string_lossy())
213+
.map(|x| x.to_string())
214+
.unwrap_or("Unknown file".to_string());
215+
let comparable = f
216+
.axis_ranges()
217+
.map(|(ax, min, def, max)| format!("{}={:.2}:{:.2}:{:.2}", ax, min, def, max))
218+
.collect::<Vec<String>>()
219+
.join(", ");
220+
(comparable.clone(), comparable, label)
221+
})
222+
.collect();
223+
assert_all_the_same(
224+
context,
225+
&values,
226+
"axis-range-mismatch",
227+
"Variable axis ranges not matching between font files",
228+
)
229+
}
230+
231+
#[check(
232+
id = "opentype/varfont/foundry_defined_tag_name",
233+
title = "Validate foundry-defined design-variation axis tag names.",
234+
rationale = "According to the OpenType spec's syntactic requirements for
235+
foundry-defined design-variation axis tags available at
236+
https://learn.microsoft.com/en-us/typography/opentype/spec/dvaraxisreg
237+
238+
Foundry-defined tags must begin with an uppercase letter
239+
and must use only uppercase letters or digits.",
240+
proposal = "https://github.com/fonttools/fontbakery/issues/4043"
241+
)]
242+
fn varfont_foundry_defined_tag_name(t: &Testable, _context: &Context) -> CheckFnResult {
243+
let f = testfont!(t);
244+
skip!(!f.is_variable_font(), "not-variable", "Not a variable font");
245+
let mut problems = vec![];
246+
for axis in f.font().axes().iter() {
247+
let tag = axis.tag().to_string();
248+
if REGISTERED_AXIS_TAGS.contains(&tag.as_str()) {
249+
continue;
250+
}
251+
if REGISTERED_AXIS_TAGS.contains(&tag.to_lowercase().as_str()) {
252+
problems.push(Status::warn("foundry-defined-similar-registered-name",
253+
&format!("Foundry-defined axis tag {} is similar to a registered tag name {}, consider renaming. If this tag was meant to be a registered tag, please use all lowercase letters in the tag name.", tag, tag.to_lowercase())
254+
));
255+
}
256+
// Axis tag must be uppercase and contain only uppercase letters or digits
257+
if !tag
258+
.chars()
259+
.next()
260+
.map(|c| c.is_ascii_uppercase())
261+
.unwrap_or(false)
262+
{
263+
problems.push(Status::fail(
264+
"invalid-foundry-defined-tag-first-letter",
265+
&format!(
266+
"Foundry-defined axis tag {} must begin with an uppercase letter",
267+
tag
268+
),
269+
))
270+
} else if !tag
271+
.chars()
272+
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
273+
{
274+
problems.push(Status::fail("invalid-foundry-defined-tag-chars",
275+
&format!("Foundry-defined axis tag {} must begin with an uppercase letter and contain only uppercase letters or digits.", tag)
276+
));
277+
}
278+
}
279+
return_result(problems)
280+
}
281+
282+
#[check(
283+
id = "opentype/varfont/same_size_instance_records",
284+
title = "Validates that all of the instance records in a given font have the same size",
285+
rationale = "According to the 'fvar' documentation in OpenType spec v1.9
286+
https://docs.microsoft.com/en-us/typography/opentype/spec/fvar
287+
288+
All of the instance records in a given font must be the same size, with
289+
all either including or omitting the postScriptNameID field. [...]
290+
If the value is 0xFFFF, then the value is ignored, and no PostScript name
291+
equivalent is provided for the instance.",
292+
proposal = "https://github.com/fonttools/fontbakery/issues/3705"
293+
)]
294+
fn same_size_instance_records(t: &Testable, _context: &Context) -> CheckFnResult {
295+
let f = testfont!(t);
296+
skip!(!f.is_variable_font(), "not-variable", "Not a variable font");
297+
let has_a_postscriptname: HashSet<bool> = f
298+
.font()
299+
.named_instances()
300+
.iter()
301+
.map(|ni| ni.postscript_name_id().is_none())
302+
.collect();
303+
Ok(if has_a_postscriptname.len() > 1 {
304+
Status::just_one_fail(
305+
"different-size-instance-records",
306+
"Instance records don't all have the same size.",
307+
)
308+
} else {
309+
Status::just_one_pass()
310+
})
311+
}
312+
313+
#[check(
314+
id = "opentype/varfont/valid_nameids",
315+
title = "Validates that all of the name IDs in an instance record are within the correct range",
316+
rationale = r#"
317+
According to the 'fvar' documentation in OpenType spec v1.9
318+
https://docs.microsoft.com/en-us/typography/opentype/spec/fvar
319+
320+
The axisNameID field provides a name ID that can be used to obtain strings
321+
from the 'name' table that can be used to refer to the axis in application
322+
user interfaces. The name ID must be greater than 255 and less than 32768.
323+
324+
The postScriptNameID field provides a name ID that can be used to obtain
325+
strings from the 'name' table that can be treated as equivalent to name
326+
ID 6 (PostScript name) strings for the given instance. Values of 6 and
327+
"undefined" can be used; otherwise, values must be greater than 255 and
328+
less than 32768.
329+
330+
The subfamilyNameID field provides a name ID that can be used to obtain
331+
strings from the 'name' table that can be treated as equivalent to name
332+
ID 17 (typographic subfamily) strings for the given instance. Values of
333+
2 or 17 can be used; otherwise, values must be greater than 255 and less
334+
than 32768.
335+
"#,
336+
proposal = "https://github.com/fonttools/fontbakery/issues/3703"
337+
)]
338+
fn varfont_valid_nameids(t: &Testable, _context: &Context) -> CheckFnResult {
339+
let f = testfont!(t);
340+
skip!(!f.is_variable_font(), "not-variable", "Not a variable font");
341+
let mut problems = vec![];
342+
let valid_nameid = |n: NameId| (255..32768).contains(&n.to_u16());
343+
344+
// Do the axes first
345+
for axis in f.font().axes().iter() {
346+
let axis_name_id = axis.name_id();
347+
if !valid_nameid(axis_name_id) {
348+
problems.push(Status::fail(
349+
"invalid-axis-name-id",
350+
&format!(
351+
"Axis name ID {} ({}) is out of range. It must be greater than 255 and less than 32768.",
352+
axis_name_id, f.get_name_entry_strings(axis_name_id).next().unwrap_or_default()
353+
),
354+
));
355+
}
356+
}
357+
358+
for instance in f.font().named_instances().iter() {
359+
let subfamily_name_id = instance.subfamily_name_id();
360+
if let Some(n) = instance.postscript_name_id() {
361+
if n != NameId::new(6) && !valid_nameid(n) {
362+
problems.push(Status::fail(
363+
"invalid-postscript-name-id",
364+
&format!(
365+
"PostScript name ID {} ({}) is out of range. It must be greater than 255 and less than 32768, or 6 or 0xFFFF.",
366+
n, f.get_name_entry_strings(n).next().unwrap_or_default()
367+
),
368+
));
369+
}
370+
}
371+
if !valid_nameid(subfamily_name_id) {
372+
problems.push(Status::fail(
373+
"invalid-subfamily-name-id",
374+
&format!(
375+
"Instance subfamily name ID {} ({}) is out of range. It must be greater than 255 and less than 32768.",
376+
subfamily_name_id, f.get_name_entry_strings(subfamily_name_id).next().unwrap_or_default()
377+
),
378+
));
379+
}
380+
}
381+
return_result(problems)
382+
}

profile-universal/src/lib.rs

+13-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ impl fontspector_checkapi::Plugin for Universal {
1010
cr.register_check(checks::bold_italic_unique::bold_italic_unique);
1111
cr.register_check(checks::code_pages::code_pages);
1212
cr.register_check(checks::fvar::axis_ranges_correct);
13+
cr.register_check(checks::fvar::distinct_instance_records);
14+
cr.register_check(checks::fvar::family_axis_ranges);
1315
cr.register_check(checks::fvar::regular_coords_correct);
16+
cr.register_check(checks::fvar::same_size_instance_records);
17+
cr.register_check(checks::fvar::varfont_foundry_defined_tag_name);
18+
cr.register_check(checks::fvar::varfont_valid_nameids);
1419
cr.register_check(checks::glyf::glyf_unused_data);
1520
cr.register_check(checks::glyf::check_point_out_of_bounds);
1621
cr.register_check(checks::glyf::check_glyf_non_transformed_duplicate_components);
@@ -47,6 +52,11 @@ impl fontspector_checkapi::Plugin for Universal {
4752
"opentype/points_out_of_bounds",
4853
"opentype/glyf_non_transformed_duplicate_components",
4954
"opentype/name/no_copyright_on_description",
55+
"opentype/varfont/distinct_instance_records",
56+
"opentype/varfont/family_axis_ranges",
57+
"opentype/varfont/foundry_defined_tag_name",
58+
"opentype/varfont/same_size_instance_records",
59+
"opentype/varfont/valid_nameids",
5060
5161
# Checks left to port
5262
"opentype/cff2_call_depth",
@@ -70,6 +80,9 @@ impl fontspector_checkapi::Plugin for Universal {
7080
# "opentype/varfont/regular_wdth_coord",
7181
# "opentype/varfont/regular_wght_coord",
7282
# "opentype/fsselection_matches_macstyle", (merged into opentype/fsselection)
83+
# "opentype/varfont/valid_axis_nameid", (merged into opentype/varfont/valid_nameids)
84+
# "opentype/varfont/valid_postscript_nameid", (above)
85+
# "opentype/varfont/valid_subfamily_nameid", (above)
7386
7487
# Checks I haven't got around to classifying yet
7588
"opentype/gdef_mark_chars",
@@ -93,14 +106,7 @@ impl fontspector_checkapi::Plugin for Universal {
93106
"opentype/postscript_name",
94107
"opentype/slant_direction",
95108
"opentype/stat_has_axis_value_tables",
96-
"opentype/varfont/distinct_instance_records",
97-
"opentype/varfont/family_axis_ranges",
98-
"opentype/varfont/foundry_defined_tag_name",
99-
"opentype/varfont/same_size_instance_records",
100-
"opentype/varfont/valid_axis_nameid",
101109
"opentype/varfont/valid_default_instance_nameids",
102-
"opentype/varfont/valid_postscript_nameid",
103-
"opentype/varfont/valid_subfamily_nameid",
104110
"opentype/vendor_id",
105111
"opentype/weight_class_fvar",
106112
"opentype/xavgcharwidth",

0 commit comments

Comments
 (0)