cranpose-render-common 0.1.129

Common rendering contracts for Cranpose
Documentation
use std::sync::Arc;

use cranpose_ui::text::TextStyle;

#[derive(Clone, Default)]
pub(crate) struct FontTracking(Arc<[(f32, f32)]>);

impl FontTracking {
    pub(crate) fn from_face(face: &ttf_parser::Face<'_>) -> Self {
        face.tables()
            .trak
            .and_then(|table| Self::from_table(table, f32::from(face.units_per_em())))
            .unwrap_or_default()
    }

    fn from_table(table: ttf_parser::trak::Table<'_>, units_per_em: f32) -> Option<Self> {
        let data = table.horizontal;
        let normal = data.tracks.into_iter().find(|track| track.value == 0.0)?;
        let mut points = Vec::with_capacity(usize::from(data.sizes.len()));
        for index in 0..data.sizes.len() {
            let size = data.sizes.get(index)?.0;
            if size <= 0.0 || points.last().is_some_and(|&(previous, _)| size <= previous) {
                return None;
            }
            points.push((size, f32::from(normal.values.get(index)?) / units_per_em));
        }
        Some(Self(points.into()))
    }

    fn at_size(&self, size: f32) -> f32 {
        if !size.is_finite() || size <= 0.0 || self.0.is_empty() {
            return 0.0;
        }
        let index = self.0.partition_point(|&(point_size, _)| point_size < size);
        let em = if index == 0 {
            self.0[0].1
        } else if index == self.0.len() {
            self.0[index - 1].1
        } else {
            let (low_size, low) = self.0[index - 1];
            let (high_size, high) = self.0[index];
            low + (high - low) * (size - low_size) / (high_size - low_size)
        };
        em * size
    }

    pub(crate) fn resolve(&self, style: &TextStyle, size: f32) -> f32 {
        if style.span_style.letter_spacing.is_unspecified() {
            self.at_size(size)
        } else {
            style.resolve_letter_spacing(size)
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const TABLE: &[u8] = include_bytes!("../tests/fixtures/default_tracking.trak");

    #[test]
    fn normal_tracking_interpolates_in_font_units_and_clamps_size_endpoints() {
        let table = ttf_parser::trak::Table::parse(TABLE).unwrap();
        let tracking = FontTracking::from_table(table, 2048.0).unwrap();
        for (size, units) in [
            (6.0, 38.0),
            (9.0, 38.0),
            (9.5, 31.0),
            (10.0, 24.0),
            (15.0, -11.0),
            (20.0, -46.0),
            (40.0, -46.0),
        ] {
            assert!((tracking.at_size(size) - units * size / 2048.0).abs() < 0.00001);
        }
        for size in [0.0, -1.0, f32::NAN, f32::INFINITY] {
            assert_eq!(tracking.at_size(size), 0.0);
        }
        assert_eq!(FontTracking::default().at_size(10.0), 0.0);
    }

    #[test]
    fn missing_normal_track_and_unordered_sizes_are_ignored() {
        for (offset, value) in [(20, 65536i32), (28, 0), (32, 8 * 65536)] {
            let mut bytes = TABLE.to_vec();
            bytes[offset..offset + 4].copy_from_slice(&value.to_be_bytes());
            let table = ttf_parser::trak::Table::parse(&bytes).unwrap();
            assert!(FontTracking::from_table(table, 2048.0).is_none());
        }
        for length in 0..TABLE.len() {
            let parsed = ttf_parser::trak::Table::parse(&TABLE[..length]);
            assert!(
                parsed
                    .and_then(|table| FontTracking::from_table(table, 2048.0))
                    .is_none()
            );
        }
    }
}