//! Axes of variation in a variable font.
use read_fonts::{
tables::avar::Avar,
tables::fvar::{self, Fvar},
types::{Fixed, Tag},
TableProvider,
};
use crate::{
collections::SmallVec,
instance::{Location, NormalizedCoord},
setting::VariationSetting,
string::StringId,
};
/// Axis of variation in a variable font.
///
/// In variable fonts, an axis usually refers to a single aspect of a
/// typeface's design that can be altered by the user.
///
/// See <https://fonts.google.com/knowledge/glossary/axis_in_variable_fonts>
#[derive(Clone)]
pub struct Axis {
index: usize,
record: fvar::VariationAxisRecord,
}
impl Axis {
/// Returns the tag that identifies the axis.
pub fn tag(&self) -> Tag {
self.record.axis_tag()
}
/// Returns the index of the axis in its owning collection.
pub fn index(&self) -> usize {
self.index
}
/// Returns the localized string identifier for the name of the axis.
pub fn name_id(&self) -> StringId {
self.record.axis_name_id()
}
/// Returns true if the axis should be hidden in user interfaces.
pub fn is_hidden(&self) -> bool {
const AXIS_HIDDEN_FLAG: u16 = 0x1;
self.record.flags() & AXIS_HIDDEN_FLAG != 0
}
/// Returns the minimum value of the axis.
pub fn min_value(&self) -> f32 {
self.record.min_value().to_f64() as _
}
/// Returns the default value of the axis.
pub fn default_value(&self) -> f32 {
self.record.default_value().to_f64() as _
}
/// Returns the maximum value of the axis.
pub fn max_value(&self) -> f32 {
self.record.max_value().to_f64() as _
}
/// Returns a normalized coordinate for the given user coordinate.
///
/// The value will be clamped to the range specified by the minimum
/// and maximum values.
///
/// This does not apply any axis variation remapping.
pub fn normalize(&self, coord: f32) -> NormalizedCoord {
self.record
.normalize(Fixed::from_f64(coord as _))
.to_f2dot14()
}
}
/// Collection of axes in a variable font.
///
/// Converts user ([fvar](https://learn.microsoft.com/en-us/typography/opentype/spec/fvar))
/// locations to normalized locations. See [`Self::location`].
///
/// See the [`Axis`] type for more detail.
#[derive(Clone)]
pub struct AxisCollection<'a> {
fvar: Option<Fvar<'a>>,
avar: Option<Avar<'a>>,
}
impl<'a> AxisCollection<'a> {
/// Creates a new axis collection from the given font.
pub fn new(font: &impl TableProvider<'a>) -> Self {
let fvar = font.fvar().ok();
let avar = font.avar().ok();
Self { fvar, avar }
}
/// Returns the number of variation axes in the font.
pub fn len(&self) -> usize {
self.fvar
.as_ref()
.map(|fvar| fvar.axis_count() as usize)
.unwrap_or(0)
}
/// Returns true if the collection is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the axis at the given index.
pub fn get(&self, index: usize) -> Option<Axis> {
let record = *self.fvar.as_ref()?.axes().ok()?.get(index)?;
Some(Axis { index, record })
}
/// Returns the axis with the given tag.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let opsz = Tag::new(b"opsz");
/// assert_eq!(font.axes().get_by_tag(opsz).unwrap().tag(), opsz);
/// # }
/// ```
pub fn get_by_tag(&self, tag: Tag) -> Option<Axis> {
self.iter().find(|axis| axis.tag() == tag)
}
/// Given an iterator of variation settings in user space, computes an
/// ordered sequence of normalized coordinates.
///
/// * Setting selectors that don't match an axis are ignored.
/// * Setting values are clamped to the range of their associated axis
/// before normalization.
/// * If more than one setting for an axis is provided, the last one is
/// used.
/// * Omitted settings are set to 0.0, representing the default position
/// in variation space.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let location = font.axes().location([("wght", 250.0), ("wdth", 75.0)]);
/// # }
/// ```
pub fn location<I>(&self, settings: I) -> Location
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
let mut location = Location::new(self.len());
self.location_to_slice(settings, location.coords_mut());
location
}
/// Given an iterator of variation settings in user space, computes an
/// ordered sequence of normalized coordinates and stores them in the
/// target slice.
///
/// * Setting selectors that don't match an axis are ignored.
/// * Setting values are clamped to the range of their associated axis
/// before normalization.
/// * If more than one setting for an axis is provided, the last one is
/// used.
/// * If no setting for an axis is provided, the associated coordinate is
/// set to the normalized value 0.0, representing the default position
/// in variation space.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let axes = font.axes();
/// let mut location = vec![NormalizedCoord::default(); axes.len()];
/// axes.location_to_slice([("wght", 250.0), ("wdth", 75.0)], &mut location);
/// # }
/// ```
pub fn location_to_slice<I>(&self, settings: I, location: &mut [NormalizedCoord])
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
for coord in location.iter_mut() {
*coord = NormalizedCoord::default();
}
let avar_mappings = self.avar.as_ref().map(|avar| avar.axis_segment_maps());
for setting in settings.into_iter() {
let setting = setting.into();
// 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 (i, axis) in self
.iter()
.enumerate()
.filter(|v| v.1.tag() == setting.selector)
{
if let Some(target_coord) = location.get_mut(i) {
let coord = axis.record.normalize(Fixed::from_f64(setting.value as f64));
*target_coord = avar_mappings
.as_ref()
.and_then(|mappings| mappings.get(i).transpose().ok())
.flatten()
.map(|mapping| mapping.apply(coord))
.unwrap_or(coord)
.to_f2dot14();
}
}
}
}
/// Given an iterator of variation settings in user space, returns a
/// new iterator yielding those settings that are valid for this axis
/// collection.
///
/// * Setting selectors that don't match an axis are dropped.
/// * If more than one setting for an axis is provided, the last one is
/// retained.
/// * Setting values are clamped to the range of their associated axis.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// // Assuming a font contains a single "wght" (weight) axis with range
/// // 100-900:
/// let axes = font.axes();
/// let filtered: Vec<_> = axes
/// .filter([("wght", 400.0), ("opsz", 100.0), ("wght", 1200.0)])
/// .collect();
/// // The first "wght" and "opsz" settings are dropped and the final
/// // "wght" axis is clamped to the maximum value of 900.
/// assert_eq!(&filtered, &[("wght", 900.0).into()]);
/// # }
/// ```
pub fn filter<I>(&self, settings: I) -> impl Iterator<Item = VariationSetting> + Clone
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
#[derive(Copy, Clone, Default)]
struct Entry {
tag: Tag,
min: f32,
max: f32,
value: f32,
present: bool,
}
let mut results = SmallVec::<_, 8>::with_len(self.len(), Entry::default());
for (axis, result) in self.iter().zip(results.as_mut_slice()) {
result.tag = axis.tag();
result.min = axis.min_value();
result.max = axis.max_value();
result.value = axis.default_value();
}
for setting in settings {
let setting = setting.into();
for entry in results.as_mut_slice() {
if entry.tag == setting.selector {
entry.value = setting.value.max(entry.min).min(entry.max);
entry.present = true;
}
}
}
results
.into_iter()
.filter(|entry| entry.present)
.map(|entry| VariationSetting::new(entry.tag, entry.value))
}
/// Returns an iterator over the axes in the collection.
pub fn iter(&self) -> impl Iterator<Item = Axis> + 'a + Clone {
let copy = self.clone();
(0..self.len()).filter_map(move |i| copy.get(i))
}
}
/// Named instance of a variation.
///
/// A set of fixed axis positions selected by the type designer and assigned a
/// name.
///
/// See <https://fonts.google.com/knowledge/glossary/instance>
#[derive(Clone)]
pub struct NamedInstance<'a> {
axes: AxisCollection<'a>,
record: fvar::InstanceRecord<'a>,
}
impl<'a> NamedInstance<'a> {
/// Returns the string identifier for the subfamily name of the instance.
pub fn subfamily_name_id(&self) -> StringId {
self.record.subfamily_name_id
}
/// Returns the string identifier for the PostScript name of the instance.
pub fn postscript_name_id(&self) -> Option<StringId> {
self.record.post_script_name_id
}
/// Returns an iterator over the ordered sequence of user space coordinates
/// that define the instance, one coordinate per axis.
pub fn user_coords(&self) -> impl Iterator<Item = f32> + 'a + Clone {
self.record
.coordinates
.iter()
.map(|coord| coord.get().to_f64() as _)
}
/// Computes a location in normalized variation space for this instance.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let location = font.named_instances().get(0).unwrap().location();
/// # }
/// ```
pub fn location(&self) -> Location {
let mut location = Location::new(self.axes.len());
self.location_to_slice(location.coords_mut());
location
}
/// Computes a location in normalized variation space for this instance and
/// stores the result in the given slice.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let instance = font.named_instances().get(0).unwrap();
/// let mut location = vec![NormalizedCoord::default(); instance.user_coords().count()];
/// instance.location_to_slice(&mut location);
/// # }
/// ```
pub fn location_to_slice(&self, location: &mut [NormalizedCoord]) {
let settings = self
.axes
.iter()
.map(|axis| axis.tag())
.zip(self.user_coords());
self.axes.location_to_slice(settings, location);
}
}
/// Collection of named instances in a variable font.
///
/// See the [`NamedInstance`] type for more detail.
#[derive(Clone)]
pub struct NamedInstanceCollection<'a> {
axes: AxisCollection<'a>,
}
impl<'a> NamedInstanceCollection<'a> {
/// Creates a new instance collection from the given font.
pub fn new(font: &impl TableProvider<'a>) -> Self {
Self {
axes: AxisCollection::new(font),
}
}
/// Returns the number of instances in the collection.
pub fn len(&self) -> usize {
self.axes
.fvar
.as_ref()
.map(|fvar| fvar.instance_count() as usize)
.unwrap_or(0)
}
/// Returns true if the collection is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the instance at the given index.
pub fn get(&self, index: usize) -> Option<NamedInstance<'a>> {
let record = self.axes.fvar.as_ref()?.instances().ok()?.get(index).ok()?;
Some(NamedInstance {
axes: self.axes.clone(),
record,
})
}
/// Returns an iterator over the instances in the collection.
pub fn iter(&self) -> impl Iterator<Item = NamedInstance<'a>> + 'a + Clone {
let copy = self.clone();
(0..self.len()).filter_map(move |i| copy.get(i))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MetadataProvider as _;
use font_test_data::VAZIRMATN_VAR;
use read_fonts::FontRef;
use std::str::FromStr;
#[test]
fn axis() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
let axis = font.axes().get(0).unwrap();
assert_eq!(axis.index(), 0);
assert_eq!(axis.tag(), Tag::new(b"wght"));
assert_eq!(axis.min_value(), 100.0);
assert_eq!(axis.default_value(), 400.0);
assert_eq!(axis.max_value(), 900.0);
assert_eq!(axis.name_id(), StringId::new(257));
assert_eq!(
font.localized_strings(axis.name_id())
.english_or_first()
.unwrap()
.to_string(),
"Weight"
);
}
#[test]
fn named_instances() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
let named_instances = font.named_instances();
let thin = named_instances.get(0).unwrap();
assert_eq!(thin.subfamily_name_id(), StringId::new(258));
assert_eq!(
font.localized_strings(thin.subfamily_name_id())
.english_or_first()
.unwrap()
.to_string(),
"Thin"
);
assert_eq!(thin.location().coords(), &[NormalizedCoord::from_f32(-1.0)]);
let regular = named_instances.get(3).unwrap();
assert_eq!(regular.subfamily_name_id(), StringId::new(261));
assert_eq!(
font.localized_strings(regular.subfamily_name_id())
.english_or_first()
.unwrap()
.to_string(),
"Regular"
);
assert_eq!(
regular.location().coords(),
&[NormalizedCoord::from_f32(0.0)]
);
let bold = named_instances.get(6).unwrap();
assert_eq!(bold.subfamily_name_id(), StringId::new(264));
assert_eq!(
font.localized_strings(bold.subfamily_name_id())
.english_or_first()
.unwrap()
.to_string(),
"Bold"
);
assert_eq!(
bold.location().coords(),
&[NormalizedCoord::from_f32(0.6776123)]
);
}
#[test]
fn location() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
let axes = font.axes();
let axis = axes.get_by_tag(Tag::from_str("wght").unwrap()).unwrap();
assert_eq!(
axes.location([("wght", -1000.0)]).coords(),
&[NormalizedCoord::from_f32(-1.0)]
);
assert_eq!(
axes.location([("wght", 100.0)]).coords(),
&[NormalizedCoord::from_f32(-1.0)]
);
assert_eq!(
axes.location([("wght", 200.0)]).coords(),
&[NormalizedCoord::from_f32(-0.5)]
);
assert_eq!(
axes.location([("wght", 400.0)]).coords(),
&[NormalizedCoord::from_f32(0.0)]
);
// avar table maps 0.8 to 0.83875
assert_eq!(
axes.location(&[(
"wght",
axis.default_value() + (axis.max_value() - axis.default_value()) * 0.8,
)])
.coords(),
&[NormalizedCoord::from_f32(0.83875)]
);
assert_eq!(
axes.location([("wght", 900.0)]).coords(),
&[NormalizedCoord::from_f32(1.0)]
);
assert_eq!(
axes.location([("wght", 1251.5)]).coords(),
&[NormalizedCoord::from_f32(1.0)]
);
}
#[test]
fn filter() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
// This font contains one wght axis with the range 100-900 and default
// value of 400.
let axes = font.axes();
// Drop axes that are not present in the font
let drop_missing: Vec<_> = axes.filter(&[("slnt", 25.0), ("wdth", 50.0)]).collect();
assert_eq!(&drop_missing, &[]);
// Clamp an out of range value
let clamp: Vec<_> = axes.filter(&[("wght", 50.0)]).collect();
assert_eq!(&clamp, &[("wght", 100.0).into()]);
// Combination of the above two: drop the missing axis and clamp out of range value
let drop_missing_and_clamp: Vec<_> =
axes.filter(&[("slnt", 25.0), ("wght", 1000.0)]).collect();
assert_eq!(&drop_missing_and_clamp, &[("wght", 900.0).into()]);
// Ensure we take the later value in the case of duplicates
let drop_duplicate_and_missing: Vec<_> = axes
.filter(&[("wght", 400.0), ("opsz", 100.0), ("wght", 120.5)])
.collect();
assert_eq!(&drop_duplicate_and_missing, &[("wght", 120.5).into()]);
}
}