blob: c9e4b0da5ce732169e35caf048482d1a3b715531 [file] [edit]
//! A graph for resolving table offsets
use font_types::Uint24;
use crate::{table_type::TableType, tables::layout::LookupType, write::TableData};
use std::{
collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque},
sync::atomic::AtomicU64,
};
#[cfg(feature = "dot2")]
mod graphviz;
mod splitting;
static OBJECT_COUNTER: AtomicU64 = AtomicU64::new(0);
/// An identifier for an object in the compilation graph.
#[derive(Debug, Clone, Copy, PartialOrd, Ord, Hash, PartialEq, Eq)]
pub struct ObjectId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum OffsetLen {
Offset16 = 2,
Offset24 = 3,
Offset32 = 4,
}
impl OffsetLen {
/// The maximum value for an offset of this length.
pub const fn max_value(self) -> u32 {
match self {
Self::Offset16 => u16::MAX as u32,
Self::Offset24 => (1 << 24) - 1,
Self::Offset32 => u32::MAX,
}
}
}
impl std::fmt::Display for OffsetLen {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Offset16 => write!(f, "Offset16"),
Self::Offset24 => write!(f, "Offset24"),
Self::Offset32 => write!(f, "Offset32"),
}
}
}
/// A ranking used for sorting the graph.
///
/// Nodes are assigned a space, and nodes in lower spaces are always
/// packed before nodes in higher spaces.
#[derive(Debug, Clone, Copy, PartialOrd, Ord, Hash, PartialEq, Eq)]
pub struct Space(u32);
impl Space {
/// A generic space for nodes reachable via 16-bit offsets.
const SHORT_REACHABLE: Space = Space(0);
/// A generic space for nodes that are reachable via any offset.
const REACHABLE: Space = Space(1);
/// The first space used for assignment to specific subgraphs.
const INIT: Space = Space(2);
const fn is_custom(self) -> bool {
self.0 >= Space::INIT.0
}
}
impl ObjectId {
pub fn next() -> Self {
ObjectId(OBJECT_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
}
}
#[derive(Debug, Default)]
pub(crate) struct ObjectStore {
pub(crate) objects: HashMap<TableData, ObjectId>,
}
impl ObjectStore {
pub(crate) fn add(&mut self, data: TableData) -> ObjectId {
*self.objects.entry(data).or_insert_with(ObjectId::next)
}
}
/// A graph of subtables, starting at a single root.
///
/// This type is used during compilation, to determine the final write order
/// for the various subtables.
//NOTE: we don't derive Debug because it's way too verbose to be useful
pub struct Graph {
/// the actual data for each table
objects: BTreeMap<ObjectId, TableData>,
/// graph-specific state used for sorting
nodes: BTreeMap<ObjectId, Node>,
order: Vec<ObjectId>,
root: ObjectId,
parents_invalid: bool,
distance_invalid: bool,
positions_invalid: bool,
next_space: Space,
num_roots_per_space: HashMap<Space, usize>,
}
#[derive(Debug)]
struct Node {
size: u32,
distance: u32,
/// overall position after sorting
position: u32,
space: Space,
parents: Vec<(ObjectId, OffsetLen)>,
priority: Priority,
}
/// Scored used when computing shortest distance
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Distance {
// a space ranking; like rankings are packed together,
// and larger rankings are packed after smaller ones.
space: Space,
distance: u64,
// a tie-breaker, based on order within a parent
order: u32,
}
//TODO: remove me? maybe? not really used right now...
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Priority(u8);
/// A record of an overflowing offset
#[derive(Clone, Debug)]
pub(crate) struct Overflow {
parent: ObjectId,
child: ObjectId,
distance: u32,
offset_type: OffsetLen,
}
impl Priority {
const ZERO: Priority = Priority(0);
const ONE: Priority = Priority(1);
const TWO: Priority = Priority(2);
const THREE: Priority = Priority(3);
#[cfg(test)]
fn increase(&mut self) -> bool {
let result = *self != Priority::THREE;
self.0 = (self.0 + 1).min(3);
result
}
}
impl Distance {
const ROOT: Distance = Distance {
space: Space::SHORT_REACHABLE,
distance: 0,
order: 0,
};
fn rev(self) -> std::cmp::Reverse<Distance> {
std::cmp::Reverse(self)
}
}
impl Node {
pub fn new(size: u32) -> Self {
Node {
//obj,
size,
position: Default::default(),
distance: Default::default(),
space: Space::REACHABLE,
parents: Default::default(),
priority: Default::default(),
}
}
#[cfg(test)]
fn raise_priority(&mut self) -> bool {
self.priority.increase()
}
fn modified_distance(&self, order: u32) -> Distance {
let prev_dist = self.distance as i64;
let distance = match self.priority {
Priority::ZERO => prev_dist,
Priority::ONE => prev_dist - self.size as i64 / 2,
Priority::TWO => prev_dist - self.size as i64,
Priority::THREE => 0,
_ => 0,
}
.max(0) as u64;
Distance {
space: self.space,
distance,
order,
}
}
}
impl Graph {
pub(crate) fn from_obj_store(store: ObjectStore, root: ObjectId) -> Self {
let objects = store.objects.into_iter().map(|(k, v)| (v, k)).collect();
Self::from_objects(objects, root)
}
fn from_objects(objects: BTreeMap<ObjectId, TableData>, root: ObjectId) -> Self {
let nodes = objects
.iter()
//TODO: ensure table sizes elsewhere?
.map(|(key, obj)| (*key, Node::new(obj.bytes.len().try_into().unwrap())))
.collect();
Graph {
objects,
nodes,
order: Default::default(),
root,
parents_invalid: true,
distance_invalid: true,
positions_invalid: true,
next_space: Space::INIT,
num_roots_per_space: Default::default(),
}
}
/// Write out the serialized graph.
///
/// This is not public API, and you are responsible for ensuring that
/// the graph is sorted before calling (by calling `pack_objects`, and
/// checking that it has succeeded).
pub(crate) fn serialize(&self) -> Vec<u8> {
fn write_offset(at: &mut [u8], len: OffsetLen, resolved: u32) {
let at = &mut at[..len as u8 as usize];
match len {
OffsetLen::Offset16 => at.copy_from_slice(
u16::try_from(resolved)
.expect("offset overflow should be checked before now")
.to_be_bytes()
.as_slice(),
),
OffsetLen::Offset24 => at.copy_from_slice(
Uint24::checked_new(resolved)
.expect("offset overflow should be checked before now")
.to_be_bytes()
.as_slice(),
),
OffsetLen::Offset32 => at.copy_from_slice(resolved.to_be_bytes().as_slice()),
}
}
assert!(
!self.order.is_empty(),
"graph must be sorted before serialization"
);
let mut offsets = HashMap::new();
let mut out = Vec::new();
let mut off = 0;
// first pass: write out bytes, record positions of offsets
for id in &self.order {
let node = self.objects.get(id).unwrap();
offsets.insert(*id, off);
off += node.bytes.len() as u32;
out.extend_from_slice(&node.bytes);
}
// second pass: write offsets
let mut table_head = 0;
for id in &self.order {
let node = self.objects.get(id).unwrap();
for offset in &node.offsets {
let abs_off = *offsets
.get(&offset.object)
.expect("all offsets visited in first pass");
let rel_off = abs_off - (table_head + offset.adjustment);
let buffer_pos = table_head + offset.pos;
let write_over = out.get_mut(buffer_pos as usize..).unwrap();
write_offset(write_over, offset.len, rel_off);
}
table_head += node.bytes.len() as u32;
}
out
}
/// Attempt to pack the graph.
///
/// This involves finding an order for objects such that all offsets are
/// resolveable.
///
/// In the simple case, this just means finding a topological ordering.
/// In exceptional cases, however, this may require us to significantly
/// modify the graph.
///
/// Our implementation is closely modeled on the implementation in the
/// HarfBuzz repacker; see the [repacker docs] for further detail.
///
/// returns `true` if a solution is found, `false` otherwise
///
/// [repacker docs]: https://github.com/harfbuzz/harfbuzz/blob/main/docs/repacker.md
pub(crate) fn pack_objects(&mut self) -> bool {
if self.basic_sort() {
return true;
}
self.try_splitting_subtables();
self.try_promoting_subtables();
log::info!("assigning spaces");
self.assign_spaces_hb();
self.sort_shortest_distance();
if !self.has_overflows() {
return true;
}
// now isolate spaces in a loop, until there are no more left:
let overflows = loop {
let overflows = self.find_overflows();
if overflows.is_empty() {
// we're done
return true;
}
log::trace!(
"failed with {} overflows, current size {}",
overflows.len(),
self.debug_len()
);
if !self.try_isolating_subgraphs(&overflows) {
log::debug!("finished isolating all subgraphs without solution");
break overflows;
}
self.sort_shortest_distance();
};
assert!(!overflows.is_empty());
self.debug_overflows(&overflows);
false
}
/// Initial sorting operation. Attempt Kahn, falling back to shortest distance.
///
/// This has to be called first, since it establishes an initial order.
/// subsequent operations on the graph require this order.
///
/// returns `true` if sort succeeds with no overflows
fn basic_sort(&mut self) -> bool {
log::trace!("sorting {} objects", self.objects.len());
self.sort_kahn();
if !self.has_overflows() {
return true;
}
log::trace!("kahn failed, trying shortest distance");
self.sort_shortest_distance();
!self.has_overflows()
}
fn has_overflows(&self) -> bool {
for (parent_id, data) in &self.objects {
let parent = &self.nodes[parent_id];
for link in &data.offsets {
let child = &self.nodes[&link.object];
//TODO: account for 'whence'
let rel_off = child.position - parent.position;
if link.len.max_value() < rel_off {
return true;
}
}
}
false
}
pub(crate) fn find_overflows(&self) -> Vec<Overflow> {
let mut result = Vec::new();
for (parent_id, data) in &self.objects {
let parent = &self.nodes[parent_id];
for link in &data.offsets {
let child = &self.nodes[&link.object];
//TODO: account for 'whence'
let rel_off = child.position - parent.position;
if link.len.max_value() < rel_off {
result.push(Overflow {
parent: *parent_id,
child: link.object,
distance: rel_off,
offset_type: link.len,
});
}
}
}
result
}
fn debug_overflows(&self, overflows: &[Overflow]) {
let (parents, children): (HashSet<_>, HashSet<_>) =
overflows.iter().map(|x| (x.parent, x.child)).unzip();
log::debug!(
"found {} overflows from {} parents to {} children",
overflows.len(),
parents.len(),
children.len()
);
for overflow in overflows {
log::debug!(
"{:?} -> {:?} type {} dist {}",
overflow.parent,
overflow.child,
overflow.offset_type,
overflow.distance
);
}
}
// only valid if order is up to date. Returns total byte len of graph.
fn debug_len(&self) -> usize {
self.order
.iter()
.map(|id| self.objects.get(id).unwrap().bytes.len())
.sum()
}
fn update_parents(&mut self) {
if !self.parents_invalid {
return;
}
for node in self.nodes.values_mut() {
node.parents.clear();
}
for (id, obj) in &self.objects {
for link in &obj.offsets {
self.nodes
.get_mut(&link.object)
.unwrap()
.parents
.push((*id, link.len));
}
}
self.parents_invalid = false;
}
fn remove_orphans(&mut self) {
let mut visited = HashSet::with_capacity(self.nodes.len());
self.find_subgraph_hb(self.root, &mut visited);
if visited.len() != self.nodes.len() {
log::info!("removing {} orphan nodes", self.nodes.len() - visited.len());
for id in self
.nodes
.keys()
.copied()
.collect::<HashSet<_>>()
.difference(&visited)
{
self.nodes.remove(id);
self.objects.remove(id);
}
}
}
fn sort_kahn(&mut self) {
self.positions_invalid = true;
if self.nodes.len() <= 1 {
self.order.extend(self.nodes.keys().copied());
return;
}
let mut queue = BinaryHeap::new();
let mut removed_edges = HashMap::new();
let mut current_pos: u32 = 0;
self.order.clear();
self.update_parents();
queue.push(std::cmp::Reverse(self.root));
while let Some(id) = queue.pop().map(|x| x.0) {
let next = &self.objects[&id];
self.order.push(id);
self.nodes.get_mut(&id).unwrap().position = current_pos;
current_pos += next.bytes.len() as u32;
for link in &next.offsets {
let seen_edges = removed_edges.entry(link.object).or_insert(0usize);
*seen_edges += 1;
// if the target of this link has no other incoming links, add
// to the queue
if *seen_edges == self.nodes[&link.object].parents.len() {
queue.push(std::cmp::Reverse(link.object));
}
}
}
//TODO: check for orphans & cycles?
for (id, seen_len) in &removed_edges {
if *seen_len != self.nodes[id].parents.len() {
panic!("cycle or something?");
}
}
}
pub(crate) fn sort_shortest_distance(&mut self) {
self.positions_invalid = true;
self.update_parents();
self.update_distances();
self.assign_space_0();
let mut queue = BinaryHeap::new();
let mut removed_edges = HashMap::with_capacity(self.nodes.len());
let mut current_pos = 0;
self.order.clear();
queue.push((Distance::ROOT.rev(), self.root));
let mut obj_order = 1u32;
while let Some((_, id)) = queue.pop() {
let next = &self.objects[&id];
self.order.push(id);
self.nodes.get_mut(&id).unwrap().position = current_pos;
current_pos += next.bytes.len() as u32;
for link in &next.offsets {
let seen_edges = removed_edges.entry(link.object).or_insert(0usize);
*seen_edges += 1;
// if the target of this link has no other incoming links, add
// to the queue
if *seen_edges == self.nodes[&link.object].parents.len() {
let distance = self.nodes[&link.object].modified_distance(obj_order);
obj_order += 1;
queue.push((distance.rev(), link.object));
}
}
}
//TODO: check for orphans & cycles?
for (id, seen_len) in &removed_edges {
if *seen_len != self.nodes[id].parents.len() {
panic!("cycle or something?");
}
}
}
fn update_distances(&mut self) {
self.nodes
.values_mut()
.for_each(|node| node.distance = u32::MAX);
self.nodes.get_mut(&self.root).unwrap().distance = u32::MIN;
let mut queue = BinaryHeap::new();
let mut visited = HashSet::new();
queue.push((Default::default(), self.root));
while let Some((_, next_id)) = queue.pop() {
if !visited.insert(next_id) {
continue;
}
let next_distance = self.nodes[&next_id].distance;
let next_obj = &self.objects[&next_id];
for link in &next_obj.offsets {
if visited.contains(&link.object) {
continue;
}
let child = self.nodes.get_mut(&link.object).unwrap();
let child_distance = next_distance + child.size;
if child_distance < child.distance {
child.distance = child_distance;
queue.push((child_distance, link.object));
}
}
}
self.distance_invalid = false;
}
/// isolate and assign spaces to subgraphs reachable via long offsets.
///
/// This finds all subgraphs that are reachable via long offsets, and
/// isolates them (ensuring they are *only* reachable via long offsets),
/// assigning each unique space an identifier.
///
/// Each space may have multiple roots; this works by finding the connected
/// components from each root (counting only nodes reachable via long offsets).
///
/// This is a close port of the [assign_spaces] method used by the HarfBuzz
/// repacker.
///
/// [assign_spaces]: https://github.com/harfbuzz/harfbuzz/blob/main/src/graph/graph.hh#L624
fn assign_spaces_hb(&mut self) -> bool {
self.update_parents();
let (visited, mut roots) = self.find_space_roots_hb();
if roots.is_empty() {
return false;
}
log::trace!("found {} space roots to isolate", roots.len());
// we want to *invert* the visited set, but we don't have a fancy hb_set_t
let mut visited = self
.order
.iter()
.copied()
.collect::<HashSet<_>>()
.difference(&visited)
.copied()
.collect::<HashSet<_>>();
let mut connected_roots = BTreeSet::new(); // we can reuse this
while let Some(next) = roots.iter().copied().next() {
connected_roots.clear();
self.find_connected_nodes_hb(next, &mut roots, &mut visited, &mut connected_roots);
self.isolate_subgraph_hb(&mut connected_roots);
self.distance_invalid = true;
self.positions_invalid = true;
}
true
}
/// Find the root nodes of 32 (and later 24?)-bit space.
///
/// These are the set of nodes that have incoming long offsets, for which
/// no ancestor has an incoming long offset.
///
/// Ported from the [find_space_roots] method in HarfBuzz.
///
/// [find_space_roots]: https://github.com/harfbuzz/harfbuzz/blob/main/src/graph/graph.hh#L508
fn find_space_roots_hb(&self) -> (HashSet<ObjectId>, BTreeSet<ObjectId>) {
let mut visited = HashSet::new();
let mut roots = BTreeSet::new();
let mut queue = VecDeque::from([self.root]);
while let Some(id) = queue.pop_front() {
if visited.contains(&id) {
continue;
}
let obj = self.objects.get(&id).unwrap();
for link in &obj.offsets {
//FIXME: harfbuzz has a bunch of logic here for 24-bit offsets
if link.len == OffsetLen::Offset32 {
roots.insert(link.object);
self.find_subgraph_hb(link.object, &mut visited);
} else {
queue.push_back(link.object);
}
}
}
(visited, roots)
}
fn find_subgraph_hb(&self, idx: ObjectId, nodes: &mut HashSet<ObjectId>) {
if !nodes.insert(idx) {
return;
}
for link in self.objects.get(&idx).unwrap().offsets.iter() {
self.find_subgraph_hb(link.object, nodes);
}
}
fn find_subgraph_map_hb(&self, idx: ObjectId, graph: &mut BTreeMap<ObjectId, usize>) {
use std::collections::btree_map::Entry;
for link in &self.objects[&idx].offsets {
match graph.entry(link.object) {
// To avoid double counting, we only recurse if we are seeing
// this node for the first time.
Entry::Vacant(entry) => {
entry.insert(1);
self.find_subgraph_map_hb(link.object, graph);
}
Entry::Occupied(entry) => {
*entry.into_mut() += 1;
}
}
}
}
/// find all of the members of 'targets' that are reachable, skipping nodes in `visited`.
fn find_connected_nodes_hb(
&self,
id: ObjectId,
targets: &mut BTreeSet<ObjectId>,
visited: &mut HashSet<ObjectId>,
connected: &mut BTreeSet<ObjectId>,
) {
if !visited.insert(id) {
return;
}
if targets.remove(&id) {
connected.insert(id);
}
// recurse to all children and parents
for (obj, _) in &self.nodes.get(&id).unwrap().parents {
self.find_connected_nodes_hb(*obj, targets, visited, connected);
}
for link in &self.objects.get(&id).unwrap().offsets {
self.find_connected_nodes_hb(link.object, targets, visited, connected);
}
}
/// Isolate the subgraph with the provided roots, moving it to a new space.
///
/// This duplicates any nodes in this subgraph that are shared with
/// any other nodes in the graph.
///
/// Based on the [isolate_subgraph] method in HarfBuzz.
///
/// [isolate_subgraph]: https://github.com/harfbuzz/harfbuzz/blob/main/src/graph/graph.hh#L508
fn isolate_subgraph_hb(&mut self, roots: &mut BTreeSet<ObjectId>) -> bool {
self.update_parents();
// map of object id -> number of incoming edges
let mut subgraph = BTreeMap::new();
for root in roots.iter() {
// for the roots, we set the edge count to the number of long
// incoming offsets; if this differs from the total number of
// incoming offsets it means we need to dupe the root as well.
let inbound_wide_offsets = self.nodes[root]
.parents
.iter()
.filter(|(_, len)| !matches!(len, OffsetLen::Offset16))
.count();
subgraph.insert(*root, inbound_wide_offsets);
self.find_subgraph_map_hb(*root, &mut subgraph);
}
let next_space = self.next_space();
log::debug!("moved {} roots to {next_space:?}", roots.len(),);
self.num_roots_per_space.insert(next_space, roots.len());
let mut id_map = HashMap::new();
for (id, incoming_edges_in_subgraph) in &subgraph {
// there are edges to this object from outside the subgraph; dupe it.
if *incoming_edges_in_subgraph < self.nodes[id].parents.len() {
self.duplicate_subgraph(*id, &mut id_map, next_space);
}
}
// now remap any links in the subgraph from nodes that were not
// themselves duplicated (since they were not reachable from outside)
for id in subgraph.keys().filter(|k| !id_map.contains_key(k)) {
self.nodes.get_mut(id).unwrap().space = next_space;
let obj = self.objects.get_mut(id).unwrap();
for link in &mut obj.offsets {
if let Some(new_id) = id_map.get(&link.object) {
link.object = *new_id;
}
}
}
if id_map.is_empty() {
return false;
}
// now everything but the links to the roots roots has been remapped;
// remap those, if needed
for root in roots.iter() {
let Some(new_id) = id_map.get(root) else {
continue;
};
self.parents_invalid = true;
self.positions_invalid = true;
for (parent_id, len) in &self.nodes[new_id].parents {
if !matches!(len, OffsetLen::Offset16) {
for link in &mut self.objects.get_mut(parent_id).unwrap().offsets {
if link.object == *root {
link.object = *new_id;
}
}
}
}
}
// if any roots changed, we also rename them in the input set:
for (old, new) in id_map {
if roots.remove(&old) {
roots.insert(new);
}
}
true
}
/// for each space that has overflows and > 1 roots, select half the roots
/// and move them to a separate subgraph.
//
/// return `true` if any change was made.
///
/// This is a port of the [_try_isolating_subgraphs] method in hb-repacker.
///
/// [_try_isolating_subgraphs]: https://github.com/harfbuzz/harfbuzz/blob/main/src/hb-repacker.hh#L182
fn try_isolating_subgraphs(&mut self, overflows: &[Overflow]) -> bool {
let mut to_isolate = BTreeMap::new();
for overflow in overflows {
let parent_space = self.nodes[&overflow.parent].space;
// we only isolate subgraphs in wide-space
if !parent_space.is_custom() || self.num_roots_per_space[&parent_space] < 2 {
continue;
}
// if parent space is custom it means all children should also be
// in the same custom space.
assert_eq!(parent_space, self.nodes[&overflow.child].space);
let root = self.find_root_of_space(overflow.parent);
assert_eq!(self.nodes[&root].space, parent_space);
to_isolate
.entry(parent_space)
.or_insert_with(BTreeSet::new)
.insert(root);
}
if to_isolate.is_empty() {
return false;
}
for (space, mut roots) in to_isolate {
let n_total_roots = self.num_roots_per_space[&space];
debug_assert!(n_total_roots >= 2, "checked in the loop above");
let max_to_move = n_total_roots / 2;
log::trace!(
"moving {} of {} candidate roots from {space:?} to new space",
max_to_move.min(roots.len()),
roots.len()
);
while roots.len() > max_to_move {
roots.pop_last();
}
self.isolate_subgraph_hb(&mut roots);
*self.num_roots_per_space.get_mut(&space).unwrap() -= roots.len();
}
true
}
// invariant: obj must not be in space 0
fn find_root_of_space(&self, obj: ObjectId) -> ObjectId {
let space = self.nodes[&obj].space;
debug_assert!(space.is_custom());
let parent = self.nodes[&obj].parents[0].0;
if self.nodes[&parent].space != space {
return obj;
}
self.find_root_of_space(parent)
}
fn next_space(&mut self) -> Space {
self.next_space = Space(self.next_space.0 + 1);
self.next_space
}
fn try_promoting_subtables(&mut self) {
let Some((can_promote, parent_id)) = self.get_promotable_subtables() else {
return;
};
let to_promote = self.select_promotions_hb(&can_promote, parent_id);
log::info!(
"promoting {} of {} eligible subtables",
to_promote.len(),
can_promote.len()
);
self.actually_promote_subtables(&to_promote);
}
fn actually_promote_subtables(&mut self, to_promote: &[ObjectId]) {
fn make_extension(type_: LookupType, subtable_id: ObjectId) -> TableData {
const EXT_FORMAT: u16 = 1;
let mut data = TableData::new(TableType::Named("ExtensionPosFormat1"));
data.write(EXT_FORMAT);
data.write(type_.to_raw());
data.add_offset(subtable_id, 4, 0);
data
}
for id in to_promote {
// 'id' is a lookup table.
// we need to:
// - change the subtable type
// - create a new extension table for each subtable
// - update the object ids
let mut lookup = self.objects.remove(id).unwrap();
let lookup_type = lookup.type_.to_lookup_type().expect("validated before now");
for subtable_ref in &mut lookup.offsets {
let ext_table = make_extension(lookup_type, subtable_ref.object);
let ext_id = self.add_object(ext_table);
subtable_ref.object = ext_id;
}
lookup.write_over(lookup_type.promote().to_raw(), 0);
lookup.type_ = lookup_type.promote().into();
self.objects.insert(*id, lookup);
}
self.parents_invalid = true;
self.positions_invalid = true;
}
/// Manually add an object to the graph, after initial compilation.
///
/// This can be used to perform edits to the graph during compilation, such
/// as for table splitting or promotion.
///
/// This has drawbacks; in particular, at this stage we no longer deduplicate
/// objects.
fn add_object(&mut self, data: TableData) -> ObjectId {
self.parents_invalid = true;
self.distance_invalid = true;
let id = ObjectId::next();
self.nodes.insert(id, Node::new(data.bytes.len() as _));
self.objects.insert(id, data);
id
}
// get the list of tables that can be promoted, as well as the id of their parent table
fn get_promotable_subtables(&self) -> Option<(Vec<ObjectId>, ObjectId)> {
let can_promote = self
.objects
.iter()
.filter_map(|(id, obj)| (obj.type_.is_promotable()).then_some(*id))
.collect::<Vec<_>>();
if can_promote.is_empty() {
return None;
}
// sanity check: ensure that all promotable tables have a common root.
let parents = can_promote
.iter()
.flat_map(|id| {
self.nodes
.get(id)
.expect("all nodes exist")
.parents
.iter()
.map(|x| x.0)
})
.collect::<HashSet<_>>();
// the only promotable subtables should be lookups, and there should
// be a single LookupList that is their parent; if there is more than
// one parent then something weird is going on.
if parents.len() > 1 {
if cfg!(debug_assertions) {
panic!("Promotable subtables exist with multiple parents");
} else {
log::warn!("Promotable subtables exist with multiple parents");
return None;
}
}
let parent_id = *parents.iter().next().unwrap();
Some((can_promote, parent_id))
}
/// select the tables to promote to extension, harfbuzz algorithm
///
/// Based on the logic in HarfBuzz's [`_promote_exetnsions_if_needed`][hb-promote][hb-promote] function.
///
/// [hb-promote]: https://github.com/harfbuzz/harfbuzz/blob/5d543d64222c6ce45332d0c188790f90691ef112/src/hb-repacker.hh#L97
fn select_promotions_hb(&self, candidates: &[ObjectId], parent_id: ObjectId) -> Vec<ObjectId> {
struct LookupSize {
id: ObjectId,
subgraph_size: usize,
subtable_count: usize,
}
impl LookupSize {
// I could impl Ord but then I need to impl PartialEq and it ends
// up being way more code
fn sort_key(&self) -> impl Ord {
let bytes_per_subtable = self.subtable_count as f64 / self.subgraph_size as f64;
// f64 isn't ord, so we turn it into an integer,
// then reverse, because we want bigger things first
std::cmp::Reverse((bytes_per_subtable * 1e9) as u64)
}
}
let mut lookup_sizes = Vec::with_capacity(candidates.len());
let mut reusable_buffer = HashSet::new();
let mut queue = VecDeque::new();
for id in candidates {
// get the subgraph size
queue.clear();
queue.push_back(*id);
let subgraph_size = self.find_subgraph_size(&mut queue, &mut reusable_buffer);
let subtable_count = self.objects.get(id).unwrap().offsets.len();
lookup_sizes.push(LookupSize {
id: *id,
subgraph_size,
subtable_count,
});
}
lookup_sizes.sort_by_key(LookupSize::sort_key);
const EXTENSION_SIZE: usize = 8; // number of bytes added by an extension subtable
const MAX_LAYER_SIZE: usize = u16::MAX as usize;
let lookup_list_size = self.objects.get(&parent_id).unwrap().bytes.len();
let mut l2_l3_size = lookup_list_size; // size of LookupList + lookups
let mut l3_l4_size = 0; // Lookups + lookup subtables
let mut l4_plus_size = 0; // subtables and anything below that
// start by assuming all lookups are extensions; we will adjust this later
// if we do not promote.
for lookup in &lookup_sizes {
let subtables_size = lookup.subtable_count * EXTENSION_SIZE;
l3_l4_size += subtables_size;
l4_plus_size += subtables_size;
}
let mut layers_full = false;
let mut to_promote = Vec::new();
for lookup in &lookup_sizes {
if !layers_full {
let lookup_size = self.objects.get(&lookup.id).unwrap().bytes.len();
let subtables_size = self.find_children_size(lookup.id);
let remaining_size = lookup.subgraph_size - lookup_size - subtables_size;
l2_l3_size += lookup_size;
l3_l4_size += lookup_size + subtables_size;
// adjust down, because we are demoting out of extension space
l3_l4_size -= lookup.subtable_count * EXTENSION_SIZE;
l4_plus_size += subtables_size + remaining_size;
if l2_l3_size < MAX_LAYER_SIZE
&& l3_l4_size < MAX_LAYER_SIZE
&& l4_plus_size < MAX_LAYER_SIZE
{
// this lookup fits in the 16-bit space, great
continue;
}
layers_full = true;
}
to_promote.push(lookup.id);
}
to_promote
}
/// See if we have any subtables that support splitting, and split them
/// if needed.
///
/// Based on [`_presplit_subtables_if_needed`][presplit] in hb-repacker
///
/// [presplit]: https://github.com/harfbuzz/harfbuzz/blob/5d543d64222c6ce45332d0c188790f90691ef112/src/hb-repacker.hh#LL72C22-L72C22
fn try_splitting_subtables(&mut self) {
let splittable = self
.objects
.iter()
.filter_map(|(id, obj)| obj.type_.is_splittable().then_some(*id))
.collect::<Vec<_>>();
for lookup in &splittable {
self.split_subtables_if_needed(*lookup);
}
if !splittable.is_empty() {
self.remove_orphans();
}
}
fn split_subtables_if_needed(&mut self, lookup: ObjectId) {
// So You Want to Split Subtables:
// - support PairPos and MarkBase.
let type_ = self.objects[&lookup].type_;
match type_ {
TableType::GposLookup(LookupType::PAIR_POS) => splitting::split_pair_pos(self, lookup),
TableType::GposLookup(LookupType::MARK_TO_BASE) => {
splitting::split_mark_to_base(self, lookup)
}
_ => (),
}
}
/// the size only of children of this object, not the whole subgraph
fn find_children_size(&self, id: ObjectId) -> usize {
self.objects[&id]
.offsets
.iter()
.map(|off| self.objects.get(&off.object).unwrap().bytes.len())
.sum()
}
fn find_subgraph_size(
&self,
queue: &mut VecDeque<ObjectId>,
visited: &mut HashSet<ObjectId>,
) -> usize {
let mut size = 0;
visited.clear();
while !queue.is_empty() {
let next = queue.pop_front().unwrap();
visited.insert(next);
let obj = self.objects.get(&next).unwrap();
size += obj.bytes.len();
queue.extend(
obj.offsets
.iter()
.filter_map(|obj| (!visited.contains(&obj.object)).then_some(obj.object)),
);
}
size
}
fn duplicate_subgraph(
&mut self,
root: ObjectId,
dupes: &mut HashMap<ObjectId, ObjectId>,
space: Space,
) -> ObjectId {
if let Some(existing) = dupes.get(&root) {
return *existing;
}
self.parents_invalid = true;
self.distance_invalid = true;
let new_root = ObjectId::next();
log::trace!("duplicating node {root:?} to {new_root:?}");
let mut obj = self.objects.get(&root).cloned().unwrap();
let mut node = Node::new(obj.bytes.len() as u32);
node.space = space;
for link in &mut obj.offsets {
// recursively duplicate the object
link.object = self.duplicate_subgraph(link.object, dupes, space);
}
dupes.insert(root, new_root);
self.objects.insert(new_root, obj);
self.nodes.insert(new_root, node);
new_root
}
/// Find the set of nodes that are reachable from root only following
/// 16 & 24 bit offsets, and assign them to space 0.
fn assign_space_0(&mut self) {
let mut stack = VecDeque::from([self.root]);
while let Some(next) = stack.pop_front() {
match self.nodes.get_mut(&next) {
Some(node) if node.space != Space::SHORT_REACHABLE => {
node.space = Space::SHORT_REACHABLE
}
_ => continue,
}
for link in self
.objects
.get(&next)
.iter()
.flat_map(|obj| obj.offsets.iter())
{
if link.len != OffsetLen::Offset32 {
stack.push_back(link.object);
}
}
}
}
#[cfg(test)]
fn find_descendents(&self, root: ObjectId) -> HashSet<ObjectId> {
let mut result = HashSet::new();
let mut stack = VecDeque::from([root]);
while let Some(id) = stack.pop_front() {
if result.insert(id) {
for link in self
.objects
.get(&id)
.iter()
.flat_map(|obj| obj.offsets.iter())
{
stack.push_back(link.object);
}
}
}
result
}
#[cfg(feature = "dot2")]
pub(crate) fn write_graph_viz(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
// if this is set then we prune the generated graph
const PRUNE_GRAPH_ENV_VAR: &str = "FONTC_PRUNE_GRAPH";
let try_trim_graph = std::env::var_os(PRUNE_GRAPH_ENV_VAR).is_some();
graphviz::GraphVizGraph::from_graph(self, try_trim_graph).write_to_file(path)
}
}
impl Default for Priority {
fn default() -> Self {
Priority::ZERO
}
}
#[cfg(test)]
mod tests {
use std::ops::Range;
use font_types::GlyphId16;
use crate::TableWriter;
use super::*;
fn make_ids<const N: usize>() -> [ObjectId; N] {
let mut ids = [ObjectId::next(); N];
for id in ids.iter_mut().skip(1) {
*id = ObjectId::next();
}
ids
}
struct Link {
from: ObjectId,
to: ObjectId,
width: OffsetLen,
}
struct TestGraphBuilder {
objects: Vec<(ObjectId, usize)>,
links: Vec<Link>,
}
impl TestGraphBuilder {
fn new<const N: usize>(ids: [ObjectId; N], sizes: [usize; N]) -> Self {
TestGraphBuilder {
objects: ids.into_iter().zip(sizes).collect(),
links: Default::default(),
}
}
fn add_link(&mut self, from: ObjectId, to: ObjectId, width: OffsetLen) -> &mut Self {
self.links.push(Link { from, to, width });
self
}
fn build(&self) -> Graph {
let mut objects = self
.objects
.iter()
.map(|(id, size)| {
let table = TableData::make_mock(*size);
(*id, table)
})
.collect::<BTreeMap<_, _>>();
for link in &self.links {
objects
.get_mut(&link.from)
.unwrap()
.add_mock_offset(link.to, link.width);
}
let root = self.objects.first().unwrap().0;
Graph::from_objects(objects, root)
}
}
//#[test]
//fn difference_smoke_test() {
//assert!(Distance::MIN < Distance::MAX);
//assert!(
//Distance::from_offset_and_size(OffsetLen::Offset16, 10)
//< Distance::from_offset_and_size(OffsetLen::Offset16, 20)
//);
//assert!(
//Distance::from_offset_and_size(OffsetLen::Offset32, 10)
//> Distance::from_offset_and_size(OffsetLen::Offset16, 20)
//);
//assert!(Distance::new(10, 3) > Distance::new(10, 1));
//}
#[test]
fn priority_smoke_test() {
let mut node = Node::new(20);
node.distance = 100;
let mod0 = node.modified_distance(1);
node.raise_priority();
let mod1 = node.modified_distance(1);
assert!(mod0 > mod1);
node.raise_priority();
let mod2 = node.modified_distance(1);
assert!(mod1 > mod2);
node.raise_priority();
let mod3 = node.modified_distance(1);
assert!(mod2 > mod3, "{mod2:?} {mod3:?}");
// max priority is 3
node.raise_priority();
let mod4 = node.modified_distance(1);
assert_eq!(mod3, mod4);
}
#[test]
fn kahn_basic() {
let ids = make_ids::<4>();
let sizes = [10, 10, 20, 10];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[0], ids[2], OffsetLen::Offset16)
.add_link(ids[0], ids[3], OffsetLen::Offset16)
.add_link(ids[3], ids[1], OffsetLen::Offset16)
.build();
graph.sort_kahn();
// 3 links 1, so 1 must be last
assert_eq!(&graph.order, &[ids[0], ids[2], ids[3], ids[1]]);
}
#[test]
fn shortest_basic() {
let ids = make_ids::<4>();
let sizes = [10, 10, 20, 10];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[0], ids[2], OffsetLen::Offset16)
.add_link(ids[0], ids[3], OffsetLen::Offset16)
.build();
graph.sort_shortest_distance();
// but 2 is larger than 3, so should be ordered after
assert_eq!(&graph.order, &[ids[0], ids[1], ids[3], ids[2]]);
}
#[test]
fn overflow_basic() {
let ids = make_ids::<3>();
let sizes = [10, u16::MAX as usize - 5, 100];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[0], ids[2], OffsetLen::Offset16)
.add_link(ids[1], ids[2], OffsetLen::Offset16)
.build();
graph.sort_kahn();
assert_eq!(graph.find_overflows().len(), 1);
assert_eq!(graph.find_overflows()[0].parent, ids[0]);
assert_eq!(graph.find_overflows()[0].child, ids[2]);
}
#[test]
fn duplicate_subgraph() {
let _ = env_logger::builder().is_test(true).try_init();
let ids = make_ids::<10>();
let sizes = [10; 10];
// root has two children, one 16 and one 32-bit offset.
// those subgraphs share three nodes, which must be deduped.
//
// before after
// 0 0
// / ⑊ ┌───┘⑊
// 1 2 ---+ 1 2 ---+
// |\ / \ | / \ / \ |
// | 3 4 5 9 3 3' 4 5
// | \ / \ | \ / \
// | 6 7 6 6' 7
// | | |
// | 8──┘ 8──┘
// | │ /
// 9 ───┘ 9'
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[0], ids[2], OffsetLen::Offset32)
.add_link(ids[1], ids[3], OffsetLen::Offset16)
.add_link(ids[1], ids[9], OffsetLen::Offset16)
.add_link(ids[2], ids[3], OffsetLen::Offset16)
.add_link(ids[2], ids[4], OffsetLen::Offset16)
.add_link(ids[2], ids[5], OffsetLen::Offset16)
.add_link(ids[3], ids[6], OffsetLen::Offset16)
.add_link(ids[4], ids[6], OffsetLen::Offset16)
.add_link(ids[4], ids[7], OffsetLen::Offset16)
.add_link(ids[7], ids[8], OffsetLen::Offset16)
.add_link(ids[8], ids[9], OffsetLen::Offset16)
.build();
assert_eq!(graph.nodes.len(), 10);
let one = graph.find_descendents(ids[1]);
let two = graph.find_descendents(ids[2]);
assert_eq!(one.intersection(&two).count(), 3);
graph.assign_spaces_hb();
// 3, 6, and 9 should be duplicated
assert_eq!(graph.nodes.len(), 13);
let one = graph.find_descendents(ids[1]);
let two = graph.find_descendents(ids[2]);
assert_eq!(one.intersection(&two).count(), 0);
for id in &one {
assert!(!graph.nodes.get(id).unwrap().space.is_custom());
}
for id in &two {
assert!(graph.nodes.get(id).unwrap().space.is_custom());
}
}
#[test]
fn split_overflowing_spaces() {
// this attempts to show a simplified version of a gsub table with extension
// subtables, before any isolation/deduplication has happened.
//
// before after
// 0 (GSUB) 0
// | |
// 1 (lookup List) 1
// | |
// 2 (Lookup) 2
// / \ / \
// ╔═3 4═╗ (ext subtables) ╔═3 4═╗
// ║ ║ ║ ║ (long offsets)
// 5─┐ ┌─6 (subtables) 5 6
// │ └─8─┘ │ / \ / \
// │ │ (cov tables) 7' 8' 7 8
// └───7───┘
//
let _ = env_logger::builder().is_test(true).try_init();
let ids = make_ids::<9>();
// make the coverage tables big enough that overflow is unavoidable
let sizes = [10, 4, 12, 8, 8, 14, 14, 65520, 65520];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[1], ids[2], OffsetLen::Offset16)
.add_link(ids[2], ids[3], OffsetLen::Offset16)
.add_link(ids[2], ids[4], OffsetLen::Offset16)
.add_link(ids[3], ids[5], OffsetLen::Offset32)
.add_link(ids[4], ids[6], OffsetLen::Offset32)
.add_link(ids[5], ids[7], OffsetLen::Offset16)
.add_link(ids[5], ids[8], OffsetLen::Offset16)
.add_link(ids[6], ids[7], OffsetLen::Offset16)
.add_link(ids[6], ids[8], OffsetLen::Offset16)
.build();
graph.sort_shortest_distance();
assert!(graph.has_overflows());
assert_eq!(graph.nodes.len(), 9);
graph.assign_spaces_hb();
graph.sort_shortest_distance();
// now spaces are assigned, but not isolated
assert_eq!(graph.nodes[&ids[5]].space, graph.nodes[&ids[6]].space);
assert_eq!(graph.nodes.len(), 9);
// now isolate space that overflows
let overflows = graph.find_overflows();
graph.try_isolating_subgraphs(&overflows);
graph.sort_shortest_distance();
assert_eq!(graph.nodes.len(), 11);
assert!(graph.find_overflows().is_empty());
// ensure we are correctly update the roots_per_space thing
assert_eq!(graph.num_roots_per_space[&graph.nodes[&ids[6]].space], 1);
assert_eq!(graph.num_roots_per_space[&graph.nodes[&ids[5]].space], 1);
}
#[test]
fn all_roads_lead_to_overflow() {
// this is a regression test for a bug we had where we would fail
// to correctly duplicate shared subgraphs when there were
// multiple links between two objects, which caused us to overcount
// the 'incoming edges in subgraph'.
let _ = env_logger::builder().is_test(true).try_init();
let ids = make_ids::<9>();
let sizes = [10, 10, 10, 10, 10, 65524, 65524, 10, 24];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset32)
.add_link(ids[0], ids[2], OffsetLen::Offset32)
.add_link(ids[0], ids[3], OffsetLen::Offset32)
.add_link(ids[0], ids[4], OffsetLen::Offset32)
.add_link(ids[1], ids[5], OffsetLen::Offset16)
.add_link(ids[1], ids[5], OffsetLen::Offset16)
.add_link(ids[2], ids[6], OffsetLen::Offset16)
.add_link(ids[3], ids[7], OffsetLen::Offset16)
.add_link(ids[5], ids[8], OffsetLen::Offset16)
.add_link(ids[5], ids[8], OffsetLen::Offset16)
.add_link(ids[6], ids[8], OffsetLen::Offset16)
.add_link(ids[7], ids[8], OffsetLen::Offset16)
.build();
graph.assign_spaces_hb();
graph.sort_shortest_distance();
let overflows = graph.find_overflows();
assert!(!overflows.is_empty());
graph.try_isolating_subgraphs(&overflows);
graph.sort_shortest_distance();
let overflows = graph.find_overflows();
assert!(!overflows.is_empty());
assert!(graph.try_isolating_subgraphs(&overflows));
graph.sort_shortest_distance();
assert!(!graph.has_overflows());
}
#[test]
fn two_roots_one_space() {
// If a subgraph is reachable from multiple long offsets, they are all
// initially placed in the same space.
//
// ┌──0═══╗ ┌──0═══╗
// │ ║ ║ │ ║ ║
// │ ║ ║ │ ║ ║
// 1 2 3 1 2 3
// │ \ / │ \ /
// └────4 4 4'
// │ │ │
// 5 5 5'
let ids = make_ids::<6>();
let sizes = [10; 6];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[0], ids[2], OffsetLen::Offset32)
.add_link(ids[0], ids[3], OffsetLen::Offset32)
.add_link(ids[1], ids[4], OffsetLen::Offset16)
.add_link(ids[2], ids[4], OffsetLen::Offset16)
.add_link(ids[3], ids[4], OffsetLen::Offset16)
.add_link(ids[4], ids[5], OffsetLen::Offset16)
.build();
assert_eq!(graph.nodes.len(), 6);
graph.assign_spaces_hb();
assert_eq!(graph.nodes.len(), 8);
let one = graph.find_descendents(ids[1]);
assert!(one.iter().all(|id| !graph.nodes[id].space.is_custom()));
let two = graph.find_descendents(ids[2]);
let three = graph.find_descendents(ids[3]);
assert_eq!(two.intersection(&three).count(), 2);
assert_eq!(two.union(&three).count(), 4);
assert_eq!(
two.union(&three)
.map(|id| graph.nodes[id].space)
.collect::<HashSet<_>>()
.len(),
1
);
}
#[test]
fn duplicate_shared_root_subgraph() {
// if a node is linked from both 16 & 32-bit space, and has no parents
// in 32 bit space, it should always still be deduped.
//
// before after
// 0 0
// / ⑊ / ⑊
// 1 ⑊ 1 2
// └───╴2 │
// 2'
let ids = make_ids::<3>();
let sizes = [10; 3];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[0], ids[2], OffsetLen::Offset32)
.add_link(ids[1], ids[2], OffsetLen::Offset16)
.build();
graph.assign_spaces_hb();
assert_eq!(graph.nodes.len(), 4);
}
#[test]
fn assign_space_even_without_any_duplication() {
// the subgraph of the long offset (0->2) is already isolated, and
// so requires no duplication; but we should still correctly assign a
// space to the children.
//
// 0
// / ⑊
// 1 2
// /
// 3
let ids = make_ids::<4>();
let sizes = [10; 4];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset16)
.add_link(ids[0], ids[2], OffsetLen::Offset32)
.add_link(ids[2], ids[3], OffsetLen::Offset16)
.build();
graph.assign_spaces_hb();
let two = graph.find_descendents(ids[2]);
assert!(two.iter().all(|id| graph.nodes[id].space.is_custom()));
}
#[test]
fn sort_respects_spaces() {
let ids = make_ids::<4>();
let sizes = [10; 4];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset32)
.add_link(ids[0], ids[2], OffsetLen::Offset32)
.add_link(ids[0], ids[3], OffsetLen::Offset16)
.build();
graph.sort_shortest_distance();
assert_eq!(&graph.order, &[ids[0], ids[3], ids[1], ids[2]]);
}
#[test]
fn assign_32bit_spaces_if_needed() {
let ids = make_ids::<3>();
let sizes = [10, u16::MAX as usize, 10];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset32)
.add_link(ids[0], ids[2], OffsetLen::Offset16)
.add_link(ids[1], ids[2], OffsetLen::Offset16)
.build();
graph.basic_sort();
// this will overflow unless the 32-bit offset is put last.
assert!(graph.has_overflows());
graph.pack_objects();
assert!(!graph.has_overflows());
}
/// Construct a real gsub table that cannot be packed unless we use extension
/// subtables
#[test]
fn pack_real_gsub_table_with_extension_promotion() {
use crate::tables::{gsub, layout};
// trial and error: a number that just triggers overflow.
const NUM_SUBTABLES: usize = 3279;
// make an rsub rule for each glyph.
let rsub_rules = (0u16..NUM_SUBTABLES as u16)
.map(|id| {
// Each rule will use unique coverage tables, so nothing is shared.
let coverage = std::iter::once(GlyphId16::new(id)).collect();
let backtrack = [id + 1, id + 3].into_iter().map(GlyphId16::new).collect();
gsub::ReverseChainSingleSubstFormat1::new(
coverage,
vec![backtrack],
vec![],
vec![GlyphId16::new(id + 1)],
)
})
.collect();
let list = layout::LookupList::<gsub::SubstitutionLookup>::new(vec![
gsub::SubstitutionLookup::Reverse(layout::Lookup::new(
layout::LookupFlag::empty(),
rsub_rules,
)),
]);
let table = gsub::Gsub::new(Default::default(), Default::default(), list);
let mut graph = TableWriter::make_graph(&table);
assert!(
!graph.basic_sort(),
"simple sorting should not resolve this graph"
);
const BASE_LEN: usize = 10 // GPOS header len
+ 2 // scriptlist table + featurelist (both empty, get deduped)
+ 4 // lookup list, one offset
+ 6; // lookup table (no offsets)
const RSUB_LEN: usize = 16 // base table len
+ 6 // one-glyph coverage table
+ 8; // two-glyph backtrack coverage table
const EXTENSION_LEN: usize = 8;
assert_eq!(graph.debug_len(), BASE_LEN + NUM_SUBTABLES * RSUB_LEN);
assert!(graph.pack_objects());
assert_eq!(
graph.debug_len(),
BASE_LEN + NUM_SUBTABLES * RSUB_LEN + NUM_SUBTABLES * EXTENSION_LEN
);
const EXPECTED_N_TABLES: usize = 5 // header, script/feature/lookup lists, lookup
- 1 // because script/feature are both empty, thus identical
+ NUM_SUBTABLES * 3 // subtable + coverage + backtrack
+ NUM_SUBTABLES; // extension table for each subtable
assert_eq!(graph.order.len(), EXPECTED_N_TABLES);
}
#[test]
fn pack_real_gpos_table_with_extension_promotion() {
use crate::tables::{gpos, layout};
let _ = env_logger::builder().is_test(true).try_init();
fn make_big_pair_pos(glyph_range: Range<u16>) -> gpos::PositionLookup {
let coverage = glyph_range.clone().map(GlyphId16::new).collect();
let pair_sets = glyph_range
.map(|id| {
let value_rec = gpos::ValueRecord::new().with_x_advance(id as _);
gpos::PairSet::new(
(id..id + 165)
.map(|id2| {
gpos::PairValueRecord::new(
GlyphId16::new(id2),
value_rec.clone(),
gpos::ValueRecord::default(),
)
})
.collect(),
)
})
.collect::<Vec<_>>();
gpos::PositionLookup::Pair(layout::Lookup::new(
layout::LookupFlag::empty(),
vec![gpos::PairPos::format_1(coverage, pair_sets)],
))
}
// this is a shallow graph with large nodes, which makes it easier
// to visualize with graphviz.
let pp1 = make_big_pair_pos(1..20);
let pp2 = make_big_pair_pos(100..120);
let pp3 = make_big_pair_pos(200..221);
let pp4 = make_big_pair_pos(400..422);
let pp5 = make_big_pair_pos(500..523);
let pp6 = make_big_pair_pos(600..624);
let table = gpos::Gpos::new(
Default::default(),
Default::default(),
layout::LookupList::new(vec![pp1, pp2, pp3, pp4, pp5, pp6]),
);
// this constructs a graph where there are overflows in a single pairpos
// subtable.
let mut graph = TableWriter::make_graph(&table);
assert!(
!graph.basic_sort(),
"simple sorting should not resolve this graph",
);
// uncomment these two lines if you want to visualize the graph:
//graph.write_graph_viz("promote_gpos_before.dot");
let n_tables_before = graph.order.len();
assert!(graph.pack_objects());
//graph.write_graph_viz("promote_gpos_after.dot");
// we should have resolved this overflow by promoting a single lookup
// to be an extension, but our logic for determining when to promote
// is not quite perfect, so it promotes an extra.
//
// if our impl changes and this is failing because we're only promoting
// a single extension, then that's great
assert_eq!(n_tables_before + 2, graph.order.len());
}
#[test]
fn unpackable_graph_should_fail() {
let _ = env_logger::builder().is_test(true).try_init();
// specifically, it should not run forever.
let ids = make_ids::<4>();
let sizes = [10, 10, 66000, 66000];
let mut graph = TestGraphBuilder::new(ids, sizes)
.add_link(ids[0], ids[1], OffsetLen::Offset32)
.add_link(ids[1], ids[2], OffsetLen::Offset16)
.add_link(ids[1], ids[3], OffsetLen::Offset16)
.build();
assert!(!graph.pack_objects());
}
}