#[path = "./value_record.rs"]
mod value_record;
#[cfg(feature = "std")]
mod closure;
use crate::array::ComputedArray;
pub use super::layout::{
ClassDef, CoverageTable, Device, DeviceOrVariationIndex, FeatureList, FeatureVariations,
Lookup, ScriptList,
};
use super::layout::{ExtensionLookup, LookupFlag, Subtables};
pub use value_record::ValueRecord;
#[cfg(test)]
#[path = "../tests/test_gpos.rs"]
mod spec_tests;
include!("../../generated/generated_gpos.rs");
pub type PositionLookupList<'a> = super::layout::LookupList<'a, PositionLookup<'a>>;
pub type PositionSequenceContext<'a> = super::layout::SequenceContext<'a>;
pub type PositionChainContext<'a> = super::layout::ChainedSequenceContext<'a>;
impl<'a> AnchorTable<'a> {
pub fn x_device(&self) -> Option<Result<DeviceOrVariationIndex<'a>, ReadError>> {
match self {
AnchorTable::Format3(inner) => inner.x_device(),
_ => None,
}
}
pub fn y_device(&self) -> Option<Result<DeviceOrVariationIndex<'a>, ReadError>> {
match self {
AnchorTable::Format3(inner) => inner.y_device(),
_ => None,
}
}
}
impl<'a, T: FontRead<'a, Args = ()>> ExtensionLookup<'a, T> for ExtensionPosFormat1<'a, T> {
fn extension(&self) -> Result<T, ReadError> {
self.extension()
}
}
type PosSubtables<'a, T> = Subtables<'a, T, ExtensionPosFormat1<'a, T>>;
pub enum PositionSubtables<'a> {
Single(PosSubtables<'a, SinglePos<'a>>),
Pair(PosSubtables<'a, PairPos<'a>>),
Cursive(PosSubtables<'a, CursivePosFormat1<'a>>),
MarkToBase(PosSubtables<'a, MarkBasePosFormat1<'a>>),
MarkToLig(PosSubtables<'a, MarkLigPosFormat1<'a>>),
MarkToMark(PosSubtables<'a, MarkMarkPosFormat1<'a>>),
Contextual(PosSubtables<'a, PositionSequenceContext<'a>>),
ChainContextual(PosSubtables<'a, PositionChainContext<'a>>),
EmptyExtension,
}
impl<'a> PositionLookup<'a> {
pub fn lookup_flag(&self) -> LookupFlag {
self.of_unit_type().lookup_flag()
}
pub fn lookup_type(&self) -> u16 {
self.of_unit_type().lookup_type()
}
pub fn mark_filtering_set(&self) -> Option<u16> {
self.of_unit_type().mark_filtering_set()
}
pub fn subtables(&self) -> Result<PositionSubtables<'a>, ReadError> {
let raw_lookup = self.of_unit_type();
let offsets = raw_lookup.subtable_offsets();
let data = raw_lookup.offset_data();
match raw_lookup.lookup_type() {
1 => Ok(PositionSubtables::Single(Subtables::new(offsets, data))),
2 => Ok(PositionSubtables::Pair(Subtables::new(offsets, data))),
3 => Ok(PositionSubtables::Cursive(Subtables::new(offsets, data))),
4 => Ok(PositionSubtables::MarkToBase(Subtables::new(offsets, data))),
5 => Ok(PositionSubtables::MarkToLig(Subtables::new(offsets, data))),
6 => Ok(PositionSubtables::MarkToMark(Subtables::new(offsets, data))),
7 => Ok(PositionSubtables::Contextual(Subtables::new(offsets, data))),
8 => Ok(PositionSubtables::ChainContextual(Subtables::new(
offsets, data,
))),
9 => {
let Some(lookup_type) = offsets.iter().find_map(|off| {
off.get()
.resolve::<ExtensionPosFormat1<()>>(data)
.ok()
.map(|ext| ext.extension_lookup_type())
}) else {
return Ok(PositionSubtables::EmptyExtension);
};
match lookup_type {
1 => Ok(PositionSubtables::Single(Subtables::new_ext(offsets, data))),
2 => Ok(PositionSubtables::Pair(Subtables::new_ext(offsets, data))),
3 => Ok(PositionSubtables::Cursive(Subtables::new_ext(
offsets, data,
))),
4 => Ok(PositionSubtables::MarkToBase(Subtables::new_ext(
offsets, data,
))),
5 => Ok(PositionSubtables::MarkToLig(Subtables::new_ext(
offsets, data,
))),
6 => Ok(PositionSubtables::MarkToMark(Subtables::new_ext(
offsets, data,
))),
7 => Ok(PositionSubtables::Contextual(Subtables::new_ext(
offsets, data,
))),
8 => Ok(PositionSubtables::ChainContextual(Subtables::new_ext(
offsets, data,
))),
other => Err(ReadError::InvalidFormat(other as _)),
}
}
other => Err(ReadError::InvalidFormat(other as _)),
}
}
}
impl<'a> PairSet<'a> {
pub fn find_pair_value_record(&self, second_glyph: GlyphId16) -> Option<PairValueRecord<'a>> {
let records = self.pair_value_records();
let (mut lo, mut hi) = (0usize, records.len());
while lo < hi {
#[allow(clippy::manual_midpoint)]
let mid = (lo + hi) / 2;
let record = records.get(mid).ok()?;
match record.second_glyph().cmp(&second_glyph) {
core::cmp::Ordering::Less => lo = mid + 1,
core::cmp::Ordering::Greater => hi = mid,
core::cmp::Ordering::Equal => return Some(record),
}
}
None
}
}
#[derive(Copy, Clone)]
pub struct ClassValueRecords<'a> {
data: FontData<'a>,
start: u32,
record_size: u32,
format1_len: u32,
class1_count: u16,
class2_count: u16,
format1: ValueFormat,
format2: ValueFormat,
}
impl<'a> ClassValueRecords<'a> {
#[inline]
pub fn class1_count(&self) -> u16 {
self.class1_count
}
#[inline]
pub fn class2_count(&self) -> u16 {
self.class2_count
}
#[inline]
pub fn get(&self, class1: u16, class2: u16) -> Option<[ValueRecord<'a>; 2]> {
if class1 >= self.class1_count || class2 >= self.class2_count {
return None;
}
let record_offset = self.start as usize
+ class1 as usize * self.record_size as usize * self.class2_count as usize
+ class2 as usize * self.record_size as usize;
Some([
ValueRecord::new(self.data, record_offset, self.format1),
ValueRecord::new(
self.data,
record_offset + self.format1_len as usize,
self.format2,
),
])
}
}
impl<'a> PairPosFormat2<'a> {
pub fn class_value_records(&self) -> ClassValueRecords<'a> {
let format1 = self.value_format1();
let format2 = self.value_format2();
let format1_len = format1.record_byte_len();
ClassValueRecords {
data: self.offset_data(),
start: self.class1_records_byte_range().start as u32,
record_size: (format1_len + format2.record_byte_len()) as u32,
format1_len: format1_len as u32,
class1_count: self.class1_count(),
class2_count: self.class2_count(),
format1,
format2,
}
}
#[inline]
pub fn value_record_refs(&self, class1: u16, class2: u16) -> Option<[ValueRecord<'a>; 2]> {
self.class_value_records().get(class1, class2)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_same_record(a: ValueRecord, b: ValueRecord) {
assert_eq!(a.format(), b.format());
assert_eq!(a.bytes(), b.bytes());
assert_eq!(a, b);
}
#[test]
fn pair_pos2_hoisted_matches_nested() {
let data = FontData::new(font_test_data::gpos::PAIRPOSFORMAT2);
let table = PairPosFormat2::read(data).unwrap();
let class1_count = table.class1_count();
let class2_count = table.class2_count();
assert!(class1_count > 0 && class2_count > 0);
let hoisted = table.class_value_records();
let rows = table.class1_records();
for class1 in 0..class1_count {
let row = rows.get(class1 as usize).unwrap();
let cells = row.class2_records();
for class2 in 0..class2_count {
let cell = cells.get(class2 as usize).unwrap();
let [a, b] = hoisted.get(class1, class2).unwrap();
assert_same_record(a, cell.value_record1);
assert_same_record(b, cell.value_record2);
}
}
assert!(hoisted.get(class1_count, 0).is_none());
assert!(hoisted.get(0, class2_count).is_none());
}
#[test]
fn pair_set_find_matches_scan() {
let data = FontData::new(font_test_data::gpos::PAIRPOSFORMAT1);
let table = PairPosFormat1::read(data).unwrap();
let mut seen = 0;
for set_offset in table.pair_set_offsets() {
let pair_set = set_offset
.get()
.resolve_with_args::<PairSet>(data, (table.value_format1(), table.value_format2()))
.unwrap();
let records = pair_set.pair_value_records();
assert!(!records.is_empty());
for i in 0..records.len() {
let want = records.get(i).unwrap();
let found = pair_set
.find_pair_value_record(want.second_glyph())
.unwrap();
assert_eq!(found.second_glyph(), want.second_glyph());
assert_same_record(found.value_record1, want.value_record1);
assert_same_record(found.value_record2, want.value_record2);
seen += 1;
}
assert!(pair_set
.find_pair_value_record(GlyphId16::new(0xFFFF))
.is_none());
}
assert!(seen > 0);
}
#[test]
fn value_record_resolves_devices_against_its_table() {
let data = FontData::new(font_test_data::gpos::VALUEFORMATTABLE);
let table = SinglePosFormat1::read(data).unwrap();
let record = table.value_record();
assert!(record
.format()
.intersects(ValueFormat::ANY_DEVICE_OR_VARIDX));
assert!(record.offset() > 0);
assert!(record.x_placement_device().unwrap().is_ok());
assert!(record.y_advance_device().unwrap().is_ok());
}
#[test]
fn single_pos2_records_are_evenly_spaced() {
let data = FontData::new(font_test_data::gpos::SINGLEPOSFORMAT2);
let table = SinglePosFormat2::read(data).unwrap();
let records = table.value_records();
let stride = table.value_format().record_byte_len();
assert!(records.len() > 1);
let first = records.get(0).unwrap();
for i in 0..records.len() {
let rec = records.get(i).unwrap();
assert_eq!(rec.offset(), first.offset() + i * stride);
assert_eq!(rec.format(), table.value_format());
}
assert!(records.get(records.len()).is_err());
}
#[test]
fn default_for_generics() {
let ExtensionSubtable::Single(inner) = ExtensionSubtable::default() else {
panic!("this is quite bad");
};
assert_eq!(inner.extension_lookup_type(), 0);
let SinglePos::Format1(_hmm) = inner.extension().unwrap_or_default() else {
panic!("unexpected");
};
}
}