use super::condition::Evaluator;
use super::{
FeatureLookups, FeatureLookupsFlags, FeatureVariations, LookupConditionRecord,
LookupVariationRecord,
};
use crate::{
types::{Compatible, F2Dot14, FixedSize},
FontData,
};
use alloc::{collections::BTreeSet, vec::Vec};
impl<'a> FeatureVariations<'a> {
pub fn lookup_variation_records(&self) -> Option<&'a [LookupVariationRecord]> {
if !self.version().compatible((1, 1)) {
return None;
}
let data = self.offset_data();
if u64::from(self.feature_variation_record_count()) * 8
> data.len().saturating_sub(8) as u64
{
return None;
}
let count = usize::try_from(self.lookup_variation_record_count()?).ok()?;
let start = self.lookup_variation_record_count_byte_range().end;
let end = start.checked_add(count.checked_mul(LookupVariationRecord::RAW_BYTE_LEN)?)?;
data.read_array(start..end).ok()
}
pub fn lookup_variations(&self) -> Option<LookupVariations<'a>> {
let records = self.lookup_variation_records()?;
let sorted = records
.windows(2)
.all(|pair| pair[0].feature_index() < pair[1].feature_index());
(!records.is_empty() && sorted).then(|| LookupVariations {
data: self.offset_data(),
records,
})
}
}
#[derive(Clone)]
pub struct LookupVariations<'a> {
data: FontData<'a>,
records: &'a [LookupVariationRecord],
}
impl<'a> LookupVariations<'a> {
pub fn apply<E>(
&self,
feature_index: u16,
coords: &[F2Dot14],
delta: impl FnMut(u32) -> Result<f64, E>,
lookups: &mut Vec<u16>,
) -> bool {
let Some(table) = self.feature_lookups(feature_index) else {
return false;
};
if !table
.flags()
.contains(FeatureLookupsFlags::ADD_DEFAULT_LOOKUPS)
{
lookups.clear();
}
let mut delta = delta;
let mut evaluator = Evaluator::new(coords, |index| {
delta(index).map_err(|_| crate::model::Error::Malformed)
});
let mut lists = LookupLists::new(&table);
for record in table.lookup_condition_records() {
if evaluator.evaluate_record(table.offset_data(), record).ok() == Some(true) {
lists.extend(record, lookups);
}
}
true
}
pub fn add_all(&self, feature_index: u16, lookups: &mut Vec<u16>) -> bool {
let Some(table) = self.feature_lookups(feature_index) else {
return false;
};
let mut lists = LookupLists::new(&table);
for record in table.lookup_condition_records() {
lists.extend(record, lookups);
}
true
}
fn feature_lookups(&self, feature_index: u16) -> Option<FeatureLookups<'a>> {
let index = self
.records
.binary_search_by_key(&feature_index, LookupVariationRecord::feature_index)
.ok()?;
let record = &self.records[index];
if record.feature_lookups_offset().to_u32() == 0 {
return None;
}
let table = record.feature_lookups(self.data).ok()?;
let records_end = usize::try_from(table.lookup_condition_count())
.ok()?
.checked_mul(LookupConditionRecord::RAW_BYTE_LEN)?
.checked_add(table.lookup_condition_count_byte_range().end)?;
(table.version().major == 1 && records_end <= table.offset_data().len()).then_some(table)
}
}
struct LookupLists<'a> {
data: FontData<'a>,
added: BTreeSet<u32>,
}
impl<'a> LookupLists<'a> {
fn new(table: &FeatureLookups<'a>) -> Self {
Self {
data: table.offset_data(),
added: BTreeSet::new(),
}
}
fn extend(&mut self, record: &LookupConditionRecord, lookups: &mut Vec<u16>) {
let offset = record.lookup_index_list_offset().to_u32();
if offset == 0 || !self.added.insert(offset) {
return;
}
if let Ok(list) = record.lookup_index_list(self.data) {
let indices = list.lookup_indices();
if indices.len() == usize::from(list.lookup_index_count()) {
lookups.extend(indices.iter().map(|index| index.get()));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{types::MajorMinor, FontRead, ReadError};
fn feature_lookups<'a>(
table: &FeatureVariations<'a>,
feature_index: u16,
) -> Option<Result<FeatureLookups<'a>, ReadError>> {
let records = table.lookup_variation_records()?;
let record = records
.iter()
.find(|record| record.feature_index() == feature_index)?;
Some(record.feature_lookups(table.offset_data()))
}
fn lookup_variation_table(flags: u16) -> alloc::vec::Vec<u8> {
let mut bytes = alloc::vec::Vec::new();
bytes.extend([0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]);
bytes.extend([0, 2, 0, 0, 0, 18]);
assert_eq!(bytes.len(), 18);
bytes.extend([0, 1, 0, 0]);
bytes.extend(flags.to_be_bytes());
bytes.extend([0, 0, 0, 2]);
bytes.extend([0, 0, 0, 26, 0, 0, 0, 34]);
bytes.extend([0, 0, 0, 0, 0, 0, 0, 40]);
assert_eq!(bytes.len() - 18, 26);
bytes.extend([0, 1, 0, 0, 0x20, 0, 0x40, 0]);
assert_eq!(bytes.len() - 18, 34);
bytes.extend([0, 2, 0, 7, 0, 8]);
bytes.extend([0, 1, 0, 9]);
bytes
}
fn no_delta(_: u32) -> Result<f64, core::convert::Infallible> {
Ok(0.0)
}
#[test]
fn lookup_variations_replace_feature_lookups_by_location() {
let bytes = lookup_variation_table(0);
let table = FeatureVariations::read(bytes.as_slice().into()).unwrap();
let variations = table.lookup_variations().unwrap();
let mut lookups = alloc::vec![1, 2];
assert!(variations.apply(2, &[F2Dot14::from_f32(0.75)], no_delta, &mut lookups));
assert_eq!(lookups, [7, 8, 9]);
let mut lookups = alloc::vec![1, 2];
assert!(variations.apply(2, &[F2Dot14::from_f32(0.25)], no_delta, &mut lookups));
assert_eq!(lookups, [9]);
let mut lookups = alloc::vec![1, 2];
assert!(!variations.apply(3, &[F2Dot14::from_f32(0.75)], no_delta, &mut lookups));
assert_eq!(lookups, [1, 2]);
let mut lookups = alloc::vec![1];
assert!(variations.add_all(2, &mut lookups));
assert_eq!(lookups, [1, 7, 8, 9]);
}
#[test]
fn lookup_variations_can_add_to_default_lookups() {
let bytes = lookup_variation_table(FeatureLookupsFlags::ADD_DEFAULT_LOOKUPS.bits());
let table = FeatureVariations::read(bytes.as_slice().into()).unwrap();
let mut lookups = alloc::vec![1, 2];
assert!(table.lookup_variations().unwrap().apply(
2,
&[F2Dot14::from_f32(0.75)],
no_delta,
&mut lookups
));
assert_eq!(lookups, [1, 2, 7, 8, 9]);
}
#[test]
fn lookup_variations_add_a_shared_list_once() {
let mut bytes = alloc::vec::Vec::new();
bytes.extend([0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 0, 0, 18]);
bytes.extend([0, 1, 0, 0, 0, 0, 0, 0, 0, 3]);
for _ in 0..3 {
bytes.extend([0, 0, 0, 0, 0, 0, 0, 34]);
}
bytes.extend([0, 2, 0, 7, 0, 8]);
let table = FeatureVariations::read(bytes.as_slice().into()).unwrap();
let variations = table.lookup_variations().unwrap();
let mut lookups = alloc::vec![];
assert!(variations.add_all(2, &mut lookups));
assert_eq!(lookups, [7, 8]);
let mut lookups = alloc::vec![];
assert!(variations.apply(2, &[], no_delta, &mut lookups));
assert_eq!(lookups, [7, 8]);
}
#[test]
fn lookup_variations_absent_or_unsorted() {
let v1_0 = [0, 1, 0, 0, 0, 0, 0, 0];
let table = FeatureVariations::read(v1_0.as_slice().into()).unwrap();
assert!(table.lookup_variations().is_none());
let v1_1 = [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0];
let table = FeatureVariations::read(v1_1.as_slice().into()).unwrap();
assert!(table.lookup_variations().is_none());
let unsorted = [
0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0,
];
let table = FeatureVariations::read(unsorted.as_slice().into()).unwrap();
assert!(table.lookup_variations().is_none());
}
#[test]
fn lookup_variations_versioned_empty_arrays() {
let v1_0 = [0, 1, 0, 0, 0, 0, 0, 0];
let table = FeatureVariations::read(v1_0.as_slice().into()).unwrap();
assert_eq!(table.version(), MajorMinor::VERSION_1_0);
assert_eq!(table.lookup_variation_record_count(), None);
assert!(table.lookup_variation_records().is_none());
assert!(feature_lookups(&table, 0).is_none());
let v1_1 = [0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0];
let table = FeatureVariations::read(v1_1.as_slice().into()).unwrap();
assert_eq!(table.version(), MajorMinor::VERSION_1_1);
assert_eq!(table.lookup_variation_record_count(), Some(0));
assert_eq!(table.lookup_variation_records().unwrap().len(), 0);
assert!(feature_lookups(&table, 0).is_none());
}
#[test]
fn lookup_variations_truncated_arrays_and_invalid_offsets() {
let bytes = [
0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 3, 0xff, 0xff, 0xff, 0xff,
];
for len in 8..bytes.len() {
let table = FeatureVariations::read(bytes[..len].into()).unwrap();
assert!(table.lookup_variation_records().is_none());
assert!(feature_lookups(&table, 3).is_none());
}
let table = FeatureVariations::read(bytes.as_slice().into()).unwrap();
assert_eq!(table.lookup_variation_records().unwrap().len(), 1);
assert_eq!(
feature_lookups(&table, 3).unwrap().err(),
Some(ReadError::OutOfBounds)
);
assert!(feature_lookups(&table, 2).is_none());
assert!(feature_lookups(&table, 4).is_none());
}
}