allsorts_subset_browser/tables/variable_fonts/
fvar.rs

1#![deny(missing_docs)]
2
3//! `fvar` Font Variations Table
4//!
5//! <https://learn.microsoft.com/en-us/typography/opentype/spec/fvar>
6
7use crate::binary::read::{
8    ReadArray, ReadBinary, ReadBinaryDep, ReadCtxt, ReadFrom, ReadScope, ReadUnchecked,
9};
10use crate::binary::{U16Be, U32Be};
11use crate::error::ParseError;
12use crate::tables::variable_fonts::avar::AvarTable;
13use crate::tables::variable_fonts::UserTuple;
14use crate::tables::{F2Dot14, Fixed};
15use tinyvec::TinyVec;
16
17/// `fvar` font Variations Table
18///
19/// <https://learn.microsoft.com/en-us/typography/opentype/spec/fvar#fvar-header>
20pub struct FvarTable<'a> {
21    /// Major version number of the font variations table
22    pub major_version: u16,
23    /// Minor version number of the font variations table
24    pub minor_version: u16,
25    /// The VariationAxisRecords
26    axes: ReadArray<'a, VariationAxisRecord>,
27    /// The number of named instances defined in the font (the number of records
28    /// in the instances array).
29    instance_count: u16,
30    /// The size in bytes of each InstanceRecord
31    instance_size: u16,
32    instance_array: &'a [u8],
33}
34
35/// Variation axis
36///
37/// <https://learn.microsoft.com/en-us/typography/opentype/spec/fvar#variationaxisrecord>
38#[derive(Eq, PartialEq, Debug)]
39pub struct VariationAxisRecord {
40    /// Tag identifying the design variation for the axis.
41    pub axis_tag: u32,
42    /// The minimum coordinate value for the axis.
43    pub min_value: Fixed,
44    /// The default coordinate value for the axis.
45    pub default_value: Fixed,
46    /// The maximum coordinate value for the axis.
47    pub max_value: Fixed,
48    /// Axis qualifiers.
49    pub flags: u16,
50    /// The name ID for entries in the `name` table that provide a display name
51    /// for this axis.
52    pub axis_name_id: u16,
53}
54
55/// Variation instance record
56///
57/// Instances are like named presets for a variable font. Each instance has name
58/// and a value for each variation axis.
59///
60/// <https://learn.microsoft.com/en-us/typography/opentype/spec/fvar#instancerecord>
61#[derive(Debug)]
62pub struct InstanceRecord<'a> {
63    /// The name ID for entries in the `name` table that provide subfamily names
64    /// for this instance.
65    pub subfamily_name_id: u16,
66    /// Flags
67    pub flags: u16,
68    /// The coordinates array for this instance.
69    pub coordinates: UserTuple<'a>,
70    /// Optional. The name ID for entries in the `name` table that provide
71    /// PostScript names for this instance.
72    pub post_script_name_id: Option<u16>,
73}
74
75// Wes counted the number of axes in 399 variable fonts in Google Fonts and this
76// was the result:
77//
78// | Axis Count | Number |
79// |------------|--------|
80// | 1          | 279    |
81// | 2          | 108    |
82// | 3          | 2      |
83// | 4          | 5      |
84// | 5          | 1      |
85// | 13         | 2      |
86// | 15         | 2      |
87//
88// With this in mind the majority of fonts are handled with two axes. However,
89// the minimum size of a TinyVec is 24 bytes due to the Vec it can also hold, so
90// I choose 4 since it doesn't use any more space than when set to two.
91
92/// Coordinate array specifying a position within the font’s variation space
93/// (owned version).
94///
95/// Owned version of [Tuple].
96///
97/// This must be constructed through the the [FvarTable] to
98/// ensure that the number of elements matches the
99/// [axis_count](FvarTable::axis_count()).
100#[derive(Debug)]
101pub struct OwnedTuple(TinyVec<[F2Dot14; 4]>);
102
103/// A variation tuple containing a normalized value for each variation axis.
104#[derive(Debug, Copy, Clone)]
105pub struct Tuple<'a>(&'a [F2Dot14]);
106
107impl FvarTable<'_> {
108    /// Returns an iterator over the variation axes of the font.
109    pub fn axes(&self) -> impl Iterator<Item = VariationAxisRecord> + '_ {
110        self.axes.iter()
111    }
112
113    /// Returns the number of variation axes in the font.
114    pub fn axis_count(&self) -> u16 {
115        // NOTE(cast): Valid as self.axes is contructed from a u16 length
116        self.axes.len() as u16
117    }
118
119    /// Returns an iterator over the pre-defined instances in the font.
120    pub fn instances(&self) -> impl Iterator<Item = Result<InstanceRecord<'_>, ParseError>> + '_ {
121        // These are pulled out to work around lifetime errors if &self is moved into
122        // the closure.
123        let instance_array = self.instance_array;
124        let axis_count = self.axis_count();
125        let instance_size = usize::from(self.instance_size);
126        (0..usize::from(self.instance_count)).map(move |i| {
127            let offset = i * instance_size;
128            instance_array
129                .get(offset..(offset + instance_size))
130                .ok_or(ParseError::BadIndex)
131                .and_then(|data| {
132                    ReadScope::new(data).read_dep::<InstanceRecord<'_>>((instance_size, axis_count))
133                })
134        })
135    }
136
137    /// Turn a user tuple into a tuple normalized over the range -1..1.
138    pub fn normalize(
139        &self,
140        user_tuple: impl ExactSizeIterator<Item = Fixed>,
141        avar: Option<&AvarTable<'_>>,
142    ) -> Result<OwnedTuple, ParseError> {
143        if user_tuple.len() != usize::from(self.axis_count()) {
144            return Err(ParseError::BadValue);
145        }
146
147        let mut tuple = TinyVec::with_capacity(user_tuple.len());
148        let mut avar_iter = avar.map(|avar| avar.segment_maps());
149        for (axis, user_value) in self.axes().zip(user_tuple) {
150            let mut normalized_value = default_normalize(&axis, user_value);
151
152            // If avar table is present do more normalization with it
153            if let Some(avar) = avar_iter.as_mut() {
154                let segment_map = avar.next().ok_or(ParseError::BadIndex)?;
155                normalized_value = segment_map.normalize(normalized_value);
156                // Do the -1..1 clamping again to ensure the value remains in range
157                normalized_value = normalized_value.clamp(Fixed::from(-1), Fixed::from(1));
158            }
159
160            // Convert the final, normalized 16.16 coordinate value to 2.14.
161            tuple.push(F2Dot14::from(normalized_value));
162        }
163        Ok(OwnedTuple(tuple))
164    }
165
166    /// Construct a new [OwnedTuple].
167    ///
168    /// Returns `None` if the number of elements in `values` does not match
169    /// [axis_count](FvarTable::axis_count()).
170    pub fn owned_tuple(&self, values: &[F2Dot14]) -> Option<OwnedTuple> {
171        (values.len() == usize::from(self.axis_count())).then(|| OwnedTuple(TinyVec::from(values)))
172    }
173}
174
175fn default_normalize(axis: &VariationAxisRecord, coord: Fixed) -> Fixed {
176    // Clamp
177    let coord = coord.clamp(axis.min_value, axis.max_value);
178
179    // Interpolate
180    let normalised_value = if coord < axis.default_value {
181        -(axis.default_value - coord) / (axis.default_value - axis.min_value)
182    } else if coord > axis.default_value {
183        (coord - axis.default_value) / (axis.max_value - axis.default_value)
184    } else {
185        Fixed::from(0)
186    };
187
188    // After the default normalization calculation is performed, some results may be
189    // slightly outside the range [-1, +1]. Values must be clamped to this
190    // range.
191    normalised_value.clamp(Fixed::from(-1), Fixed::from(1))
192}
193
194impl<'b> ReadBinary for FvarTable<'b> {
195    type HostType<'a> = FvarTable<'a>;
196
197    fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
198        let scope = ctxt.scope();
199        let major_version = ctxt.read_u16be()?;
200        ctxt.check_version(major_version == 1)?;
201        let minor_version = ctxt.read_u16be()?;
202        let axes_array_offset = ctxt.read_u16be()?;
203        let _reserved = ctxt.read_u16be()?;
204        let axis_count = ctxt.read_u16be()?;
205        let axis_size = ctxt.read_u16be()?;
206        let instance_count = ctxt.read_u16be()?;
207        let instance_size = ctxt.read_u16be()?;
208        let instance_length = usize::from(instance_count) * usize::from(instance_size);
209        let mut data_ctxt = scope.offset(usize::from(axes_array_offset)).ctxt();
210        let axes = data_ctxt.read_array_stride(usize::from(axis_count), usize::from(axis_size))?;
211        let instance_array = data_ctxt.read_slice(instance_length)?;
212
213        Ok(FvarTable {
214            major_version,
215            minor_version,
216            axes,
217            instance_count,
218            instance_size,
219            instance_array,
220        })
221    }
222}
223
224/// Utility type for reading just the axis count from `fvar` table.
225pub struct FvarAxisCount;
226
227impl<'b> ReadBinary for FvarAxisCount {
228    type HostType<'a> = u16;
229
230    fn read<'a>(ctxt: &mut ReadCtxt<'a>) -> Result<Self::HostType<'a>, ParseError> {
231        let major_version = ctxt.read_u16be()?;
232        ctxt.check_version(major_version == 1)?;
233        let _minor_version = ctxt.read_u16be()?;
234        let _axes_array_offset = ctxt.read_u16be()?;
235        let _reserved = ctxt.read_u16be()?;
236        let axis_count = ctxt.read_u16be()?;
237
238        Ok(axis_count)
239    }
240}
241
242impl ReadFrom for VariationAxisRecord {
243    type ReadType = ((U32Be, Fixed, Fixed), (Fixed, U16Be, U16Be));
244
245    fn read_from(
246        ((axis_tag, min_value, default_value), (max_value, flags, axis_name_id)): (
247            (u32, Fixed, Fixed),
248            (Fixed, u16, u16),
249        ),
250    ) -> Self {
251        VariationAxisRecord {
252            axis_tag,
253            min_value,
254            default_value,
255            max_value,
256            flags,
257            axis_name_id,
258        }
259    }
260}
261
262impl ReadBinaryDep for InstanceRecord<'_> {
263    type Args<'a> = (usize, u16);
264    type HostType<'a> = InstanceRecord<'a>;
265
266    fn read_dep<'a>(
267        ctxt: &mut ReadCtxt<'a>,
268        (record_size, axis_count): (usize, u16),
269    ) -> Result<Self::HostType<'a>, ParseError> {
270        let axis_count = usize::from(axis_count);
271        let subfamily_name_id = ctxt.read_u16be()?;
272        let flags = ctxt.read_u16be()?;
273        let coordinates = ctxt.read_array(axis_count).map(UserTuple)?;
274        // If the record size is larger than the size of the subfamily_name_id, flags,
275        // and coordinates then the optional post_script_name_id is present.
276        let post_script_name_id = (record_size > axis_count * Fixed::SIZE + 4)
277            .then(|| ctxt.read_u16be())
278            .transpose()?;
279
280        Ok(InstanceRecord {
281            subfamily_name_id,
282            flags,
283            coordinates,
284            post_script_name_id,
285        })
286    }
287}
288
289impl OwnedTuple {
290    /// Borrow this value as a [Tuple].
291    pub fn as_tuple(&self) -> Tuple<'_> {
292        Tuple(&self.0)
293    }
294}
295
296impl std::ops::Deref for OwnedTuple {
297    type Target = [F2Dot14];
298
299    fn deref(&self) -> &Self::Target {
300        &self.0
301    }
302}
303
304impl<'a> Tuple<'a> {
305    /// Construct a `Tuple` from a pointer and length.
306    ///
307    /// ## Safety
308    ///
309    /// You must ensure all the requirements of [std::slice::from_raw_parts] are upheld in
310    /// addition to:
311    ///
312    /// - There must be exactly `fvar.axis_count` values.
313    /// - Values must be clamped to -1 to 1.
314    pub unsafe fn from_raw_parts(data: *const F2Dot14, length: usize) -> Tuple<'a> {
315        Tuple(std::slice::from_raw_parts(data, length))
316    }
317
318    /// Retrieve the instance value for the axis at `index`
319    pub fn get(&self, index: u16) -> Option<F2Dot14> {
320        self.0.get(usize::from(index)).copied()
321    }
322}
323
324impl std::ops::Deref for Tuple<'_> {
325    type Target = [F2Dot14];
326
327    fn deref(&self) -> &Self::Target {
328        self.0
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use crate::error::ReadWriteError;
336    use crate::font_data::FontData;
337    use crate::tables::{FontTableProvider, NameTable};
338    use crate::tag;
339    use crate::tests::read_fixture;
340
341    #[test]
342    fn fvar() {
343        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
344        let scope = ReadScope::new(&buffer);
345        let font_file = scope
346            .read::<FontData<'_>>()
347            .expect("unable to parse font file");
348        let table_provider = font_file
349            .table_provider(0)
350            .expect("unable to create font provider");
351        let fvar_data = table_provider
352            .read_table_data(tag::FVAR)
353            .expect("unable to read fvar table data");
354        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
355        let name_table_data = table_provider
356            .read_table_data(tag::NAME)
357            .expect("unable to read name table data");
358        let name_table = ReadScope::new(&name_table_data)
359            .read::<NameTable<'_>>()
360            .unwrap();
361
362        let expected = [
363            VariationAxisRecord {
364                axis_tag: tag!(b"wght"),
365                min_value: <Fixed as From<i32>>::from(100),
366                default_value: <Fixed as From<i32>>::from(400),
367                max_value: <Fixed as From<i32>>::from(900),
368                flags: 0,
369                axis_name_id: 279,
370            },
371            VariationAxisRecord {
372                axis_tag: tag!(b"wdth"),
373                min_value: <Fixed as From<f32>>::from(62.5),
374                default_value: <Fixed as From<i32>>::from(100),
375                max_value: <Fixed as From<i32>>::from(100),
376                flags: 0,
377                axis_name_id: 280,
378            },
379            VariationAxisRecord {
380                axis_tag: tag!(b"CTGR"),
381                min_value: <Fixed as From<i32>>::from(0),
382                default_value: <Fixed as From<i32>>::from(0),
383                max_value: <Fixed as From<i32>>::from(100),
384                flags: 0,
385                axis_name_id: 281,
386            },
387        ];
388        assert_eq!(fvar.axes().collect::<Vec<_>>(), expected);
389
390        let instances = fvar.instances().collect::<Result<Vec<_>, _>>().unwrap();
391        assert_eq!(instances.len(), 72);
392        let first = instances.first().unwrap();
393        let subfamily_name = name_table.string_for_id(first.subfamily_name_id).unwrap();
394        assert_eq!(subfamily_name, "Thin");
395        // axis="wght" value="100.0", axis="wdth" value="100.0", axis="CTGR" value="0.0"
396        let coordinates = [
397            <Fixed as From<f32>>::from(100.),
398            <Fixed as From<f32>>::from(100.),
399            <Fixed as From<f32>>::from(0.),
400        ];
401        assert_eq!(first.coordinates.0.iter().collect::<Vec<_>>(), coordinates);
402
403        let last = instances.last().unwrap();
404        let subfamily_name = name_table.string_for_id(last.subfamily_name_id).unwrap();
405        assert_eq!(subfamily_name, "Display ExtraCondensed Black");
406        //  axis="wght" value="900.0", axis="wdth" value="62.5", axis="CTGR"
407        // value="100.0"
408        let coordinates = [
409            <Fixed as From<f32>>::from(900.),
410            <Fixed as From<f32>>::from(62.5),
411            <Fixed as From<f32>>::from(100.),
412        ];
413        assert_eq!(last.coordinates.0.iter().collect::<Vec<_>>(), coordinates);
414    }
415
416    #[test]
417    fn test_fvar_normalization() -> Result<(), ReadWriteError> {
418        let buffer = read_fixture("tests/fonts/opentype/NotoSans-VF.abc.ttf");
419        let scope = ReadScope::new(&buffer);
420        let font_file = scope.read::<FontData<'_>>()?;
421        let provider = font_file.table_provider(0)?;
422        let fvar_data = provider
423            .read_table_data(tag::FVAR)
424            .expect("unable to read fvar table data");
425        let fvar = ReadScope::new(&fvar_data).read::<FvarTable<'_>>().unwrap();
426        let avar_data = provider.table_data(tag::AVAR)?;
427        let avar = avar_data
428            .as_ref()
429            .map(|avar_data| ReadScope::new(avar_data).read::<AvarTable<'_>>())
430            .transpose()?;
431        let name_table_data = provider
432            .read_table_data(tag::NAME)
433            .expect("unable to read name table data");
434        let name_table = ReadScope::new(&name_table_data)
435            .read::<NameTable<'_>>()
436            .unwrap();
437
438        // Pick an instance
439        let mut instance = None;
440        for inst in fvar.instances() {
441            let inst = inst?;
442            let subfamily = name_table.string_for_id(inst.subfamily_name_id);
443            if subfamily.as_deref() == Some("Display Condensed Thin") {
444                // - wght = min: 100, max: 900, default: 400
445                // - wdth = min: 62.5, max: 100, default: 100
446                // - CTGR = min: 0, max: 100, default: 0
447                //
448                // Coordinates: [100.0, 62.5, 100.0]
449                instance = Some(inst);
450                break;
451            }
452        }
453        let instance = instance.unwrap();
454
455        // The instance is a UserTuple record that needs be normalised into a ReadTuple
456        // record
457        let tuple = fvar.normalize(instance.coordinates.iter(), avar.as_ref())?;
458        assert_eq!(
459            tuple.0.as_slice(),
460            &[
461                F2Dot14::from(-1.0),
462                F2Dot14::from(-0.7000122),
463                F2Dot14::from(1.0)
464            ]
465        );
466
467        Ok(())
468    }
469
470    #[test]
471    fn test_default_normalization() {
472        // https://learn.microsoft.com/en-us/typography/opentype/spec/otvaroverview#avar-normalization-example
473        let axis = VariationAxisRecord {
474            axis_tag: tag!(b"wght"),
475            min_value: Fixed::from(100),
476            default_value: Fixed::from(400),
477            max_value: Fixed::from(900),
478            flags: 0,
479            axis_name_id: 0,
480        };
481        let user_coord = Fixed::from(250);
482        assert_eq!(default_normalize(&axis, user_coord), Fixed::from(-0.5))
483    }
484}