Skip to content

Commit 57a2274

Browse files
authored
Merge pull request #15 from felipesanches/issue_14
Split profiles Universal and OpenType
2 parents 9512dfd + 72550af commit 57a2274

24 files changed

+504
-182
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ members = [
55
"fontspector-cli",
66
"fontspector-checkapi",
77
"fontspector-checkhelper",
8+
"profile-opentype",
89
"profile-universal",
910
"profile-testplugin",
1011
"profile-googlefonts",

fontspector-cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ fontspector-checkapi = { path = "../fontspector-checkapi" }
1212
# These profiles are baked-in
1313
fontbakery-bridge = { path = "../fontbakery-bridge" }
1414
profile-universal = { path = "../profile-universal" }
15+
profile-opentype = { path = "../profile-opentype" }
1516
profile-googlefonts = { path = "../profile-googlefonts" }
1617
pluginator = { workspace = true }
1718
clap = { version = "3.2.5", features = ["derive"] }

profile-opentype/Cargo.toml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "profile-opentype"
3+
version = "0.1.0"
4+
edition = "2021"
5+
authors = [
6+
"Simon Cozens <[email protected]>",
7+
"Felipe Sanches <[email protected]>",
8+
]
9+
10+
[dependencies]
11+
fontspector-checkapi = { path = "../fontspector-checkapi" }
12+
fontspector-checkhelper = { workspace = true }
13+
read-fonts = {workspace = true}
14+
write-fonts = {workspace = true}
15+
font-types = {workspace = true}
16+
skrifa = {workspace = true}
17+
itertools = "0.13.0"
18+
unicode-properties = "0.1.3"
File renamed without changes.
File renamed without changes.
File renamed without changes.

profile-universal/src/checks/head.rs profile-opentype/src/checks/head.rs

+29-29
Original file line numberDiff line numberDiff line change
@@ -114,35 +114,6 @@ fn mac_style(f: &Testable, _context: &Context) -> CheckFnResult {
114114
return_result(problems)
115115
}
116116

117-
#[check(
118-
id = "opentype/family/equal_font_versions",
119-
title = "Make sure all font files have the same version value.",
120-
rationale = "Within a family released at the same time, all members of the family should have the same version number in the head table.",
121-
proposal = "legacy:check/014",
122-
implementation = "all"
123-
)]
124-
fn equal_font_versions(c: &TestableCollection, context: &Context) -> CheckFnResult {
125-
let fonts = TTF.from_collection(c);
126-
let versions_names: Result<Vec<_>, ReadError> = fonts
127-
.iter()
128-
.map(|f| {
129-
f.font().head().map(|head| {
130-
(
131-
head.font_revision(),
132-
format!("{:.03}", head.font_revision().to_f32()),
133-
f.filename.to_string_lossy(),
134-
)
135-
})
136-
})
137-
.collect();
138-
assert_all_the_same(
139-
context,
140-
&versions_names?,
141-
"mismatch",
142-
"Version info differs among font files of the same font project.",
143-
)
144-
}
145-
146117
#[check(
147118
id = "opentype/unitsperem",
148119
proposal = "legacy:check/043",
@@ -180,3 +151,32 @@ fn unitsperem(f: &Testable, _context: &Context) -> CheckFnResult {
180151
)),
181152
}
182153
}
154+
155+
#[check(
156+
id = "opentype/family/equal_font_versions",
157+
title = "Make sure all font files have the same version value.",
158+
rationale = "Within a family released at the same time, all members of the family should have the same version number in the head table.",
159+
proposal = "legacy:check/014",
160+
implementation = "all"
161+
)]
162+
fn equal_font_versions(c: &TestableCollection, context: &Context) -> CheckFnResult {
163+
let fonts = TTF.from_collection(c);
164+
let versions_names: Result<Vec<_>, ReadError> = fonts
165+
.iter()
166+
.map(|f| {
167+
f.font().head().map(|head| {
168+
(
169+
head.font_revision(),
170+
format!("{:.03}", head.font_revision().to_f32()),
171+
f.filename.to_string_lossy(),
172+
)
173+
})
174+
})
175+
.collect();
176+
assert_all_the_same(
177+
context,
178+
&versions_names?,
179+
"mismatch",
180+
"Version info differs among font files of the same font project.",
181+
)
182+
}

profile-opentype/src/checks/hhea.rs

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
use fontspector_checkapi::{prelude::*, testfont, FileTypeConvert};
2+
use read_fonts::TableProvider;
3+
4+
#[check(
5+
id = "opentype/maxadvancewidth",
6+
title = "MaxAdvanceWidth is consistent with values in the Hmtx and Hhea tables?",
7+
rationale = "The 'hhea' table contains a field which specifies the maximum advance width. This value should be consistent with the maximum advance width of all glyphs specified in the 'hmtx' table.",
8+
proposal = "legacy:check/073"
9+
)]
10+
fn maxadvancewidth(t: &Testable, _context: &Context) -> CheckFnResult {
11+
let f = testfont!(t);
12+
let hhea_advance_width_max = f.font().hhea()?.advance_width_max().to_u16();
13+
let hmtx_advance_width_max = f
14+
.font()
15+
.hmtx()?
16+
.h_metrics()
17+
.iter()
18+
.map(|m| m.advance.get())
19+
.max()
20+
.unwrap_or_default();
21+
Ok(if hmtx_advance_width_max != hhea_advance_width_max {
22+
Status::just_one_fail(
23+
"mismatch",
24+
&format!(
25+
"AdvanceWidthMax mismatch: expected {} from hmtx; got {} for hhea",
26+
hmtx_advance_width_max, hhea_advance_width_max
27+
),
28+
)
29+
} else {
30+
Status::just_one_pass()
31+
})
32+
}
33+
34+
#[check(
35+
id = "opentype/caret_slope",
36+
title = "Check hhea.caretSlopeRise and hhea.caretSlopeRun",
37+
proposal = "https://github.com/fonttools/fontbakery/issues/3670",
38+
rationale = r#"
39+
Checks whether hhea.caretSlopeRise and hhea.caretSlopeRun
40+
match with post.italicAngle.
41+
42+
For Upright fonts, you can set hhea.caretSlopeRise to 1
43+
and hhea.caretSlopeRun to 0.
44+
45+
For Italic fonts, you can set hhea.caretSlopeRise to head.unitsPerEm
46+
and calculate hhea.caretSlopeRun like this:
47+
round(math.tan(
48+
math.radians(-1 * font["post"].italicAngle)) * font["head"].unitsPerEm)
49+
50+
This check allows for a 0.1° rounding difference between the Italic angle
51+
as calculated by the caret slope and post.italicAngle
52+
"#
53+
)]
54+
fn caret_slope(t: &Testable, _context: &Context) -> CheckFnResult {
55+
let f = testfont!(t);
56+
let post_italic_angle = f.font().post()?.italic_angle().to_f32();
57+
let upem = f.font().head()?.units_per_em();
58+
let run = f.font().hhea()?.caret_slope_run();
59+
let rise = f.font().hhea()?.caret_slope_rise();
60+
if rise == 0 {
61+
return Ok(Status::just_one_fail(
62+
"zero-rise",
63+
"caretSlopeRise must not be zero. Set it to 1 for upright fonts.",
64+
));
65+
}
66+
let hhea_angle = (-run as f32 / rise as f32).atan().to_degrees();
67+
let expected_run = (-post_italic_angle.to_radians().tan() * upem as f32).round() as i16;
68+
let expected_rise = if expected_run == 0 { 1 } else { upem };
69+
if (post_italic_angle - hhea_angle).abs() > 0.1 {
70+
return Ok(Status::just_one_warn(
71+
"mismatch",
72+
&format!(
73+
"hhea.caretSlopeRise and hhea.caretSlopeRun do not match with post.italicAngle.
74+
Got caretSlopeRise: {}, caretSlopeRun: {}, expected caretSlopeRise: {}, caretSlopeRun: {}",
75+
rise, run, expected_rise, expected_run
76+
),
77+
));
78+
}
79+
Ok(Status::just_one_pass())
80+
}
File renamed without changes.
File renamed without changes.

profile-opentype/src/checks/mod.rs

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
pub mod fvar;
2+
pub mod bold_italic_unique;
3+
pub mod code_pages;
4+
pub mod gdef;
5+
pub mod glyf;
6+
pub mod head;
7+
pub mod hhea;
8+
pub mod kern;
9+
pub mod layout;
10+
pub mod name;
11+
pub mod os2;
12+
pub mod post;
13+
pub mod stat;
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use fontspector_checkapi::{prelude::*, testfont, FileTypeConvert};
2+
use read_fonts::TableProvider;
3+
4+
#[check(
5+
id = "name/trailing_spaces",
6+
title = "Name table records must not have trailing spaces.",
7+
rationale = "This check ensures that no entries in the name table end in spaces;
8+
trailing spaces, particularly in font names, can be confusing to users.
9+
In most cases this can be fixed by removing trailing spaces from the
10+
metadata fields in the font editor.",
11+
proposal = "https://github.com/googlefonts/fontbakery/issues/2417",
12+
hotfix = fix_trailing_spaces
13+
)]
14+
fn name_trailing_spaces(f: &Testable, _context: &Context) -> CheckFnResult {
15+
let mut problems: Vec<Status> = vec![];
16+
17+
if let Ok(name_table) = testfont!(f).font().name() {
18+
for name_record in name_table.name_record().iter() {
19+
if name_record
20+
.string(name_table.string_data())
21+
.map(|s| s.to_string())
22+
.map(|s| s.trim_end() != s)
23+
.unwrap_or(false)
24+
{
25+
problems.push(Status::fail("trailing-space",&format!(
26+
"Name table record {:}/{:}/{:}/{:} has trailing spaces that must be removed:\n`{:}`",
27+
name_record.platform_id,
28+
name_record.encoding_id,
29+
name_record.language_id,
30+
name_record.name_id,
31+
name_record.string(name_table.string_data()).map_err(|_| CheckError::Error("Error reading string".to_string()))?,
32+
)))
33+
}
34+
}
35+
}
36+
return_result(problems)
37+
}
38+
39+
fn fix_trailing_spaces(_f: &Testable) -> FixFnResult {
40+
Ok(false)
41+
}

profile-universal/src/checks/os2.rs profile-opentype/src/checks/os2.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -258,5 +258,5 @@ fn xavgcharwidth(f: &Testable, _context: &Context) -> CheckFnResult {
258258
actual, expected, rule
259259
)
260260
)
261-
} )
261+
})
262262
}
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use fontspector_checkapi::{prelude::*, testfont, FileTypeConvert};
2+
3+
const OPTIONAL_TABLE_TAGS: [&[u8; 4]; 20] = [
4+
b"cvt ", b"fpgm", b"loca", b"prep", b"VORG", b"EBDT", b"EBLC", b"EBSC", b"BASE", b"GPOS",
5+
b"GSUB", b"JSTF", b"gasp", b"hdmx", b"LTSH", b"PCLT", b"VDMX", b"vhea", b"vmtx", b"kern",
6+
];
7+
8+
#[check(
9+
id = "required_tables",
10+
title = "Font contains all required tables?",
11+
rationale = "
12+
According to the OpenType spec
13+
https://docs.microsoft.com/en-us/typography/opentype/spec/otff#required-tables
14+
15+
Whether TrueType or CFF outlines are used in an OpenType font, the following
16+
tables are required for the font to function correctly:
17+
18+
- cmap (Character to glyph mapping)⏎
19+
- head (Font header)⏎
20+
- hhea (Horizontal header)⏎
21+
- hmtx (Horizontal metrics)⏎
22+
- maxp (Maximum profile)⏎
23+
- name (Naming table)⏎
24+
- OS/2 (OS/2 and Windows specific metrics)⏎
25+
- post (PostScript information)
26+
27+
The spec also documents that variable fonts require the following table:
28+
29+
- STAT (Style attributes)
30+
31+
Depending on the typeface and coverage of a font, certain tables are
32+
recommended for optimum quality.
33+
34+
For example:⏎
35+
- the performance of a non-linear font is improved if the VDMX, LTSH,
36+
and hdmx tables are present.⏎
37+
- Non-monospaced Latin fonts should have a kern table.⏎
38+
- A gasp table is necessary if a designer wants to influence the sizes
39+
at which grayscaling is used under Windows. Etc.
40+
",
41+
proposal = "legacy:check/052"
42+
)]
43+
fn required_tables(t: &Testable, _context: &Context) -> CheckFnResult {
44+
let f = testfont!(t);
45+
let mut required_table_tags: Vec<_> = vec![
46+
b"cmap", b"head", b"hhea", b"hmtx", b"maxp", b"name", b"OS/2", b"post",
47+
];
48+
49+
if f.is_variable_font() {
50+
// According to https://github.com/fonttools/fontbakery/issues/1671
51+
// STAT table is required on WebKit on MacOS 10.12 for variable fonts.
52+
required_table_tags.push(b"STAT");
53+
}
54+
55+
// See https://github.com/fonttools/fontbakery/issues/617
56+
//
57+
// We should collect the rationale behind the need for each of the
58+
// required tables above. Perhaps split it into individual checks
59+
// with the correspondent rationales for each subset of required tables.
60+
//
61+
// opentype/kern_table is a good example of a separate
62+
// check for a specific table providing a detailed description of
63+
// the rationale behind it.
64+
65+
let mut problems: Vec<Status> = vec![];
66+
let mut optional: Vec<String> = vec![];
67+
68+
for tag in OPTIONAL_TABLE_TAGS {
69+
if f.has_table(tag) {
70+
optional.push(String::from_utf8(tag.to_vec()).map_err(|_| {
71+
CheckError::Error(format!("Font tag '{:?}' wasn't UTF8?", tag.to_vec()))
72+
})?)
73+
}
74+
}
75+
if !optional.is_empty() {
76+
problems.push(Status::info(
77+
"optional-tables",
78+
&format!(
79+
"This font contains the following optional tables:\n\n {}",
80+
optional.join("\n ")
81+
),
82+
))
83+
}
84+
85+
let mut missing = vec![];
86+
for tag in required_table_tags {
87+
if !f.has_table(tag) {
88+
missing.push(String::from_utf8(tag.to_vec()).map_err(|_| {
89+
CheckError::Error(format!("Font tag '{:?}' wasn't UTF8?", tag.to_vec()))
90+
})?);
91+
}
92+
}
93+
94+
// Note (from the OpenType spec):
95+
// OpenType fonts that contain TrueType outlines should use the value of 0x00010000
96+
// for sfntVersion. OpenType fonts containing CFF data (version 1 or 2) should use
97+
// 0x4F54544F ('OTTO', when re-interpreted as a Tag) for sfntVersion.
98+
let version = f.font().table_directory.sfnt_version();
99+
if version == 0x4F54544F && (!f.has_table(b"CFF ") && !f.has_table(b"CFF2")) {
100+
if f.has_table(b"fvar") {
101+
missing.push("CFF2".to_string());
102+
} else {
103+
missing.push("CFF ".to_string());
104+
}
105+
} else if version == 0x00010000 && !f.has_table(b"glyf") {
106+
missing.push("glyf".to_string());
107+
}
108+
109+
if !missing.is_empty() {
110+
problems.push(Status::fail(
111+
"required-tables",
112+
&format!(
113+
"This font is missing the following required tables:\n\n {}",
114+
missing.join("\n ")
115+
),
116+
))
117+
}
118+
119+
return_result(problems)
120+
}

profile-universal/src/checks/stat.rs profile-opentype/src/checks/stat.rs

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::{HashMap, HashSet};
22

3-
use font_types::Fixed;
43
use fontspector_checkapi::{prelude::*, skip, testfont, FileTypeConvert, TestFont};
4+
use font_types::Fixed;
55
use read_fonts::{
66
tables::stat::{AxisValue, AxisValueTableFlags},
77
ReadError, TableProvider,
@@ -30,8 +30,7 @@ fn stat_axis_record(t: &Testable, context: &Context) -> CheckFnResult {
3030
.collect();
3131
let stat_axis_tags: HashSet<_> = f
3232
.font()
33-
.stat()
34-
.map_err(|_| CheckError::skip("no-stat", "No STAT table"))?
33+
.stat()?
3534
.design_axes()?
3635
.iter()
3736
.map(|axis_record| axis_record.axis_tag().to_string())

0 commit comments

Comments
 (0)