Skip to content

Commit

Permalink
[write-fonts/gvar] add axis_count param to Gvar::new
Browse files Browse the repository at this point in the history
this ensures that we (e.g. fontc) can make valid empty gvar tables where the axis_count matches the expected fvar's axis_count

Will fix googlefonts/fontc#815 once fontc is updated to use the new API.

This is a breaking changes which warrants minor write-fonts version bump.
  • Loading branch information
anthrotype committed May 29, 2024
1 parent 2cd8893 commit c266e0f
Showing 1 changed file with 141 additions and 90 deletions.
231 changes: 141 additions & 90 deletions write-fonts/src/tables/gvar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ pub struct GlyphDelta {
/// An error representing invalid input when building a gvar table
#[derive(Clone, Debug)]
pub enum GvarInputError {
/// Glyphs do not have a consistent axis count
InconsistentAxisCount,
/// Glyph variations do not have the expected axis count
UnexpectedAxisCount {
gid: GlyphId,
expected: u16,
got: u16,
},
/// A single glyph contains variations with inconsistent axis counts
InconsistentGlyphAxisCount(GlyphId),
/// A single glyph contains variations with different delta counts
Expand All @@ -64,30 +68,15 @@ pub enum GvarInputError {
}

impl Gvar {
/// Construct a gvar table from a vector of per-glyph variations.
/// Construct a gvar table from a vector of per-glyph variations and the axis count.
///
/// Variations must be present for each glyph, but may be empty.
pub fn new(mut variations: Vec<GlyphVariations>) -> Result<Self, GvarInputError> {
// a helper that handles input validation, and returns axis count
fn validate_variations(variations: &[GlyphVariations]) -> Result<u16, GvarInputError> {
for var in variations {
var.validate()?;
}

let axis_count = variations
.iter()
.find_map(GlyphVariations::axis_count)
.unwrap_or_default();
if variations
.iter()
.filter_map(GlyphVariations::axis_count)
.any(|x| x != axis_count)
{
return Err(GvarInputError::InconsistentAxisCount);
}
Ok(axis_count)
}

/// For non-empty variations, the axis count must be equal to the provided
/// axis count, as specified by the 'fvar' table.
pub fn new(
mut variations: Vec<GlyphVariations>,
axis_count: u16,
) -> Result<Self, GvarInputError> {
fn compute_shared_peak_tuples(glyphs: &[GlyphVariations]) -> Vec<Tuple> {
const MAX_SHARED_TUPLES: usize = 4095;
let mut peak_tuple_counts = IndexMap::new();
Expand All @@ -106,7 +95,21 @@ impl Gvar {
to_share.into_iter().map(|(t, _)| t.to_owned()).collect()
}

let axis_count = validate_variations(&variations)?;
for var in &variations {
var.validate()?;
}

for var in &variations {
if let Some(x) = var.axis_count() {
if x != axis_count {
return Err(GvarInputError::UnexpectedAxisCount {
gid: var.gid,
expected: axis_count,
got: x,
});
}
}
}

let shared = compute_shared_peak_tuples(&variations);
let shared_idx_map = shared
Expand Down Expand Up @@ -640,8 +643,12 @@ impl FontWrite for TupleVariationCount {
impl std::fmt::Display for GvarInputError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GvarInputError::InconsistentAxisCount => {
write!(f, "Glyphs do not have a consistent axis count")
GvarInputError::UnexpectedAxisCount { gid, expected, got } => {
write!(
f,
"Expected {} axes for glyph {}, got {}",
expected, gid, got
)
}
GvarInputError::InconsistentGlyphAxisCount(gid) => write!(
f,
Expand All @@ -667,50 +674,53 @@ mod tests {
#[test]
fn gvar_smoke_test() {
let _ = env_logger::builder().is_test(true).try_init();
let table = Gvar::new(vec![
GlyphVariations::new(GlyphId::new(0), vec![]),
GlyphVariations::new(
GlyphId::new(1),
vec![GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
vec![
GlyphDelta::required(30, 31),
GlyphDelta::required(40, 41),
GlyphDelta::required(-50, -49),
GlyphDelta::required(101, 102),
GlyphDelta::required(10, 11),
],
None,
)],
),
GlyphVariations::new(
GlyphId::new(2),
vec![
GlyphDeltas::new(
let table = Gvar::new(
vec![
GlyphVariations::new(GlyphId::new(0), vec![]),
GlyphVariations::new(
GlyphId::new(1),
vec![GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
vec![
GlyphDelta::required(11, -20),
GlyphDelta::required(69, -41),
GlyphDelta::required(-69, 49),
GlyphDelta::required(168, 101),
GlyphDelta::required(1, 2),
],
None,
),
GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(0.8), F2Dot14::from_f32(1.0)]),
vec![
GlyphDelta::required(3, -200),
GlyphDelta::required(4, -500),
GlyphDelta::required(5, -800),
GlyphDelta::required(6, -1200),
GlyphDelta::required(7, -1500),
GlyphDelta::required(30, 31),
GlyphDelta::required(40, 41),
GlyphDelta::required(-50, -49),
GlyphDelta::required(101, 102),
GlyphDelta::required(10, 11),
],
None,
),
],
),
])
)],
),
GlyphVariations::new(
GlyphId::new(2),
vec![
GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
vec![
GlyphDelta::required(11, -20),
GlyphDelta::required(69, -41),
GlyphDelta::required(-69, 49),
GlyphDelta::required(168, 101),
GlyphDelta::required(1, 2),
],
None,
),
GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(0.8), F2Dot14::from_f32(1.0)]),
vec![
GlyphDelta::required(3, -200),
GlyphDelta::required(4, -500),
GlyphDelta::required(5, -800),
GlyphDelta::required(6, -1200),
GlyphDelta::required(7, -1500),
],
None,
),
],
),
],
2,
)
.unwrap();
let g2 = &table.glyph_variation_data_offsets[1];
let computed = g2.compute_size();
Expand Down Expand Up @@ -750,21 +760,24 @@ mod tests {
// IFF iup provides space savings, we should prefer it.
let _ = env_logger::builder().is_test(true).try_init();
let gid = GlyphId::new(0);
let table = Gvar::new(vec![GlyphVariations::new(
gid,
vec![GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
vec![
GlyphDelta::required(30, 31),
GlyphDelta::optional(30, 31),
GlyphDelta::optional(30, 31),
GlyphDelta::required(101, 102),
GlyphDelta::required(10, 11),
GlyphDelta::optional(10, 11),
],
None,
let table = Gvar::new(
vec![GlyphVariations::new(
gid,
vec![GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
vec![
GlyphDelta::required(30, 31),
GlyphDelta::optional(30, 31),
GlyphDelta::optional(30, 31),
GlyphDelta::required(101, 102),
GlyphDelta::required(10, 11),
GlyphDelta::optional(10, 11),
],
None,
)],
)],
)])
2,
)
.unwrap();

let bytes = crate::dump_table(&table).unwrap();
Expand Down Expand Up @@ -803,14 +816,17 @@ mod tests {
GlyphDelta::required(7, 8),
];
let gid = GlyphId::new(0);
let table = Gvar::new(vec![GlyphVariations::new(
gid,
vec![GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
points,
None,
let table = Gvar::new(
vec![GlyphVariations::new(
gid,
vec![GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0), F2Dot14::from_f32(1.0)]),
points,
None,
)],
)],
)])
2,
)
.unwrap();
let bytes = crate::dump_table(&table).unwrap();
let gvar = read_fonts::tables::gvar::Gvar::read(FontData::new(&bytes)).unwrap();
Expand Down Expand Up @@ -1201,7 +1217,7 @@ mod tests {
.map(|i| GlyphVariations::new(GlyphId::new(i), test_data.clone()))
.collect();

let gvar = Gvar::new(a_small_number_of_variations).unwrap();
let gvar = Gvar::new(a_small_number_of_variations, 2).unwrap();
assert_eq!(gvar.compute_flags(), expected_flags);

let writer = gvar.compile_variation_data();
Expand Down Expand Up @@ -1265,7 +1281,7 @@ mod tests {
))
}
for _ in 0..10 {
let table = Gvar::new(variations.clone()).unwrap();
let table = Gvar::new(variations.clone(), 1).unwrap();
let bytes = crate::dump_table(&table).unwrap();
let gvar = read_fonts::tables::gvar::Gvar::read(FontData::new(&bytes)).unwrap();

Expand All @@ -1281,4 +1297,39 @@ mod tests {
);
}
}

#[test]
fn unexpected_axis_count() {
let variations = GlyphVariations::new(
GlyphId::NOTDEF,
vec![
GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0)]),
vec![GlyphDelta::required(1, 2)],
None,
),
GlyphDeltas::new(
Tuple::new(vec![F2Dot14::from_f32(1.0)]),
vec![GlyphDelta::required(1, 2)],
None,
),
],
);
let gvar = Gvar::new(vec![variations], 2);
assert!(matches!(
gvar,
Err(GvarInputError::UnexpectedAxisCount {
gid: GlyphId::NOTDEF,
expected: 2,
got: 1
})
));
}

#[test]
fn empty_gvar_has_expected_axis_count() {
let variations = GlyphVariations::new(GlyphId::NOTDEF, vec![]);
let gvar = Gvar::new(vec![variations], 2).unwrap();
assert_eq!(gvar.axis_count, 2);
}
}

0 comments on commit c266e0f

Please sign in to comment.