use std::collections::HashSet;
use font_types::{FixedSize, Offset16};
use read_fonts::tables::{gpos as rgpos, layout as rlayout};
use crate::{tables::layout::CoverageTable, write::TableData};
use super::{Graph, ObjectId};
pub(crate) fn split_mark_to_base(graph: &mut Graph, lookup: ObjectId) {
super::split_subtables(graph, lookup, split_mark_to_base_subtable)
}
fn split_mark_to_base_subtable(graph: &mut Graph, subtable: ObjectId) -> Option<Vec<ObjectId>> {
const BASE_SIZE: usize = 6 * u16::RAW_BYTE_LEN + u16::RAW_BYTE_LEN + u16::RAW_BYTE_LEN; let data = &graph.objects[&subtable];
if data.read_at::<u16>(0)? != 1 {
return None;
}
debug_assert!(data.reparse::<rgpos::MarkBasePosFormat1>().is_ok());
let mark_class_count: u16 = data.read_at(6).unwrap_or(0);
if mark_class_count == 0 {
return None;
}
let base_coverage_id = data.offsets[1].object;
let base_coverage_size = graph.objects[&base_coverage_id].bytes.len();
let min_subtable_size = BASE_SIZE + base_coverage_size;
let class_info = get_class_info(graph, subtable);
let base_array_off = &data.offsets[3];
let base_array_data = &graph.objects[&base_array_off.object];
debug_assert_eq!(class_info.len(), mark_class_count as usize);
let base_count: u16 = base_array_data.read_at(0).unwrap_or(0);
let mut partial_coverage_size = 4;
let mut accumulated = min_subtable_size;
let mut split_points = Vec::new();
let mut visited = HashSet::new();
for i in 0..mark_class_count {
let info = &class_info[i as usize];
partial_coverage_size += u16::RAW_BYTE_LEN * info.marks.len();
let mut accumulated_delta =
rgpos::MarkRecord::RAW_BYTE_LEN * info.marks.len()
+ Offset16::RAW_BYTE_LEN * base_count as usize;
accumulated_delta += compute_subgraph_size(&info.children, graph, &mut visited);
accumulated += accumulated_delta;
let total = accumulated + partial_coverage_size;
if total > super::MAX_TABLE_SIZE {
log::trace!("adding split at {i}");
split_points.push(i as usize);
accumulated = min_subtable_size + accumulated_delta;
partial_coverage_size = 4 + u16::RAW_BYTE_LEN * info.marks.len();
visited.clear();
}
}
log::debug!(
"nothing to split, size '{}'",
accumulated + partial_coverage_size
);
if split_points.is_empty() {
return None;
}
split_points.push(mark_class_count as _);
let mut new_subtables = Vec::new();
let mut prev_split = 0;
for next_split in split_points {
let new_subtable = split_off_mark_pos(graph, subtable, prev_split, next_split, &class_info);
prev_split = next_split;
new_subtables.push(graph.add_object(new_subtable));
}
Some(new_subtables)
}
fn split_off_mark_pos(
graph: &mut Graph,
subtable: ObjectId,
start: usize,
end: usize,
class_info: &[Mark2BaseClassInfo],
) -> TableData {
let mark_coverage_id = graph.objects[&subtable].offsets.first().unwrap().object;
let mark_coverage = &graph.objects[&mark_coverage_id];
let mark_coverage = mark_coverage.reparse::<rlayout::CoverageTable>().unwrap();
let data = &graph.objects[&subtable];
let base_coverage_id = data.offsets[1].object;
let mark_array_id = data.offsets[2].object;
let base_array_id = data.offsets[3].object;
let old_mark_array = &graph.objects[&mark_array_id];
let old_base_array = &graph.objects[&base_array_id];
let mark_class_count = (end - start) as u16;
let mut new_subtable = TableData::new(data.type_);
let mark_glyphs_by_cov_id: HashSet<_> = (start..end)
.flat_map(|class_idx| class_info[class_idx].marks.iter())
.copied()
.collect();
let new_mark_coverage: CoverageTable = mark_coverage
.iter()
.enumerate()
.filter_map(|(i, gid)| mark_glyphs_by_cov_id.contains(&i).then_some(gid))
.collect();
let new_mark_coverage = super::make_table_data(&new_mark_coverage);
let new_mark_array = split_off_mark_array(old_mark_array, start as u16, &mark_glyphs_by_cov_id);
let new_base_array = split_off_base_array(old_base_array, start, end, class_info.len());
let new_mark_coverage_id = graph.add_object(new_mark_coverage);
let new_mark_array_id = graph.add_object(new_mark_array);
let new_base_array_id = graph.add_object(new_base_array);
new_subtable.write(1u16); new_subtable.add_offset(new_mark_coverage_id, 2, 0);
new_subtable.add_offset(base_coverage_id, 2, 0);
new_subtable.write(mark_class_count);
new_subtable.add_offset(new_mark_array_id, 2, 0);
new_subtable.add_offset(new_base_array_id, 2, 0);
new_subtable
}
fn split_off_mark_array(
old_mark_array: &TableData,
first_class: u16,
mark_glyph_coverage_ids: &HashSet<usize>,
) -> TableData {
let mark_array = old_mark_array.reparse::<rgpos::MarkArray>().unwrap();
let mark_count = mark_glyph_coverage_ids.len() as u16;
let mut new_mark_array = TableData::new(old_mark_array.type_);
new_mark_array.write(mark_count);
for (i, mark_record) in mark_array.mark_records().iter().enumerate() {
if !mark_glyph_coverage_ids.contains(&i) {
continue;
}
let new_class = mark_record.mark_class() - first_class;
let anchor_offset = old_mark_array.offsets[i].object;
new_mark_array.write(new_class);
new_mark_array.add_offset(anchor_offset, 2, 0);
}
new_mark_array
}
fn split_off_base_array(
old_base_array: &TableData,
start: usize,
end: usize,
old_mark_class_count: usize,
) -> TableData {
let mut new_base_array = TableData::new(old_base_array.type_);
let base_count: u16 = old_base_array.read_at(0).unwrap_or(0);
new_base_array.write(base_count);
let base_array: rgpos::BaseArray = old_base_array
.reparse_with_args(old_mark_class_count as u16)
.unwrap();
let mut next_offset_idx = 0;
for base_record in base_array.base_records().iter() {
let base_record = base_record.unwrap();
for (mark_class, offset) in base_record.base_anchor_offsets().iter().enumerate() {
let has_offset = !offset.get().is_null();
let in_range = (start..end).contains(&mark_class);
if in_range {
if has_offset {
let id = old_base_array.offsets[next_offset_idx].object;
new_base_array.add_offset(id, 2, 0);
} else {
new_base_array.write(0u16);
}
}
if has_offset {
next_offset_idx += 1;
}
}
}
new_base_array
}
#[derive(Clone, Debug, Default)]
struct Mark2BaseClassInfo {
marks: HashSet<usize>,
children: Vec<ObjectId>,
}
fn compute_subgraph_size(
objects: &[ObjectId],
graph: &Graph,
visited: &mut HashSet<ObjectId>,
) -> usize {
objects
.iter()
.map(|id| {
if !visited.insert(*id) {
return 0;
}
let base_size = graph.objects[id].bytes.len();
let children_size = graph.objects[id]
.offsets
.iter()
.map(|id| {
debug_assert!(graph.objects[&id.object].offsets.is_empty());
if visited.insert(id.object) {
graph.objects[&id.object].bytes.len()
} else {
0
}
})
.sum::<usize>();
base_size + children_size
})
.sum()
}
fn get_class_info(graph: &Graph, subtable: ObjectId) -> Vec<Mark2BaseClassInfo> {
let data = &graph.objects[&subtable];
let mark_class_count: u16 = data.read_at(6).unwrap_or(0);
let mut class_to_info = vec![Mark2BaseClassInfo::default(); mark_class_count as usize];
let mark_array_off = &data.offsets[2];
assert_eq!(mark_array_off.pos, 8);
let mark_array_data = &graph.objects[&mark_array_off.object];
let mark_array = mark_array_data.reparse::<rgpos::MarkArray>().unwrap();
for (i, mark_record) in mark_array.mark_records().iter().enumerate() {
let mark_class = mark_record.mark_class();
if mark_class >= mark_class_count {
continue;
}
let anchor_table_id = mark_array_data.offsets[i].object;
class_to_info[mark_class as usize].marks.insert(i);
class_to_info[mark_class as usize]
.children
.push(anchor_table_id);
}
let base_array_off = &data.offsets[3];
assert_eq!(base_array_off.pos, 10);
let base_array_data = &graph.objects[&base_array_off.object];
for off in &base_array_data.offsets {
let offset_idx = (off.pos as usize - u16::RAW_BYTE_LEN) / Offset16::RAW_BYTE_LEN;
let mark_class = offset_idx % mark_class_count as usize;
class_to_info[mark_class].children.push(off.object)
}
class_to_info
}
#[cfg(test)]
mod tests {
use std::ops::Range;
use font_types::GlyphId16;
use read_fonts::{
tables::{gpos::PositionSubtables, layout::LookupFlag},
FontRead,
};
use crate::{
tables::{
gpos::{
builders::{AnchorBuilder, MarkToBaseBuilder},
AnchorTable, BaseArray, BaseRecord, ExtensionPosFormat1, ExtensionSubtable, Gpos,
MarkArray, MarkBasePosFormat1, MarkRecord, PositionLookup,
},
layout::{
builders::{Builder, LookupBuilder},
DeviceOrVariationIndex, Lookup, LookupList, LookupType, VariationIndex,
},
variations::ivs_builder::VariationStoreBuilder,
},
TableWriter,
};
use super::*;
#[test]
fn extended_mark_to_base_is_not_reparsed_as_format1() {
use crate::tables::gpos::{
AnchorFormat1, BaseArray2, BaseRecord2, MarkArray2, MarkBasePosFormat2, MarkRecord2,
};
let coverage: CoverageTable = [font_types::GlyphId::new(65536)].into_iter().collect();
let anchor = AnchorFormat1::new(10, 20);
let table = MarkBasePosFormat2::new(
coverage.clone(),
coverage,
MarkArray2::new(vec![MarkRecord2::new(0, anchor.clone().into())]),
BaseArray2::new(vec![BaseRecord2::new(vec![Some(anchor.into())])]),
);
let mut graph = crate::write::TableWriter::make_graph(&table);
let root = graph.root;
assert!(split_mark_to_base_subtable(&mut graph, root).is_none());
}
fn u16_to_i16(val: u16) -> i16 {
match i16::try_from(val) {
Ok(val) => val,
Err(_) => i16::MAX.saturating_sub_unsigned(val),
}
}
fn make_mark_array(
class_count: u16,
glyphs_per_class: u16,
include_var_index_tables: bool,
) -> MarkArray {
let mark_glyph_count = class_count * glyphs_per_class;
let records = (0..mark_glyph_count)
.map(|cov_idx| {
let mark_class = cov_idx / glyphs_per_class;
let val = u16_to_i16(cov_idx);
let anchor = if include_var_index_tables {
let var_idx = VariationIndex::new(cov_idx, cov_idx);
AnchorTable::format_3(val, val, Some(var_idx.into()), None)
} else {
AnchorTable::format_1(val, val)
};
MarkRecord::new(mark_class, anchor)
})
.collect();
MarkArray::new(records)
}
fn make_base_array(
base_count: u16,
mark_class_count: u16,
include_var_index_tables: bool,
) -> BaseArray {
let base_records = (0..base_count)
.map(|i| {
let mark_anchors = (0..mark_class_count)
.map(|j| {
(j % 2 == 0).then(|| {
let val = u16_to_i16(i * mark_class_count + j);
if include_var_index_tables {
let var_idx = VariationIndex::new(j, j);
AnchorTable::format_3(val, val, Some(var_idx.into()), None)
} else {
AnchorTable::format_1(val, val)
}
})
})
.collect();
BaseRecord::new(mark_anchors)
})
.collect();
BaseArray::new(base_records)
}
#[test]
fn split_mark_2_base() {
let _ = env_logger::builder().is_test(true).try_init();
const MARK_CLASS_COUNT: u16 = 100;
const MARKS_PER_CLASS: u16 = 4;
const N_BASES: u16 = 100;
const N_MARKS: u16 = MARK_CLASS_COUNT * MARKS_PER_CLASS;
const FIRST_BASE_GLYPH: u16 = 2;
const FIRST_MARK_GLYPH: u16 = 2000;
let mark_coverage = (FIRST_MARK_GLYPH..FIRST_MARK_GLYPH + N_MARKS)
.map(GlyphId16::new)
.collect();
let base_coverage = (FIRST_BASE_GLYPH..FIRST_BASE_GLYPH + N_BASES)
.map(GlyphId16::new)
.collect();
let mark_array = make_mark_array(MARK_CLASS_COUNT, MARKS_PER_CLASS, false);
let base_array = make_base_array(N_BASES, MARK_CLASS_COUNT, false);
let table = MarkBasePosFormat1::new(mark_coverage, base_coverage, mark_array, base_array);
let lookup = Lookup::new(LookupFlag::empty(), vec![table]);
let mut graph = TableWriter::make_graph(&lookup);
let id = graph.root;
assert!(graph.objects[&id].type_.is_promotable());
split_mark_to_base(&mut graph, id);
graph.remove_orphans();
assert!(graph.basic_sort());
let dumped = graph.serialize();
let read_back =
rlayout::Lookup::<rgpos::MarkBasePosFormat1>::read(dumped.as_slice().into()).unwrap();
let mark_cov: CoverageTable = read_back
.subtables()
.iter()
.flat_map(|sub| {
sub.ok()
.and_then(|sub| sub.mark_coverage().ok().map(|cov| cov.iter()))
})
.flatten()
.collect();
assert_eq!(&mark_cov, lookup.subtables[0].mark_coverage.as_ref());
let base_cov: CoverageTable = read_back
.subtables()
.iter()
.flat_map(|sub| {
sub.ok()
.and_then(|sub| sub.base_coverage().ok().map(|cov| cov.iter()))
})
.flatten()
.collect();
assert_eq!(&base_cov, lookup.subtables[0].base_coverage.as_ref());
let compare_old_and_new = |base_gid, mark_gid| {
let base_gid = GlyphId16::new(base_gid);
let mark_gid = GlyphId16::new(mark_gid);
let old_subtable = &lookup.subtables[0];
let base_cov_idx = base_gid.to_u16() - FIRST_BASE_GLYPH;
let mark_cov_idx = mark_gid.to_u16() - FIRST_MARK_GLYPH;
let orig_mark_record = &old_subtable.mark_array.mark_records[mark_cov_idx as usize];
let orig_base_anchor = &old_subtable.base_array.base_records[base_cov_idx as usize]
.base_anchors[orig_mark_record.mark_class as usize];
let new_subtable = read_back
.subtables()
.iter()
.find_map(|sub| {
let sub = sub.unwrap();
sub.mark_coverage()
.unwrap()
.get(mark_gid)
.is_some()
.then_some(sub)
})
.unwrap();
let new_mark_idx = new_subtable.mark_coverage().unwrap().get(mark_gid).unwrap();
let new_base_idx = new_subtable.base_coverage().unwrap().get(base_gid).unwrap();
let new_mark_array = new_subtable.mark_array().unwrap();
let new_mark_record = &new_mark_array.mark_records()[new_mark_idx as usize];
let new_mark_anchor = new_mark_record
.mark_anchor(new_mark_array.offset_data())
.unwrap();
let new_base_array = new_subtable.base_array().unwrap();
let new_base_anchor = new_base_array
.base_records()
.get(new_base_idx as usize)
.unwrap()
.base_anchors(new_base_array.offset_data())
.get(new_mark_record.mark_class() as usize)
.transpose()
.unwrap();
fn get_f1_anchor_x_coords(old: &AnchorTable, new: &rgpos::AnchorTable) -> (i16, i16) {
match (old, new) {
(AnchorTable::Format1(old), rgpos::AnchorTable::Format1(new)) => {
(old.x_coordinate, new.x_coordinate())
}
_ => panic!("only format 1 here"),
}
}
let (old_mark_x, new_mark_x) =
get_f1_anchor_x_coords(orig_mark_record.mark_anchor.as_ref(), &new_mark_anchor);
assert_eq!(old_mark_x, new_mark_x);
let (old_base_x, new_base_x) = orig_base_anchor
.as_ref()
.zip(new_base_anchor.as_ref())
.map(|(old, new)| get_f1_anchor_x_coords(old, new))
.unwrap_or_default();
assert_eq!(old_base_x, new_base_x);
};
for base in [2, 3, 50, 51, 100, 101] {
for mark in [2000, 2001, 2222, 2211, 2398, 2399] {
compare_old_and_new(base, mark);
}
}
}
#[test]
fn full_split_including_variation_index_tables() {
let _ = env_logger::builder().is_test(true).try_init();
full_split_including_variation_index_tables_impl(false)
}
#[test]
fn full_split_in_extension_lookup() {
let _ = env_logger::builder().is_test(true).try_init();
full_split_including_variation_index_tables_impl(true)
}
fn full_split_including_variation_index_tables_impl(use_extension: bool) {
const MARK_CLASS_COUNT: u16 = 120;
const MARKS_PER_CLASS: u16 = 4;
const N_BASES: u16 = 500;
const N_MARKS: u16 = MARK_CLASS_COUNT * MARKS_PER_CLASS;
const FIRST_BASE_GLYPH: u16 = 2;
const FIRST_MARK_GLYPH: u16 = 2000;
let mark_coverage = (FIRST_MARK_GLYPH..FIRST_MARK_GLYPH + N_MARKS)
.map(GlyphId16::new)
.collect();
let base_coverage = (FIRST_BASE_GLYPH..FIRST_BASE_GLYPH + N_BASES)
.map(GlyphId16::new)
.collect();
let mark_array = make_mark_array(MARK_CLASS_COUNT, MARKS_PER_CLASS, true);
let base_array = make_base_array(N_BASES, MARK_CLASS_COUNT, true);
let table = MarkBasePosFormat1::new(mark_coverage, base_coverage, mark_array, base_array);
let mark_cov_idx_to_test = 72;
let orig_anchor = &table.mark_array.mark_records[mark_cov_idx_to_test].mark_anchor;
match orig_anchor.as_ref() {
AnchorTable::Format3(anchor) => match anchor.x_device.as_ref() {
Some(DeviceOrVariationIndex::VariationIndex(varidx))
if varidx.delta_set_outer_index == mark_cov_idx_to_test as u16 => {}
_ => panic!("bad assumptions"),
},
_ => panic!("very bad assumptions"),
};
let lookup = if use_extension {
PositionLookup::Extension(Lookup::new(
LookupFlag::empty(),
vec![ExtensionSubtable::MarkToBase(ExtensionPosFormat1::new(
LookupType::MARK_TO_BASE,
table.into(),
))],
))
} else {
PositionLookup::MarkToBase(Lookup::new(LookupFlag::empty(), vec![table.into()]))
};
let lookup_list = LookupList::new(vec![lookup]);
let bytes = crate::dump_table(&lookup_list).unwrap();
let read_back = rgpos::PositionLookupList::from(
rgpos::OriginalPositionLookupList::read(bytes.as_slice().into()).unwrap(),
);
assert_eq!(read_back.lookup_count(), 1);
let lookup = read_back.lookups().get(0).unwrap();
if use_extension {
assert_eq!(lookup.lookup_type(), LookupType::GPOS_EXT_TYPE);
}
let subtables: Vec<_> = match lookup.subtables().unwrap() {
PositionSubtables::MarkToBase(subs) => subs
.iter()
.map(|sub| match sub.unwrap() {
rgpos::MarkBasePos::Format1(t) => t,
_ => panic!("wrong subtable format"),
})
.collect(),
_ => panic!("wrong lookup type"),
};
assert_eq!(subtables.len(), 7);
let n_marks: u32 = subtables
.iter()
.map(|sub| sub.mark_coverage().unwrap().iter().count() as u32)
.sum();
assert_eq!(n_marks, N_MARKS as u32);
let gid = GlyphId16::new(FIRST_MARK_GLYPH + mark_cov_idx_to_test as u16);
let subtable = subtables
.iter()
.find(|sub| sub.mark_coverage().unwrap().get(gid).is_some())
.expect("should exist in some subtable");
let new_cov_idx = subtable.mark_coverage().unwrap().get(gid).unwrap();
let mark_array = subtable.mark_array().unwrap();
let mark_record = &mark_array.mark_records()[new_cov_idx as usize];
let mark_anchor = mark_record.mark_anchor(mark_array.offset_data()).unwrap();
let rgpos::AnchorTable::Format3(mark_anchor) = mark_anchor else {
panic!("wrong format")
};
assert!(
matches!(mark_anchor.x_device().transpose().unwrap(), Some(rgpos::DeviceOrVariationIndex::VariationIndex(varidx)) if varidx.delta_set_outer_index() == mark_cov_idx_to_test as u16)
);
}
#[test]
fn split_lookup_with_empty_subtable() {
let _ = env_logger::builder().is_test(true).try_init();
split_lookup_with_empty_subtable_impl(false)
}
#[test]
fn split_extension_lookup_with_empty_subtable() {
let _ = env_logger::builder().is_test(true).try_init();
split_lookup_with_empty_subtable_impl(true)
}
fn split_lookup_with_empty_subtable_impl(use_extension: bool) {
const N_CLASSES: u16 = 100;
const N_BASES: u16 = 100;
const FIRST_MARK_GLYPH: u16 = 1000;
let mut builder = LookupBuilder::<MarkToBaseBuilder>::new(LookupFlag::empty(), None);
let subtable = builder.last_mut().unwrap();
for class in 0..N_CLASSES {
let mark = GlyphId16::new(FIRST_MARK_GLYPH + class);
let anchor = AnchorBuilder::new(-1, class as _);
subtable
.insert_mark(mark, &class.to_string(), anchor)
.unwrap();
}
for base in 0..N_BASES {
for class in 0..N_CLASSES {
let anchor = AnchorBuilder::new(base as _, class as _);
subtable.insert_base(GlyphId16::new(base), &class.to_string(), anchor);
}
}
let mut lookup = builder.build(&mut VariationStoreBuilder::new(0));
lookup.subtables.push(MarkBasePosFormat1::default().into());
let lookup = if use_extension {
let subtables = lookup
.subtables
.into_iter()
.map(|sub| {
ExtensionSubtable::MarkToBase(ExtensionPosFormat1::new(
LookupType::MARK_TO_BASE,
sub.into_inner().into(),
))
})
.collect();
PositionLookup::Extension(Lookup::new(LookupFlag::empty(), subtables))
} else {
lookup.into()
};
let gpos = Gpos::new(
Default::default(),
Default::default(),
LookupList::new(vec![lookup]),
);
let bytes = crate::dump_table(&gpos).unwrap();
let gpos = rgpos::Gpos::read(bytes.as_slice().into()).unwrap();
let lookup = gpos.lookup_list().unwrap().lookups().get(0).unwrap();
let subtables: Vec<_> = match lookup.subtables().unwrap() {
PositionSubtables::MarkToBase(subs) => subs
.iter()
.map(|sub| match sub.unwrap() {
rgpos::MarkBasePos::Format1(sub) => sub,
_ => panic!("wrong subtable format"),
})
.collect(),
_ => panic!("wrong lookup type"),
};
assert!(subtables.len() > 2, "expected the first subtable to split");
let empty = subtables.last().unwrap();
assert_eq!(empty.mark_class_count(), 0);
assert_eq!(empty.mark_coverage().unwrap().iter().count(), 0);
let n_marks: usize = subtables
.iter()
.map(|sub| sub.mark_coverage().unwrap().iter().count())
.sum();
assert_eq!(n_marks, N_CLASSES as usize);
}
#[test]
fn class_info_with_null_base_anchors() {
let mark_array = MarkArray::new(
(0..3)
.map(|class| MarkRecord::new(class, AnchorTable::format_1(-1, class as _)))
.collect(),
);
let anchor = |x| Some(AnchorTable::format_1(x, 0));
let base_array = BaseArray::new(vec![
BaseRecord::new(vec![anchor(10), None, None]),
BaseRecord::new(vec![anchor(20), None, None]),
BaseRecord::new(vec![anchor(30), None, anchor(40)]),
]);
let mark_coverage = (10..13).map(GlyphId16::new).collect();
let base_coverage = (1..4).map(GlyphId16::new).collect();
let table = MarkBasePosFormat1::new(mark_coverage, base_coverage, mark_array, base_array);
let graph = TableWriter::make_graph(&table);
let class_info = get_class_info(&graph, graph.root);
let base_anchor_xs: Vec<Vec<i16>> = class_info
.iter()
.map(|info| {
let mut xs: Vec<_> = info
.children
.iter()
.map(|id| {
let anchor = graph.objects[id].reparse::<rgpos::AnchorFormat1>();
anchor.unwrap().x_coordinate()
})
.filter(|x| *x != -1)
.collect();
xs.sort();
xs
})
.collect();
assert_eq!(base_anchor_xs, [vec![10, 20, 30], vec![], vec![40]]);
}
#[test]
fn test_my_test_helper() {
assert_eq!(u16_to_i16(1), 1);
assert_eq!(u16_to_i16(32767), 32767);
assert_eq!(u16_to_i16(32768), -1);
assert_eq!(u16_to_i16(u16::MAX), i16::MIN);
}
#[test]
fn split_mark_array() {
const N_GLYPHS: u16 = 900;
const N_CLASSES: u16 = 75;
const GLYPHS_PER_CLASS: u16 = N_GLYPHS / N_CLASSES;
const SPLIT_CLASS_RANGE: Range<u16> = 20..25;
let mark_array = make_mark_array(N_CLASSES, GLYPHS_PER_CLASS, false);
let graph = TableWriter::make_graph(&mark_array);
let data = &graph.objects[&graph.root];
let mark_glyph_coverage_ids = mark_array
.mark_records
.iter()
.enumerate()
.filter_map(|(i, rec)| SPLIT_CLASS_RANGE.contains(&rec.mark_class).then_some(i))
.collect::<HashSet<_>>();
assert_eq!(
mark_glyph_coverage_ids.len(),
SPLIT_CLASS_RANGE.len() * GLYPHS_PER_CLASS as usize
);
let result = split_off_mark_array(data, SPLIT_CLASS_RANGE.start, &mark_glyph_coverage_ids);
assert_eq!(
result.offsets.len(),
GLYPHS_PER_CLASS as usize * SPLIT_CLASS_RANGE.len()
);
let reparsed = result.reparse::<rgpos::MarkArray>().unwrap();
for (i, rec) in reparsed.mark_records().iter().enumerate() {
let exp_class = i as u16 / GLYPHS_PER_CLASS;
assert_eq!(rec.mark_class(), exp_class);
}
for (i, offset) in result.offsets.iter().enumerate() {
let gid_delta = SPLIT_CLASS_RANGE.start * GLYPHS_PER_CLASS;
let gid = i as u16 + gid_delta;
let anchor_val = u16_to_i16(gid);
let anchor_table = &graph.objects[&offset.object];
let anchor_table = anchor_table.reparse::<rgpos::AnchorFormat1>().unwrap();
assert_eq!(anchor_table.x_coordinate(), anchor_val);
}
}
#[test]
fn split_base_array() {
const N_CLASSES: u16 = 20;
const N_BASES: u16 = 10;
const SPLIT_CLASS_RANGE: Range<u16> = 15..20;
let base_array = make_base_array(N_BASES, N_CLASSES, false);
let graph = TableWriter::make_graph(&base_array);
let data = &graph.objects[&graph.root];
let result = split_off_base_array(
data,
SPLIT_CLASS_RANGE.start as _,
SPLIT_CLASS_RANGE.end as _,
N_CLASSES as _,
);
assert_eq!(result.read_at::<u16>(0).unwrap(), N_BASES);
let mut idx = 0;
for base in 0..N_BASES {
for mark_class in 0..SPLIT_CLASS_RANGE.len() as u16 {
let original_mark_class = mark_class + SPLIT_CLASS_RANGE.start;
if original_mark_class % 2 == 0 {
let anchor_id = result.offsets[idx].object;
let anchor = &graph.objects[&anchor_id];
let anchor = anchor.reparse::<rgpos::AnchorFormat1>().unwrap();
let exp_val = u16_to_i16((base * N_CLASSES) + original_mark_class);
assert_eq!(
anchor.x_coordinate(),
exp_val,
"base {base} mark {mark_class} {anchor_id:?}"
);
idx += 1;
}
}
}
}
}