Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 102 additions & 13 deletions read-fonts/src/tables/fvar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ use alloc::vec::Vec;
pub use instance_record::InstanceRecord;

const MAX_INLINE_AVAR2_AXES: usize = 64;
/// Maximum number of axes for which we will use a quadratic path to normalize user coordinates.
const MAX_NORMALIZE_QUADRATIC_AXES: usize = 64;
const MAX_INLINE_NORMALIZE_AXES: usize = 128;

#[inline]
fn round_f64_to_i32(value: f64) -> i32 {
Expand Down Expand Up @@ -38,18 +41,45 @@ fn normalize_user_coords<T>(
coords: &mut [T],
convert: impl Fn(Fixed) -> T,
) {
for user_coord in user_coords {
// To permit non-linear interpolation, iterate over all axes to ensure we match
// multiple axes with the same tag:
// https://github.com/PeterConstable/OT_Drafts/blob/master/NLI/UnderstandingNLI.md
// We accept quadratic behavior here to avoid dynamic allocation and with the assumption
// that fonts contain a relatively small number of axes.
for (axis, coord) in axes
.iter()
.zip(coords.iter_mut())
.filter(|(axis, _)| axis.axis_tag() == user_coord.0)
{
*coord = convert(axis.normalize(user_coord.1));
let axis_count = axes.len().min(coords.len());
let axes = &axes[..axis_count];
let coords = &mut coords[..axis_count];
if axis_count <= MAX_NORMALIZE_QUADRATIC_AXES {
for (tag, user_coord) in user_coords {
// To permit non-linear interpolation, iterate over all axes to
// ensure we match multiple axes with the same tag:
// https://github.com/PeterConstable/OT_Drafts/blob/master/NLI/UnderstandingNLI.md
for (axis, coord) in axes
.iter()
.zip(coords.iter_mut())
.filter(|(axis, _)| axis.axis_tag() == tag)
{
*coord = convert(axis.normalize(user_coord));
}
}
return;
}
// Above MAX_NORMALIZE_QUADRATIC_AXES, use an indexed path to avoid O(n^2) behavior
let mut stack_axis_order = [0u16; MAX_INLINE_NORMALIZE_AXES];
let mut heap_axis_order = Vec::new();
let axis_order = if axis_count > MAX_INLINE_NORMALIZE_AXES {
heap_axis_order.resize(axis_count, 0);
heap_axis_order.as_mut_slice()
} else {
&mut stack_axis_order[..axis_count]
};
// Initialize axis_order with the identity mapping
for (i, axis_index) in axis_order.iter_mut().enumerate() {
*axis_index = i as u16;
}
// Then sort by tag to group axes with the same tag together, so we can use
// partition_point to find the range of axes for each tag
axis_order.sort_unstable_by_key(|&i| axes[i as usize].axis_tag());
for (tag, user_coord) in user_coords {
let start = axis_order.partition_point(|&i| axes[i as usize].axis_tag() < tag);
let end = axis_order.partition_point(|&i| axes[i as usize].axis_tag() <= tag);
for &i in &axis_order[start..end] {
coords[i as usize] = convert(axes[i as usize].normalize(user_coord));
}
}
}
Expand Down Expand Up @@ -200,8 +230,9 @@ impl VariationAxisRecord {

#[cfg(test)]
mod tests {
use super::*;
use crate::{FontRef, TableProvider};
use types::{F2Dot14, Fixed, NameId, Tag};
use types::{BigEndian, F2Dot14, Fixed, NameId, Tag};

#[test]
fn axes() {
Expand Down Expand Up @@ -275,6 +306,64 @@ mod tests {
);
}

#[test]
fn normalize_user_coords_uses_indexed_path_for_large_axis_counts() {
let axis = VariationAxisRecord {
axis_tag: BigEndian::from(Tag::new(b"wght")),
min_value: BigEndian::from(Fixed::from_f64(-1.0)),
default_value: BigEndian::from(Fixed::ZERO),
max_value: BigEndian::from(Fixed::ONE),
flags: BigEndian::from(0),
axis_name_id: BigEndian::from(NameId::new(1)),
};
let axes = vec![axis; MAX_NORMALIZE_QUADRATIC_AXES + 1];
let mut coords = vec![Fixed::ZERO; axes.len()];
normalize_user_coords(
&axes,
[(Tag::new(b"wght"), Fixed::from_f64(0.5))],
&mut coords,
core::convert::identity,
);
assert!(coords.iter().all(|coord| *coord == Fixed::from_f64(0.5)));
}

#[test]
fn normalize_user_coords_handles_duplicate_tags_in_indexed_path() {
let wght_axis = VariationAxisRecord {
axis_tag: BigEndian::from(Tag::new(b"wght")),
min_value: BigEndian::from(Fixed::from_f64(-1.0)),
default_value: BigEndian::from(Fixed::ZERO),
max_value: BigEndian::from(Fixed::ONE),
flags: BigEndian::from(0),
axis_name_id: BigEndian::from(NameId::new(1)),
};
let wdth_axis = VariationAxisRecord {
axis_tag: BigEndian::from(Tag::new(b"wdth")),
min_value: BigEndian::from(Fixed::from_f64(-1.0)),
default_value: BigEndian::from(Fixed::ZERO),
max_value: BigEndian::from(Fixed::ONE),
flags: BigEndian::from(0),
axis_name_id: BigEndian::from(NameId::new(2)),
};
let axes = (0..(MAX_NORMALIZE_QUADRATIC_AXES + 2))
.map(|i| if i % 2 == 0 { wght_axis } else { wdth_axis })
.collect::<Vec<_>>();
let mut coords = vec![Fixed::ZERO; axes.len()];
normalize_user_coords(
&axes,
[(Tag::new(b"wght"), Fixed::from_f64(0.25))],
&mut coords,
core::convert::identity,
);
for (i, coord) in coords.iter().enumerate() {
if i % 2 == 0 {
assert_eq!(*coord, Fixed::from_f64(0.25));
} else {
assert_eq!(*coord, Fixed::ZERO);
}
}
}

#[test]
fn user_to_normalized() {
let font = FontRef::from_index(font_test_data::VAZIRMATN_VAR, 0).unwrap();
Expand Down
Loading