use std::collections::HashMap;
use font_types::GlyphId16;
use read_fonts::tables::{
gpos::{self as rgpos},
layout as rlayout,
};
use super::{Graph, ObjectId};
use crate::{tables::layout as wlayout, write::TableData, FontWrite, TableWriter};
mod mark2base;
mod pairpos;
pub(super) use mark2base::split_mark_to_base;
pub(super) use pairpos::split_pair_pos;
const MAX_TABLE_SIZE: usize = u16::MAX as usize;
fn split_subtables(
graph: &mut Graph,
lookup: ObjectId,
split_fn: fn(&mut Graph, ObjectId) -> Option<Vec<ObjectId>>,
) {
let data = graph.objects.remove(&lookup).unwrap();
debug_assert!(
data.reparse::<rgpos::PositionLookup>().is_ok(),
"table splitting is only relevant for GPOS?"
);
log::debug!("trying to split subtables in '{}'", data.type_);
let mut new_subtables = HashMap::new();
for (i, subtable) in data.offsets.iter().enumerate() {
if let Some(split_subtables) = split_fn(graph, subtable.object) {
log::trace!("produced {} splits for subtable {i}", split_subtables.len());
new_subtables.insert(subtable.object, split_subtables);
}
}
if new_subtables.is_empty() {
graph.objects.insert(lookup, data);
log::debug!("Splitting produced no new subtables");
return;
}
let n_new_subtables = new_subtables
.values()
.map(|ids| ids.len() - 1)
.sum::<usize>();
log::debug!("Splitting produced {n_new_subtables} new subtables");
let n_total_subtables: u16 = (data.offsets.len() + n_new_subtables).try_into().unwrap();
let generic_lookup: rlayout::Lookup<()> = data.reparse().unwrap();
let mut new_data = TableData::new(data.type_);
new_data.write(generic_lookup.lookup_type());
new_data.write(generic_lookup.lookup_flag());
new_data.write(n_total_subtables);
let old_mark_filter_set = generic_lookup.mark_filtering_set();
for sub in data.offsets {
match new_subtables.get(&sub.object) {
Some(new) => new.iter().for_each(|id| new_data.add_offset(*id, 2, 0)),
None => new_data.add_offset(sub.object, 2, 0),
}
}
if let Some(mark_filtering_set) = old_mark_filter_set {
new_data.write(mark_filtering_set);
}
graph.nodes.get_mut(&lookup).unwrap().size = new_data.bytes.len() as _;
graph.objects.insert(lookup, new_data);
}
fn split_coverage(coverage: &rlayout::CoverageTable, start: u16, end: u16) -> TableData {
assert!(start <= end);
let len = end - start;
let mut data = TableData::default();
match coverage {
rlayout::CoverageTable::Format1(table) => {
data.write(1u16);
data.write(len);
for gid in &table.glyph_array()[start as usize..end as usize] {
data.write(gid.get());
}
}
rlayout::CoverageTable::Format2(table) => {
let records = table
.range_records()
.iter()
.filter_map(|record| split_range_record(record, start, end - 1))
.collect::<Vec<_>>();
data.write(2u16);
data.write(records.len() as u16);
for record in records {
data.write(record.start_glyph_id);
data.write(record.end_glyph_id);
data.write(record.start_coverage_index);
}
}
}
data
}
fn split_range_record(
record: &rlayout::RangeRecord,
start: u16,
end: u16,
) -> Option<wlayout::RangeRecord> {
let cov_start = record.start_coverage_index();
let len = record.end_glyph_id().to_u16() - record.start_glyph_id().to_u16();
let cov_range = cov_start..cov_start + len;
if cov_range.start > end || cov_range.end < start {
return None;
}
let new_cov_start = cov_range.start.saturating_sub(start);
let start_glyph_delta = start.saturating_sub(cov_range.start);
let start_glyph = record.start_glyph_id().to_u16() + start_glyph_delta;
let range_len = cov_range.end.min(end) - cov_range.start.max(start);
let end_glyph = start_glyph + range_len;
Some(wlayout::RangeRecord::new(
GlyphId16::new(start_glyph),
GlyphId16::new(end_glyph),
new_cov_start,
))
}
fn make_table_data(table: &dyn FontWrite) -> TableData {
let mut writer = TableWriter::default();
table.write_into(&mut writer);
let mut r = writer.into_data();
r.type_ = table.table_type();
r
}
#[cfg(test)]
mod tests {
use std::ops::Range;
use super::*;
fn make_read_record(start_coverage_index: u16, glyphs: Range<u16>) -> rlayout::RangeRecord {
rlayout::RangeRecord {
start_glyph_id: GlyphId16::new(glyphs.start).into(),
end_glyph_id: GlyphId16::new(glyphs.end).into(),
start_coverage_index: start_coverage_index.into(),
}
}
#[test]
fn splitting_range_records() {
let record = make_read_record(10, 20..30);
assert!(split_range_record(&record, 0, 5).is_none());
let split = split_range_record(&record, 5, 10).unwrap();
assert_eq!(split.start_glyph_id.to_u16(), 20);
assert_eq!(split.end_glyph_id.to_u16(), 20);
assert_eq!(split.start_coverage_index, 5);
let split = split_range_record(&record, 8, 12).unwrap();
assert_eq!(split.start_glyph_id.to_u16(), 20);
assert_eq!(split.end_glyph_id.to_u16(), 22);
assert_eq!(split.start_coverage_index, 2);
let split = split_range_record(&record, 12, 15).unwrap();
assert_eq!(split.start_glyph_id.to_u16(), 22);
assert_eq!(split.end_glyph_id.to_u16(), 25);
assert_eq!(split.start_coverage_index, 0);
let split = split_range_record(&record, 18, 32).unwrap();
assert_eq!(split.start_glyph_id.to_u16(), 28);
assert_eq!(split.end_glyph_id.to_u16(), 30);
assert_eq!(split.start_coverage_index, 0);
let split = split_range_record(&record, 5, 32).unwrap();
assert_eq!(split.start_glyph_id.to_u16(), 20);
assert_eq!(split.end_glyph_id.to_u16(), 30);
assert_eq!(split.start_coverage_index, 5);
let split = split_range_record(&record, 10, 20).unwrap();
assert_eq!(split.start_glyph_id.to_u16(), 20);
assert_eq!(split.end_glyph_id.to_u16(), 30);
assert_eq!(split.start_coverage_index, 0);
assert!(split_range_record(&record, 30, 35).is_none());
}
#[test]
fn simple_split_at_end() {
let record = make_read_record(0, 0..4);
let split = split_range_record(&record, 2, 6).unwrap();
assert_eq!(split.start_glyph_id.to_u16(), 2);
assert_eq!(split.end_glyph_id.to_u16(), 4);
}
#[test]
fn split_inclusive() {
let record = make_read_record(0, 0..100);
let head = split_range_record(&record, 0, 50).unwrap();
assert_eq!(head.start_glyph_id.to_u16(), 0);
assert_eq!(head.end_glyph_id.to_u16(), 50);
let tail = split_range_record(&record, 50, 100).unwrap();
assert_eq!(tail.start_glyph_id.to_u16(), 50);
assert_eq!(tail.end_glyph_id.to_u16(), 100);
}
}