cranpose-render-common 0.9.6

Scene, renderer and hit-test contracts for Cranpose backends
Documentation
use std::{
    hash::{Hash, Hasher},
    sync::{Arc, Mutex, PoisonError},
};

use cranpose_core::hash::default as default_hash;
use ttf_parser::{
    Face, GlyphId, Tag,
    gdef::{GlyphClass, Table as GlyphDefinitions},
    gsub::{SingleSubstitution, SubstitutionSubtable},
    opentype_layout::{Coverage, Feature, LayoutTable, Lookup},
};

use crate::ascii_glyphs::AsciiGlyphs;

const MAX_FEATURE_VARIANTS: usize = 16;
const DEFAULT_SCRIPTS: [&[u8; 4]; 2] = [b"DFLT", b"latn"];

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct FontFeature {
    pub(crate) tag: [u8; 4],
    pub(crate) value: u32,
}

pub(crate) fn font_feature_settings(settings: &str) -> impl Iterator<Item = FontFeature> + '_ {
    settings.split(',').filter_map(parse_font_feature)
}

pub(crate) fn enabled_feature_tags(settings: &str) -> Vec<[u8; 4]> {
    let mut features: Vec<FontFeature> = Vec::new();
    for feature in font_feature_settings(settings) {
        match features.iter_mut().find(|known| known.tag == feature.tag) {
            Some(known) => known.value = feature.value,
            None => features.push(feature),
        }
    }
    let mut tags: Vec<[u8; 4]> = features
        .into_iter()
        .filter(|feature| feature.value != 0)
        .map(|feature| feature.tag)
        .collect();
    tags.sort_unstable();
    tags
}

fn parse_font_feature(entry: &str) -> Option<FontFeature> {
    let (tag, rest) = split_feature_tag(entry.trim())?;
    let tag: [u8; 4] = tag.as_bytes().try_into().ok()?;
    if !tag.iter().all(|byte| (0x20..=0x7e).contains(byte)) {
        return None;
    }
    Some(FontFeature {
        tag,
        value: parse_feature_value(rest.trim_start())?,
    })
}

fn split_feature_tag(entry: &str) -> Option<(&str, &str)> {
    match *entry.as_bytes().first()? {
        quote @ (b'"' | b'\'') => {
            if entry.as_bytes().get(5) != Some(&quote) {
                return None;
            }
            Some((entry.get(1..5)?, entry.get(6..)?))
        }
        _ => {
            let length = entry
                .bytes()
                .position(|byte| !byte.is_ascii_alphanumeric())
                .unwrap_or(entry.len());
            if length != 4 {
                return None;
            }
            entry.split_at_checked(length)
        }
    }
}

fn parse_feature_value(value: &str) -> Option<u32> {
    if value.is_empty() || value.eq_ignore_ascii_case("on") {
        return Some(1);
    }
    if value.eq_ignore_ascii_case("off") {
        return Some(0);
    }
    value
        .bytes()
        .all(|byte| byte.is_ascii_digit())
        .then(|| value.parse().ok())
        .flatten()
}

pub(crate) struct GlyphSubstitutions {
    tags: Box<[[u8; 4]]>,
    key: u64,
    pairs: Box<[(u16, u16)]>,
}

impl GlyphSubstitutions {
    pub(crate) fn build(face: &Face<'_>, tags: &[[u8; 4]]) -> Option<Self> {
        let gsub = face.tables().gsub?;
        let definitions = face.tables().gdef;
        let lookups: Vec<Box<[(u16, u16)]>> = feature_lookup_indices(&gsub, tags)
            .into_iter()
            .filter_map(|index| single_substitutions(gsub.lookups.get(index)?, definitions))
            .collect();
        let mut covered: Vec<u16> = lookups
            .iter()
            .flat_map(|lookup| lookup.iter().map(|&(glyph, _)| glyph))
            .collect();
        covered.sort_unstable();
        covered.dedup();
        let pairs: Box<[(u16, u16)]> = covered
            .into_iter()
            .filter_map(|glyph| {
                let substitute = lookups
                    .iter()
                    .fold(glyph, |glyph, lookup| substitute(lookup, glyph));
                (substitute != glyph).then_some((glyph, substitute))
            })
            .collect();
        if pairs.is_empty() {
            return None;
        }
        let mut state = default_hash::new();
        tags.hash(&mut state);
        Some(Self {
            tags: tags.into(),
            key: state.finish().max(1),
            pairs,
        })
    }

    pub(crate) fn apply(&self, glyph: ab_glyph::GlyphId) -> ab_glyph::GlyphId {
        ab_glyph::GlyphId(substitute(&self.pairs, glyph.0))
    }

    pub(crate) fn key(&self) -> u64 {
        self.key
    }

    pub(crate) fn tags(&self) -> &[[u8; 4]] {
        &self.tags
    }
}

fn substitute(pairs: &[(u16, u16)], glyph: u16) -> u16 {
    pairs
        .binary_search_by_key(&glyph, |&(covered, _)| covered)
        .ok()
        .and_then(|index| pairs.get(index))
        .map_or(glyph, |&(_, substitute)| substitute)
}

fn feature_lookup_indices(gsub: &LayoutTable<'_>, tags: &[[u8; 4]]) -> Vec<u16> {
    let mut indices: Vec<u16> = tags
        .iter()
        .filter_map(|tag| find_feature(gsub, Tag::from_bytes(tag)))
        .filter(|feature| {
            feature
                .lookup_indices
                .into_iter()
                .all(|index| is_single_substitution(gsub, index))
        })
        .flat_map(|feature| feature.lookup_indices)
        .collect();
    indices.sort_unstable();
    indices.dedup();
    indices
}

fn is_single_substitution(gsub: &LayoutTable<'_>, index: u16) -> bool {
    gsub.lookups.get(index).is_some_and(|lookup| {
        lookup
            .subtables
            .into_iter::<SubstitutionSubtable<'_>>()
            .all(|subtable| matches!(subtable, SubstitutionSubtable::Single(_)))
    })
}

fn find_feature<'a>(gsub: &LayoutTable<'a>, tag: Tag) -> Option<Feature<'a>> {
    DEFAULT_SCRIPTS
        .iter()
        .filter_map(|script| gsub.scripts.find(Tag::from_bytes(script))?.default_language)
        .flat_map(|language| language.feature_indices)
        .filter_map(|index| gsub.features.get(index))
        .find(|feature| feature.tag == tag)
        .or_else(|| gsub.features.into_iter().find(|feature| feature.tag == tag))
}

fn single_substitutions(
    lookup: Lookup<'_>,
    definitions: Option<GlyphDefinitions<'_>>,
) -> Option<Box<[(u16, u16)]>> {
    let mut pairs = Vec::new();
    for subtable in lookup.subtables.into_iter::<SubstitutionSubtable<'_>>() {
        let SubstitutionSubtable::Single(single) = subtable else {
            continue;
        };
        for_each_covered(single.coverage(), |glyph, coverage_index| {
            let substitute = match single {
                SingleSubstitution::Format1 { delta, .. } => {
                    Some(GlyphId(glyph.0.wrapping_add_signed(delta)))
                }
                SingleSubstitution::Format2 { substitutes, .. } => substitutes.get(coverage_index),
            };
            if let Some(substitute) = substitute
                && !lookup_skips(&lookup, definitions.as_ref(), glyph)
            {
                pairs.push((glyph.0, substitute.0));
            }
        });
    }
    pairs.sort_by_key(|&(glyph, _)| glyph);
    pairs.dedup_by_key(|&mut (glyph, _)| glyph);
    (!pairs.is_empty()).then(|| pairs.into_boxed_slice())
}

fn for_each_covered(coverage: Coverage<'_>, mut visit: impl FnMut(GlyphId, u16)) {
    match coverage {
        Coverage::Format1 { glyphs } => {
            for (index, glyph) in glyphs.into_iter().enumerate() {
                if let Ok(index) = u16::try_from(index) {
                    visit(glyph, index);
                }
            }
        }
        Coverage::Format2 { records } => {
            for record in records {
                for glyph in record.start.0..=record.end.0 {
                    if let Some(index) = record.value.checked_add(glyph - record.start.0) {
                        visit(GlyphId(glyph), index);
                    }
                }
            }
        }
    }
}

fn lookup_skips(
    lookup: &Lookup<'_>,
    definitions: Option<&GlyphDefinitions<'_>>,
    glyph: GlyphId,
) -> bool {
    let Some(definitions) = definitions else {
        return false;
    };
    let flags = lookup.flags;
    match definitions.glyph_class(glyph) {
        Some(GlyphClass::Base) => flags.ignore_base_glyphs(),
        Some(GlyphClass::Ligature) => flags.ignore_ligatures(),
        Some(GlyphClass::Mark) => {
            flags.ignore_marks() || mark_filtered_out(lookup, definitions, glyph)
        }
        _ => false,
    }
}

fn mark_filtered_out(
    lookup: &Lookup<'_>,
    definitions: &GlyphDefinitions<'_>,
    glyph: GlyphId,
) -> bool {
    match lookup.mark_filtering_set {
        Some(set) => !definitions.is_mark_glyph(glyph, Some(set)),
        None => {
            let class = lookup.flags.mark_attachment_type();
            class != 0 && u16::from(class) != definitions.glyph_mark_attachment_class(glyph)
        }
    }
}

#[derive(Clone)]
pub(crate) struct FeatureGlyphs {
    pub(crate) substitutions: Arc<GlyphSubstitutions>,
    pub(crate) ascii: Arc<AsciiGlyphs>,
}

pub(crate) struct FeatureVariants {
    base_ascii: Arc<AsciiGlyphs>,
    variants: Mutex<Vec<(Box<str>, Option<FeatureGlyphs>)>>,
}

impl FeatureVariants {
    pub(crate) fn new(base_ascii: Arc<AsciiGlyphs>) -> Self {
        Self {
            base_ascii,
            variants: Mutex::new(Vec::new()),
        }
    }

    pub(crate) fn base_ascii(&self) -> &Arc<AsciiGlyphs> {
        &self.base_ascii
    }

    pub(crate) fn get(&self, settings: &str, font_data: &[u8]) -> Option<FeatureGlyphs> {
        let mut variants = self.variants.lock().unwrap_or_else(PoisonError::into_inner);
        if let Some((_, glyphs)) = variants.iter().find(|(known, _)| **known == *settings) {
            return glyphs.clone();
        }
        let glyphs = build_feature_glyphs(settings, font_data, &variants);
        if variants.len() >= MAX_FEATURE_VARIANTS {
            variants.remove(0);
        }
        variants.push((settings.into(), glyphs.clone()));
        glyphs
    }
}

fn build_feature_glyphs(
    settings: &str,
    font_data: &[u8],
    known: &[(Box<str>, Option<FeatureGlyphs>)],
) -> Option<FeatureGlyphs> {
    let tags = enabled_feature_tags(settings);
    if tags.is_empty() {
        return None;
    }
    if let Some(glyphs) = known
        .iter()
        .filter_map(|(_, glyphs)| glyphs.as_ref())
        .find(|glyphs| glyphs.substitutions.tags() == tags.as_slice())
    {
        return Some(glyphs.clone());
    }
    let face = Face::parse(font_data, 0).ok()?;
    Some(FeatureGlyphs {
        substitutions: Arc::new(GlyphSubstitutions::build(&face, &tags)?),
        ascii: Arc::new(AsciiGlyphs::new()),
    })
}

#[cfg(test)]
#[path = "tests/font_features_tests.rs"]
mod tests;