#[allow(deprecated)]
mod tests {
#[cfg(feature = "layout")]
use crate::opentype::layouts::{
classdef::ClassRangeRecord,
coverage::{Coverage, CoverageFormat1, CoverageFormat2, RangeRecord},
lookup::{
AlternateSet, AlternateSubstitutionFormat1, ChainSubRule, ChainSubRuleSet,
ChainingContextSubstitutionFormat1, ChainingContextSubstitutionFormat2,
ChainingContextSubstitutionFormat3, ContextSubstitutionFormat1, LigatureSet,
LigatureSubstitutionFormat1, LigatureTable, Lookup, LookupList, LookupResult,
LookupSubstitution, LookupType, MultipleSubstitutionFormat1, SequenceLookupRecords,
SequenceRule, SequenceRuleSet, SequenceTable, SingleSubstitutionFormat1,
SingleSubstitutionFormat2,
},
};
use crate::opentype::outline::glyf::ParsedGlyph;
#[cfg(feature = "layout")]
fn coverage_format1(glyph_ids: &[u16]) -> Coverage {
Coverage::Format1(CoverageFormat1 {
coverage_format: 1,
glyph_count: glyph_ids.len() as u16,
glyph_ids: glyph_ids.to_vec(),
})
}
#[cfg(feature = "layout")]
fn coverage_format2(ranges: &[(u16, u16, u16)]) -> Coverage {
Coverage::Format2(CoverageFormat2 {
coverage_format: 2,
range_count: ranges.len() as u16,
range_records: ranges
.iter()
.map(
|(start_glyph_id, end_glyph_id, start_coverage_index)| RangeRecord {
start_glyph_id: *start_glyph_id,
end_glyph_id: *end_glyph_id,
start_coverage_index: *start_coverage_index,
},
)
.collect(),
})
}
#[cfg(any())]
fn convert() {
use crate::fontheader::f2dot14_to_f32;
use crate::fontheader::fixed_to_f32;
let value = f2dot14_to_f32(0x7fff);
assert_eq!(value, 1.999939);
let value = f2dot14_to_f32(0x7000);
assert_eq!(value, 1.75);
let value = f2dot14_to_f32(0x0001);
assert_eq!(value, 0.000061);
let value = f2dot14_to_f32(0x0000);
assert_eq!(value, 0.0);
let value = f2dot14_to_f32(0xffff);
assert_eq!(value, -0.000061);
let value = f2dot14_to_f32(0x8000);
assert_eq!(value, -2.0);
let value = fixed_to_f32(0x7fff_ffff);
assert_eq!(value, 1.9999998807907104);
let value = fixed_to_f32(0x7000_0000);
assert_eq!(value, 1.75);
let value = fixed_to_f32(0x0000_0001);
assert_eq!(value, 0.00000005960464477539);
let value = fixed_to_f32(0x0000_0000);
assert_eq!(value, 0.0);
let value = fixed_to_f32(0xffff_ffff);
assert_eq!(value, -0.00000005960464477539);
let value = fixed_to_f32(0x8000_0000);
assert_eq!(value, -2.0);
}
#[test]
#[cfg(feature = "cff")]
fn operand_encoding_test() -> Result<(), Box<dyn std::error::Error>> {
use crate::opentype::outline::cff::{operand_encoding, Operand};
let b = [0x8b];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, 0);
assert_eq!(len, 1);
} else {
panic!("not integer");
}
for b in 32..246 {
let buf = [b];
let (value, len) = operand_encoding(&buf)?;
if let Operand::Integer(value) = value {
assert_eq!(value, b as i32 - 139);
assert_eq!(len, 1);
} else {
panic!("not integer");
}
}
let b = [0xef];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, 100);
assert_eq!(len, 1);
} else {
panic!("not integer");
}
let b = [0x27];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, -100);
assert_eq!(len, 1);
} else {
panic!("not integer");
}
let b = [0xfa, 0x7c];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, 1000);
assert_eq!(len, 2);
} else {
panic!("not real");
}
let b = [0xfe, 0x7c];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, -1000);
assert_eq!(len, 2);
} else {
panic!("not integer");
}
let b = [0x1c, 0x27, 0x10];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, 10000);
assert_eq!(len, 3);
} else {
panic!("not integer");
}
let b = [0x1c, 0xd8, 0xf0];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, -10000);
assert_eq!(len, 3);
} else {
panic!("not integer");
}
let b = [0x1d, 0x00, 0x01, 0x86, 0xa0];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, 100000);
assert_eq!(len, 5);
} else {
panic!("not integer");
}
let b = [0x1d, 0xff, 0xfe, 0x79, 0x60];
let (value, len) = operand_encoding(&b)?;
if let Operand::Integer(value) = value {
assert_eq!(value, -100000);
assert_eq!(len, 5);
} else {
panic!("not integer");
}
let b = [31];
let value = operand_encoding(&b);
assert!(value.is_ok());
let b = [0x1e, 0x2e, 0xa2, 0x5f];
let value = operand_encoding(&b);
assert!(value.is_ok());
let b = [0x1e, 0xe2, 0xa2, 0x5f];
let (value, len) = operand_encoding(&b)?;
if let Operand::Real(value) = value {
assert_eq!(value, -2.25);
assert_eq!(len, 4);
} else {
panic!("not real");
}
let b = [0x1e, 0x0a, 0x14, 0x05, 0x41, 0xc3, 0xff];
let (value, len) = operand_encoding(&b)?;
if let Operand::Real(value) = value {
assert_eq!(value, 0.140541e-3);
assert_eq!(len, 7);
} else {
panic!("not real");
}
Ok(())
}
#[test]
#[cfg(feature = "layout")]
fn lookup_single_substitution_expands_glyphs() {
let single = LookupSubstitution::Single(SingleSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[10, 12]),
delta_glyph_id: 3,
});
assert_eq!(single.get_single_glyph_id(10), Some(13));
assert_eq!(single.get_single_glyph_id(11), None);
let single2 = LookupSubstitution::Single2(SingleSubstitutionFormat2 {
subst_format: 2,
coverage: coverage_format2(&[(20, 21, 0), (30, 30, 2)]),
glyph_count: 3,
substitute_glyph_ids: vec![120, 121, 130],
});
assert_eq!(single2.get_single_glyph_id(20), Some(120));
assert_eq!(single2.get_single_glyph_id(21), Some(121));
assert_eq!(single2.get_single_glyph_id(30), Some(130));
assert_eq!(single2.get_single_glyph_id(31), None);
}
#[test]
#[cfg(feature = "layout")]
fn gpos_skips_truncated_feature_variations() {
let gpos = parse_gpos(build_gpos_v11_with_truncated_feature_variations(
*b"kern",
2,
build_gpos_pair_format1_subtable(10, 20, -50),
));
assert_eq!(gpos.major_version, 1);
assert_eq!(gpos.minor_version, 1);
assert!(gpos.feature_variations.is_none());
}
#[test]
#[cfg(feature = "layout")]
fn lookup_multiple_and_alternate_expand_sequences() {
let multiple = LookupSubstitution::Multiple(MultipleSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[40, 41]),
sequence_count: 2,
sequence_tables: vec![
SequenceTable {
glyph_count: 2,
substitute_glyph_ids: vec![400, 401],
},
SequenceTable {
glyph_count: 3,
substitute_glyph_ids: vec![410, 411, 412],
},
],
});
match multiple.get_lookup(41) {
LookupResult::Multiple(ids) => assert_eq!(ids, vec![410, 411, 412]),
_ => panic!("unexpected lookup result"),
}
let alternate = LookupSubstitution::Alternate(AlternateSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[50]),
alternate_set_count: 1,
alternate_set: vec![AlternateSet {
glyph_count: 2,
alternate_glyph_ids: vec![500, 501],
}],
});
match alternate.get_lookup(50) {
LookupResult::Multiple(ids) => assert_eq!(ids, vec![500, 501]),
_ => panic!("unexpected lookup result"),
}
}
#[test]
#[cfg(feature = "layout")]
fn lookup_ligature_and_context_expand_records() {
let ligature = LookupSubstitution::Ligature(LigatureSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[60]),
ligature_set_count: 1,
ligature_set: vec![LigatureSet {
ligature_count: 1,
ligature_table: vec![LigatureTable {
ligature_glyph: 600,
component_count: 3,
component_glyph_ids: vec![61, 62],
}],
}],
});
match ligature.get_lookup(60) {
LookupResult::Ligature(records) => {
assert_eq!(records.len(), 1);
assert_eq!(records[0].ligature_glyph, 600);
assert_eq!(records[0].component_glyph_ids, vec![61, 62]);
}
_ => panic!("unexpected lookup result"),
}
let context = LookupSubstitution::ContextSubstitution(ContextSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[70]),
rule_set_count: 1,
rule_sets: vec![SequenceRuleSet {
rule_count: 1,
rules: vec![SequenceRule {
glyph_count: 2,
input_sequence: vec![71],
lookup_count: 1,
lookup_indexes: vec![9],
}],
}],
});
match context.get_lookup(70) {
LookupResult::Context(rules) => {
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].input_sequence, vec![71]);
assert_eq!(rules[0].lookup_indexes, vec![9]);
}
_ => panic!("unexpected lookup result"),
}
}
#[test]
#[cfg(feature = "layout")]
fn lookup_chaining_variants_expand_expected_payloads() {
let chaining =
LookupSubstitution::ChainingContextSubstitution(ChainingContextSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[80]),
chain_sub_rule_set_count: 1,
chain_sub_rule_set: vec![ChainSubRuleSet {
chain_sub_rule_count: 1,
chain_sub_rule: vec![ChainSubRule {
backtrack_glyph_count: 1,
backtrack_glyph_ids: vec![79],
input_glyph_count: 2,
input_glyph_ids: vec![81],
lookahead_glyph_count: 1,
lookahead_glyph_ids: vec![82],
lookup_count: 1,
lookup_indexes: vec![7],
}],
}],
});
match chaining.get_lookup(80) {
LookupResult::Chaining(rules) => {
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].backtrack_glyph_ids, vec![79]);
assert_eq!(rules[0].lookup_indexes, vec![7]);
}
_ => panic!("unexpected lookup result"),
}
let chaining2 =
LookupSubstitution::ChainingContextSubstitution2(ChainingContextSubstitutionFormat2 {
subst_format: 2,
class_range_count: 1,
class_range_records: vec![ClassRangeRecord {
start_glyph_id: 90,
end_glyph_id: 90,
class: 4,
}],
coverage: coverage_format1(&[90]),
backtrack_class_def: None,
input_class_def: None,
lookahead_class_def: None,
chain_sub_class_set_count: 0,
chain_sub_class_sets: vec![],
});
match chaining2.get_lookup(90) {
LookupResult::Multiple(classes) => assert_eq!(classes, vec![4]),
_ => panic!("unexpected lookup result"),
}
let chaining3 =
LookupSubstitution::ChainingContextSubstitution3(ChainingContextSubstitutionFormat3 {
format: 3,
backtrack_glyph_count: 1,
backtrack_coverages: vec![coverage_format1(&[100])],
input_glyph_count: 1,
input_coverages: vec![coverage_format1(&[101, 102])],
lookahead_glyph_count: 1,
lookahead_coverages: vec![coverage_format1(&[103])],
seq_lookup_count: 1,
seq_lookup_records: SequenceLookupRecords {
lookup_records: vec![],
},
});
let (first, coverages) = chaining3.get_coverage();
assert_eq!(first.contains(100), Some(0));
let (backtrack, input, lookahead) = coverages.expect("coverage tuple");
assert_eq!(backtrack[0].contains(100), Some(0));
assert_eq!(input[0].contains(102), Some(1));
assert_eq!(lookahead[0].contains(103), Some(0));
}
#[test]
#[cfg(feature = "layout")]
fn lookup_reports_none_for_non_covered_glyph() {
let single = LookupSubstitution::Single(SingleSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[200]),
delta_glyph_id: 1,
});
match single.get_lookup(201) {
LookupResult::None => {}
_ => panic!("expected none"),
}
}
#[cfg(feature = "layout")]
fn build_lookup_list(tables: Vec<Vec<u8>>) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, tables.len() as u16);
let offsets_pos = buffer.len();
buffer.resize(buffer.len() + tables.len() * 2, 0);
let mut offsets = Vec::new();
for table in tables {
offsets.push(buffer.len() as u16);
buffer.extend_from_slice(&table);
}
for (index, offset) in offsets.iter().enumerate() {
let start = offsets_pos + index * 2;
buffer[start..start + 2].copy_from_slice(&offset.to_be_bytes());
}
buffer
}
#[cfg(feature = "layout")]
fn lookup_single_subtable(glyph_id: u16, delta_glyph_id: i16) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 6);
push_u16(&mut buffer, delta_glyph_id as u16);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, glyph_id);
buffer
}
#[cfg(feature = "layout")]
fn build_lookup_record(lookup_type: u16, subtable: Vec<u8>) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, lookup_type);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 8);
buffer.extend_from_slice(&subtable);
buffer
}
#[cfg(feature = "layout")]
fn lookup_extension_subtable(glyph_id: u16, delta_glyph_id: i16) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, LookupType::SingleSubstitution as u16);
push_u32(&mut buffer, 8);
buffer.extend_from_slice(&lookup_single_subtable(glyph_id, delta_glyph_id));
build_lookup_record(LookupType::ExtensionSubstitution as u16, buffer)
}
#[cfg(feature = "layout")]
fn lookup_reverse_chain_subtable(
coverage_glyph_id: u16,
substitute_glyph_id: u16,
backtrack_glyph_id: u16,
input_glyph_id: u16,
lookahead_glyph_id: u16,
) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, backtrack_glyph_id);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, input_glyph_id);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, lookahead_glyph_id);
push_u16(&mut buffer, substitute_glyph_id);
let coverage_offset = buffer.len() as u16;
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, coverage_glyph_id);
buffer[2..4].copy_from_slice(&coverage_offset.to_be_bytes());
build_lookup_record(
LookupType::ReverseChainingContextualSingleSubstitution as u16,
buffer,
)
}
#[cfg(feature = "layout")]
fn parse_lookup_list(tables: Vec<Vec<u8>>) -> LookupList {
let buffer = build_lookup_list(tables);
let mut reader = BytesReader::new(&buffer);
LookupList::new(&mut reader, 0, buffer.len() as u32).unwrap()
}
#[cfg(feature = "layout")]
fn coverage_table(glyph_ids: &[u16]) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, glyph_ids.len() as u16);
for glyph_id in glyph_ids {
push_u16(&mut buffer, *glyph_id);
}
buffer
}
#[cfg(feature = "layout")]
fn build_gpos_pair_format1_subtable(left: u16, right: u16, x_advance: i16) -> Vec<u8> {
let coverage = coverage_table(&[left]);
let mut pair_set = Vec::new();
push_u16(&mut pair_set, 1);
push_u16(&mut pair_set, right);
push_u16(&mut pair_set, x_advance as u16);
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 12);
push_u16(&mut buffer, 0x0004);
push_u16(&mut buffer, 0x0000);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, (12 + coverage.len()) as u16);
buffer.extend_from_slice(&coverage);
buffer.extend_from_slice(&pair_set);
buffer
}
#[cfg(feature = "layout")]
fn class_def_format1(start_glyph_id: u16, class_values: &[u16]) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, start_glyph_id);
push_u16(&mut buffer, class_values.len() as u16);
for class_value in class_values {
push_u16(&mut buffer, *class_value);
}
buffer
}
#[cfg(feature = "layout")]
fn build_gpos_pair_format2_subtable(left: u16, right: u16, x_advance: i16) -> Vec<u8> {
let coverage = coverage_table(&[left]);
let class_def1 = class_def_format1(left, &[1]);
let class_def2 = class_def_format1(right, &[1]);
let class1_count = 2u16;
let class2_count = 2u16;
let mut class_records = Vec::new();
for class1 in 0..class1_count {
for class2 in 0..class2_count {
let value = if class1 == 1 && class2 == 1 {
x_advance as u16
} else {
0
};
push_u16(&mut class_records, value);
}
}
let coverage_offset = 16u16 + class_records.len() as u16;
let class_def1_offset = coverage_offset + coverage.len() as u16;
let class_def2_offset = class_def1_offset + class_def1.len() as u16;
let mut buffer = Vec::new();
push_u16(&mut buffer, 2);
push_u16(&mut buffer, coverage_offset);
push_u16(&mut buffer, 0x0004);
push_u16(&mut buffer, 0x0000);
push_u16(&mut buffer, class_def1_offset);
push_u16(&mut buffer, class_def2_offset);
push_u16(&mut buffer, class1_count);
push_u16(&mut buffer, class2_count);
buffer.extend_from_slice(&class_records);
buffer.extend_from_slice(&coverage);
buffer.extend_from_slice(&class_def1);
buffer.extend_from_slice(&class_def2);
buffer
}
#[cfg(feature = "layout")]
fn build_script_list_with_language_systems(
scripts: &[([u8; 4], &[(u32, u16, &[u16])])],
) -> Vec<u8> {
let mut script_list = Vec::new();
push_u16(&mut script_list, scripts.len() as u16);
let script_records_pos = script_list.len();
script_list.resize(script_list.len() + scripts.len() * 6, 0);
let mut script_offsets = Vec::new();
for (_, language_systems) in scripts {
let script_start = script_list.len();
script_offsets.push(script_start as u16);
let default_index = language_systems.iter().position(|(tag, _, _)| *tag == 0);
let non_default_count = language_systems.len() - usize::from(default_index.is_some());
push_u16(&mut script_list, 0);
push_u16(&mut script_list, non_default_count as u16);
let language_records_pos = script_list.len();
script_list.resize(script_list.len() + non_default_count * 6, 0);
let mut default_offset = 0u16;
let mut non_default_offsets = Vec::new();
for (tag, required_feature_index, feature_indices) in *language_systems {
let offset = (script_list.len() - script_start) as u16;
push_u16(&mut script_list, 0);
push_u16(&mut script_list, *required_feature_index);
push_u16(&mut script_list, feature_indices.len() as u16);
for feature_index in *feature_indices {
push_u16(&mut script_list, *feature_index);
}
if *tag == 0 {
default_offset = offset;
} else {
non_default_offsets.push((*tag, offset));
}
}
script_list[script_start..script_start + 2]
.copy_from_slice(&default_offset.to_be_bytes());
for (index, (tag, offset)) in non_default_offsets.iter().enumerate() {
let start = language_records_pos + index * 6;
script_list[start..start + 4].copy_from_slice(&tag.to_be_bytes());
script_list[start + 4..start + 6].copy_from_slice(&offset.to_be_bytes());
}
}
for (index, (script_tag, _)) in scripts.iter().enumerate() {
let start = script_records_pos + index * 6;
script_list[start..start + 4].copy_from_slice(script_tag);
script_list[start + 4..start + 6].copy_from_slice(&script_offsets[index].to_be_bytes());
}
script_list
}
#[cfg(feature = "layout")]
fn build_script_list_with_default_lang_systems(scripts: &[([u8; 4], u16, &[u16])]) -> Vec<u8> {
let mut script_list = Vec::new();
push_u16(&mut script_list, scripts.len() as u16);
let script_records_pos = script_list.len();
script_list.resize(script_list.len() + scripts.len() * 6, 0);
let mut script_offsets = Vec::new();
for (_, required_feature_index, feature_indices) in scripts {
script_offsets.push(script_list.len() as u16);
push_u16(&mut script_list, 4);
push_u16(&mut script_list, 0);
push_u16(&mut script_list, 0);
push_u16(&mut script_list, *required_feature_index);
push_u16(&mut script_list, feature_indices.len() as u16);
for feature_index in *feature_indices {
push_u16(&mut script_list, *feature_index);
}
}
for (index, (script_tag, _, _)) in scripts.iter().enumerate() {
let start = script_records_pos + index * 6;
script_list[start..start + 4].copy_from_slice(script_tag);
script_list[start + 4..start + 6].copy_from_slice(&script_offsets[index].to_be_bytes());
}
script_list
}
#[cfg(feature = "layout")]
fn build_feature_list_with_entries(features: &[([u8; 4], &[u16])]) -> Vec<u8> {
let mut feature_list = Vec::new();
push_u16(&mut feature_list, features.len() as u16);
let feature_records_pos = feature_list.len();
feature_list.resize(feature_list.len() + features.len() * 6, 0);
let mut feature_offsets = Vec::new();
for (_, lookup_indices) in features {
feature_offsets.push(feature_list.len() as u16);
push_u16(&mut feature_list, 0);
push_u16(&mut feature_list, lookup_indices.len() as u16);
for lookup_index in *lookup_indices {
push_u16(&mut feature_list, *lookup_index);
}
}
for (index, (feature_tag, _)) in features.iter().enumerate() {
let start = feature_records_pos + index * 6;
feature_list[start..start + 4].copy_from_slice(feature_tag);
feature_list[start + 4..start + 6]
.copy_from_slice(&feature_offsets[index].to_be_bytes());
}
feature_list
}
#[cfg(feature = "layout")]
fn build_gpos_table(feature_tag: [u8; 4], lookup_type: u16, subtable: Vec<u8>) -> Vec<u8> {
build_gpos_table_with_scripted_features(
&[(*b"DFLT", 0xFFFF, &[0])],
&[(feature_tag, &[0])],
lookup_type,
vec![subtable],
)
}
#[cfg(feature = "layout")]
fn build_gpos_table_with_scripted_features(
scripts: &[([u8; 4], u16, &[u16])],
features: &[([u8; 4], &[u16])],
lookup_type: u16,
subtables: Vec<Vec<u8>>,
) -> Vec<u8> {
let script_list = build_script_list_with_default_lang_systems(scripts);
let feature_list = build_feature_list_with_entries(features);
let mut lookup_list = Vec::new();
push_u16(&mut lookup_list, subtables.len() as u16);
let lookup_offsets_pos = lookup_list.len();
lookup_list.resize(lookup_list.len() + subtables.len() * 2, 0);
let mut lookup_offsets = Vec::new();
for subtable in subtables {
lookup_offsets.push(lookup_list.len() as u16);
push_u16(&mut lookup_list, lookup_type);
push_u16(&mut lookup_list, 0);
push_u16(&mut lookup_list, 1);
push_u16(&mut lookup_list, 8);
lookup_list.extend_from_slice(&subtable);
}
for (index, offset) in lookup_offsets.iter().enumerate() {
let start = lookup_offsets_pos + index * 2;
lookup_list[start..start + 2].copy_from_slice(&offset.to_be_bytes());
}
let script_list_offset = 10u16;
let feature_list_offset = script_list_offset + script_list.len() as u16;
let lookup_list_offset = feature_list_offset + feature_list.len() as u16;
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, script_list_offset);
push_u16(&mut buffer, feature_list_offset);
push_u16(&mut buffer, lookup_list_offset);
buffer.extend_from_slice(&script_list);
buffer.extend_from_slice(&feature_list);
buffer.extend_from_slice(&lookup_list);
buffer
}
#[cfg(feature = "layout")]
fn parse_gpos(buffer: Vec<u8>) -> crate::opentype::extentions::gpos::GPOS {
let mut reader = BytesReader::new(&buffer);
crate::opentype::extentions::gpos::GPOS::new(&mut reader, 0, buffer.len() as u32).unwrap()
}
#[cfg(feature = "layout")]
fn build_gpos_v11_with_truncated_feature_variations(
feature_tag: [u8; 4],
lookup_type: u16,
subtable: Vec<u8>,
) -> Vec<u8> {
let script_list = build_script_list_with_default_lang_systems(&[(*b"DFLT", 0xFFFF, &[0])]);
let feature_list = build_feature_list_with_entries(&[(feature_tag, &[0])]);
let mut lookup_list = Vec::new();
push_u16(&mut lookup_list, 1);
push_u16(&mut lookup_list, 4);
push_u16(&mut lookup_list, lookup_type);
push_u16(&mut lookup_list, 0);
push_u16(&mut lookup_list, 1);
push_u16(&mut lookup_list, 8);
lookup_list.extend_from_slice(&subtable);
let script_list_offset = 12u16;
let feature_list_offset = script_list_offset + script_list.len() as u16;
let lookup_list_offset = feature_list_offset + feature_list.len() as u16;
let feature_variations_offset = lookup_list_offset + lookup_list.len() as u16;
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, script_list_offset);
push_u16(&mut buffer, feature_list_offset);
push_u16(&mut buffer, lookup_list_offset);
push_u16(&mut buffer, feature_variations_offset);
buffer.extend_from_slice(&script_list);
buffer.extend_from_slice(&feature_list);
buffer.extend_from_slice(&lookup_list);
buffer.push(0x00);
buffer
}
#[cfg(feature = "layout")]
fn build_gsub_table(feature_tag: [u8; 4], lookups: Vec<Vec<u8>>) -> Vec<u8> {
let feature_lookup_indices: Vec<u16> = (0..lookups.len() as u16).collect();
build_gsub_table_with_feature_lookups(feature_tag, &feature_lookup_indices, lookups)
}
#[cfg(feature = "layout")]
fn build_gsub_table_with_feature_lookups(
feature_tag: [u8; 4],
feature_lookup_indices: &[u16],
lookups: Vec<Vec<u8>>,
) -> Vec<u8> {
let mut script_list = Vec::new();
push_u16(&mut script_list, 1);
script_list.extend_from_slice(b"DFLT");
push_u16(&mut script_list, 8);
push_u16(&mut script_list, 4);
push_u16(&mut script_list, 0);
push_u16(&mut script_list, 0);
push_u16(&mut script_list, 0xFFFF);
push_u16(&mut script_list, 1);
push_u16(&mut script_list, 0);
let mut feature_list = Vec::new();
push_u16(&mut feature_list, 1);
feature_list.extend_from_slice(&feature_tag);
push_u16(&mut feature_list, 8);
push_u16(&mut feature_list, 0);
push_u16(&mut feature_list, feature_lookup_indices.len() as u16);
for lookup_index in feature_lookup_indices {
push_u16(&mut feature_list, *lookup_index);
}
let lookup_list = build_lookup_list(lookups);
let script_list_offset = 10u16;
let feature_list_offset = script_list_offset + script_list.len() as u16;
let lookup_list_offset = feature_list_offset + feature_list.len() as u16;
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, script_list_offset);
push_u16(&mut buffer, feature_list_offset);
push_u16(&mut buffer, lookup_list_offset);
buffer.extend_from_slice(&script_list);
buffer.extend_from_slice(&feature_list);
buffer.extend_from_slice(&lookup_list);
buffer
}
#[cfg(feature = "layout")]
fn build_gsub_table_with_scripted_feature_lookups(
scripts: &[([u8; 4], &[u16])],
feature_tag: [u8; 4],
feature_lookup_indices: &[Vec<u16>],
lookups: Vec<Vec<u8>>,
) -> Vec<u8> {
let scripts_with_required = scripts
.iter()
.map(|(script_tag, feature_indices)| (*script_tag, 0xFFFF, *feature_indices))
.collect::<Vec<_>>();
let features = feature_lookup_indices
.iter()
.map(|lookup_indices| (feature_tag, lookup_indices.as_slice()))
.collect::<Vec<_>>();
let script_list = build_script_list_with_default_lang_systems(&scripts_with_required);
let feature_list = build_feature_list_with_entries(&features);
let lookup_list = build_lookup_list(lookups);
let script_list_offset = 10u16;
let feature_list_offset = script_list_offset + script_list.len() as u16;
let lookup_list_offset = feature_list_offset + feature_list.len() as u16;
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, script_list_offset);
push_u16(&mut buffer, feature_list_offset);
push_u16(&mut buffer, lookup_list_offset);
buffer.extend_from_slice(&script_list);
buffer.extend_from_slice(&feature_list);
buffer.extend_from_slice(&lookup_list);
buffer
}
#[cfg(feature = "layout")]
fn build_gsub_table_with_scripted_features(
scripts: &[([u8; 4], u16, &[u16])],
features: &[([u8; 4], &[u16])],
lookups: Vec<Vec<u8>>,
) -> Vec<u8> {
let script_list = build_script_list_with_default_lang_systems(scripts);
let feature_list = build_feature_list_with_entries(features);
let lookup_list = build_lookup_list(lookups);
let script_list_offset = 10u16;
let feature_list_offset = script_list_offset + script_list.len() as u16;
let lookup_list_offset = feature_list_offset + feature_list.len() as u16;
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, script_list_offset);
push_u16(&mut buffer, feature_list_offset);
push_u16(&mut buffer, lookup_list_offset);
buffer.extend_from_slice(&script_list);
buffer.extend_from_slice(&feature_list);
buffer.extend_from_slice(&lookup_list);
buffer
}
#[cfg(feature = "layout")]
fn lookup_context_format3_record(
coverages: &[Vec<u8>],
sequence_index: u16,
lookup_list_index: u16,
) -> Vec<u8> {
let mut subtable = Vec::new();
push_u16(&mut subtable, 3);
push_u16(&mut subtable, coverages.len() as u16);
push_u16(&mut subtable, 1);
let offsets_pos = subtable.len();
subtable.resize(subtable.len() + coverages.len() * 2, 0);
push_u16(&mut subtable, sequence_index);
push_u16(&mut subtable, lookup_list_index);
let mut offsets = Vec::new();
for coverage in coverages {
offsets.push(subtable.len() as u16);
subtable.extend_from_slice(coverage);
}
for (index, offset) in offsets.iter().enumerate() {
let start = offsets_pos + index * 2;
subtable[start..start + 2].copy_from_slice(&offset.to_be_bytes());
}
build_lookup_record(LookupType::ContextSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn class_def_format1_table(start_glyph_id: u16, class_values: &[u16]) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, start_glyph_id);
push_u16(&mut buffer, class_values.len() as u16);
for class_value in class_values {
push_u16(&mut buffer, *class_value);
}
buffer
}
#[cfg(feature = "layout")]
fn lookup_context_format1_record(
coverage_glyph_id: u16,
input_sequence: &[u16],
lookup_indexes: &[u16],
) -> Vec<u8> {
let coverage = coverage_table(&[coverage_glyph_id]);
let mut rule = Vec::new();
push_u16(&mut rule, (input_sequence.len() + 1) as u16);
push_u16(&mut rule, lookup_indexes.len() as u16);
for glyph_id in input_sequence {
push_u16(&mut rule, *glyph_id);
}
for lookup_index in lookup_indexes {
push_u16(&mut rule, *lookup_index);
}
let mut rule_set = Vec::new();
push_u16(&mut rule_set, 1);
push_u16(&mut rule_set, 4);
rule_set.extend_from_slice(&rule);
let rule_set_offset = 8u16;
let coverage_offset = rule_set_offset + rule_set.len() as u16;
let mut subtable = Vec::new();
push_u16(&mut subtable, 1);
push_u16(&mut subtable, coverage_offset);
push_u16(&mut subtable, 1);
push_u16(&mut subtable, rule_set_offset);
subtable.extend_from_slice(&rule_set);
subtable.extend_from_slice(&coverage);
build_lookup_record(LookupType::ContextSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn lookup_context_format2_record(
coverage_glyph_id: u16,
class_values: &[u16],
input_classes: &[u16],
sequence_index: u16,
lookup_list_index: u16,
) -> Vec<u8> {
let coverage = coverage_table(&[coverage_glyph_id]);
let class_def = class_def_format1_table(coverage_glyph_id, class_values);
let mut rule = Vec::new();
push_u16(&mut rule, (input_classes.len() + 1) as u16);
push_u16(&mut rule, 1);
for class_id in input_classes {
push_u16(&mut rule, *class_id);
}
push_u16(&mut rule, sequence_index);
push_u16(&mut rule, lookup_list_index);
let mut empty_rule_set = Vec::new();
push_u16(&mut empty_rule_set, 0);
let mut active_rule_set = Vec::new();
push_u16(&mut active_rule_set, 1);
push_u16(&mut active_rule_set, 4);
active_rule_set.extend_from_slice(&rule);
let mut subtable = Vec::new();
push_u16(&mut subtable, 2);
let coverage_offset_pos = subtable.len();
push_u16(&mut subtable, 0);
let class_def_offset_pos = subtable.len();
push_u16(&mut subtable, 0);
push_u16(&mut subtable, 3);
let class_set_offsets_pos = subtable.len();
subtable.resize(subtable.len() + 3 * 2, 0);
let class0_offset = subtable.len() as u16;
subtable.extend_from_slice(&empty_rule_set);
let class1_offset = subtable.len() as u16;
subtable.extend_from_slice(&active_rule_set);
let class2_offset = subtable.len() as u16;
subtable.extend_from_slice(&empty_rule_set);
let class_def_offset = subtable.len() as u16;
subtable.extend_from_slice(&class_def);
let coverage_offset = subtable.len() as u16;
subtable.extend_from_slice(&coverage);
subtable[coverage_offset_pos..coverage_offset_pos + 2]
.copy_from_slice(&coverage_offset.to_be_bytes());
subtable[class_def_offset_pos..class_def_offset_pos + 2]
.copy_from_slice(&class_def_offset.to_be_bytes());
subtable[class_set_offsets_pos..class_set_offsets_pos + 2]
.copy_from_slice(&class0_offset.to_be_bytes());
subtable[class_set_offsets_pos + 2..class_set_offsets_pos + 4]
.copy_from_slice(&class1_offset.to_be_bytes());
subtable[class_set_offsets_pos + 4..class_set_offsets_pos + 6]
.copy_from_slice(&class2_offset.to_be_bytes());
build_lookup_record(LookupType::ContextSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn lookup_chaining_context_format3_record(
backtrack_coverages: &[Vec<u8>],
input_coverages: &[Vec<u8>],
lookahead_coverages: &[Vec<u8>],
sequence_index: u16,
lookup_list_index: u16,
) -> Vec<u8> {
let mut subtable = Vec::new();
push_u16(&mut subtable, 3);
push_u16(&mut subtable, backtrack_coverages.len() as u16);
let backtrack_offsets_pos = subtable.len();
subtable.resize(subtable.len() + backtrack_coverages.len() * 2, 0);
push_u16(&mut subtable, input_coverages.len() as u16);
let input_offsets_pos = subtable.len();
subtable.resize(subtable.len() + input_coverages.len() * 2, 0);
push_u16(&mut subtable, lookahead_coverages.len() as u16);
let lookahead_offsets_pos = subtable.len();
subtable.resize(subtable.len() + lookahead_coverages.len() * 2, 0);
push_u16(&mut subtable, 1);
push_u16(&mut subtable, sequence_index);
push_u16(&mut subtable, lookup_list_index);
let mut backtrack_offsets = Vec::new();
for coverage in backtrack_coverages {
backtrack_offsets.push(subtable.len() as u16);
subtable.extend_from_slice(coverage);
}
let mut input_offsets = Vec::new();
for coverage in input_coverages {
input_offsets.push(subtable.len() as u16);
subtable.extend_from_slice(coverage);
}
let mut lookahead_offsets = Vec::new();
for coverage in lookahead_coverages {
lookahead_offsets.push(subtable.len() as u16);
subtable.extend_from_slice(coverage);
}
for (index, offset) in backtrack_offsets.iter().enumerate() {
let start = backtrack_offsets_pos + index * 2;
subtable[start..start + 2].copy_from_slice(&offset.to_be_bytes());
}
for (index, offset) in input_offsets.iter().enumerate() {
let start = input_offsets_pos + index * 2;
subtable[start..start + 2].copy_from_slice(&offset.to_be_bytes());
}
for (index, offset) in lookahead_offsets.iter().enumerate() {
let start = lookahead_offsets_pos + index * 2;
subtable[start..start + 2].copy_from_slice(&offset.to_be_bytes());
}
build_lookup_record(LookupType::ChainingContextSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn lookup_chaining_context_format1_record(
coverage_glyph_id: u16,
backtrack_glyph_ids: &[u16],
input_glyph_ids: &[u16],
lookahead_glyph_ids: &[u16],
lookup_indexes: &[u16],
) -> Vec<u8> {
let coverage = coverage_table(&[coverage_glyph_id]);
let mut rule = Vec::new();
push_u16(&mut rule, backtrack_glyph_ids.len() as u16);
for glyph_id in backtrack_glyph_ids {
push_u16(&mut rule, *glyph_id);
}
push_u16(&mut rule, input_glyph_ids.len() as u16);
for glyph_id in input_glyph_ids {
push_u16(&mut rule, *glyph_id);
}
push_u16(&mut rule, lookahead_glyph_ids.len() as u16);
for glyph_id in lookahead_glyph_ids {
push_u16(&mut rule, *glyph_id);
}
push_u16(&mut rule, lookup_indexes.len() as u16);
for lookup_index in lookup_indexes {
push_u16(&mut rule, *lookup_index);
}
let mut rule_set = Vec::new();
push_u16(&mut rule_set, 1);
push_u16(&mut rule_set, 4);
rule_set.extend_from_slice(&rule);
let rule_set_offset = 8u16;
let coverage_offset = rule_set_offset + rule_set.len() as u16;
let mut subtable = Vec::new();
push_u16(&mut subtable, 1);
push_u16(&mut subtable, coverage_offset);
push_u16(&mut subtable, 1);
push_u16(&mut subtable, rule_set_offset);
subtable.extend_from_slice(&rule_set);
subtable.extend_from_slice(&coverage);
build_lookup_record(LookupType::ChainingContextSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn lookup_chaining_context_format2_record(
coverage_glyph_id: u16,
backtrack_classes: &[u16],
input_classes: &[u16],
lookahead_classes: &[u16],
sequence_index: u16,
lookup_list_index: u16,
) -> Vec<u8> {
let coverage = coverage_table(&[coverage_glyph_id]);
let backtrack_class_def =
class_def_format1_table(coverage_glyph_id - 1, &[backtrack_classes[0]]);
let input_class_def = class_def_format1_table(coverage_glyph_id, &[1, input_classes[0]]);
let lookahead_class_def =
class_def_format1_table(coverage_glyph_id + 2, &[lookahead_classes[0]]);
let mut rule = Vec::new();
push_u16(&mut rule, backtrack_classes.len() as u16);
for class_id in backtrack_classes {
push_u16(&mut rule, *class_id);
}
push_u16(&mut rule, (input_classes.len() + 1) as u16);
for class_id in input_classes {
push_u16(&mut rule, *class_id);
}
push_u16(&mut rule, lookahead_classes.len() as u16);
for class_id in lookahead_classes {
push_u16(&mut rule, *class_id);
}
push_u16(&mut rule, 1);
push_u16(&mut rule, sequence_index);
push_u16(&mut rule, lookup_list_index);
let mut empty_rule_set = Vec::new();
push_u16(&mut empty_rule_set, 0);
let mut active_rule_set = Vec::new();
push_u16(&mut active_rule_set, 1);
push_u16(&mut active_rule_set, 4);
active_rule_set.extend_from_slice(&rule);
let mut subtable = Vec::new();
push_u16(&mut subtable, 2);
let coverage_offset_pos = subtable.len();
push_u16(&mut subtable, 0);
let backtrack_class_def_offset_pos = subtable.len();
push_u16(&mut subtable, 0);
let input_class_def_offset_pos = subtable.len();
push_u16(&mut subtable, 0);
let lookahead_class_def_offset_pos = subtable.len();
push_u16(&mut subtable, 0);
push_u16(&mut subtable, 3);
let class_set_offsets_pos = subtable.len();
subtable.resize(subtable.len() + 3 * 2, 0);
let class0_offset = subtable.len() as u16;
subtable.extend_from_slice(&empty_rule_set);
let class1_offset = subtable.len() as u16;
subtable.extend_from_slice(&active_rule_set);
let class2_offset = subtable.len() as u16;
subtable.extend_from_slice(&empty_rule_set);
let backtrack_class_def_offset = subtable.len() as u16;
subtable.extend_from_slice(&backtrack_class_def);
let input_class_def_offset = subtable.len() as u16;
subtable.extend_from_slice(&input_class_def);
let lookahead_class_def_offset = subtable.len() as u16;
subtable.extend_from_slice(&lookahead_class_def);
let coverage_offset = subtable.len() as u16;
subtable.extend_from_slice(&coverage);
subtable[coverage_offset_pos..coverage_offset_pos + 2]
.copy_from_slice(&coverage_offset.to_be_bytes());
subtable[backtrack_class_def_offset_pos..backtrack_class_def_offset_pos + 2]
.copy_from_slice(&backtrack_class_def_offset.to_be_bytes());
subtable[input_class_def_offset_pos..input_class_def_offset_pos + 2]
.copy_from_slice(&input_class_def_offset.to_be_bytes());
subtable[lookahead_class_def_offset_pos..lookahead_class_def_offset_pos + 2]
.copy_from_slice(&lookahead_class_def_offset.to_be_bytes());
subtable[class_set_offsets_pos..class_set_offsets_pos + 2]
.copy_from_slice(&class0_offset.to_be_bytes());
subtable[class_set_offsets_pos + 2..class_set_offsets_pos + 4]
.copy_from_slice(&class1_offset.to_be_bytes());
subtable[class_set_offsets_pos + 4..class_set_offsets_pos + 6]
.copy_from_slice(&class2_offset.to_be_bytes());
build_lookup_record(LookupType::ChainingContextSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn lookup_multiple_record(glyph_id: u16, substitute_glyph_ids: &[u16]) -> Vec<u8> {
let coverage = coverage_table(&[glyph_id]);
let mut sequence = Vec::new();
push_u16(&mut sequence, substitute_glyph_ids.len() as u16);
for substitute_glyph_id in substitute_glyph_ids {
push_u16(&mut sequence, *substitute_glyph_id);
}
let sequence_offset = 8u16;
let coverage_offset = sequence_offset + sequence.len() as u16;
let mut subtable = Vec::new();
push_u16(&mut subtable, 1);
push_u16(&mut subtable, coverage_offset);
push_u16(&mut subtable, 1);
push_u16(&mut subtable, sequence_offset);
subtable.extend_from_slice(&sequence);
subtable.extend_from_slice(&coverage);
build_lookup_record(LookupType::MultipleSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn lookup_single_record(glyph_id: u16, substitute_glyph_id: u16) -> Vec<u8> {
let coverage = coverage_table(&[glyph_id]);
let mut subtable = Vec::new();
push_u16(&mut subtable, 2);
push_u16(&mut subtable, 8);
push_u16(&mut subtable, 1);
push_u16(&mut subtable, substitute_glyph_id);
subtable.extend_from_slice(&coverage);
build_lookup_record(LookupType::SingleSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn lookup_ligature_record(
glyph_id: u16,
component_glyph_ids: &[u16],
ligature_glyph: u16,
) -> Vec<u8> {
let coverage = coverage_table(&[glyph_id]);
let mut ligature_table = Vec::new();
push_u16(&mut ligature_table, ligature_glyph);
push_u16(&mut ligature_table, (component_glyph_ids.len() + 1) as u16);
for component_glyph_id in component_glyph_ids {
push_u16(&mut ligature_table, *component_glyph_id);
}
let mut ligature_set = Vec::new();
push_u16(&mut ligature_set, 1);
push_u16(&mut ligature_set, 4);
ligature_set.extend_from_slice(&ligature_table);
let ligature_set_offset = 8u16;
let coverage_offset = ligature_set_offset + ligature_set.len() as u16;
let mut subtable = Vec::new();
push_u16(&mut subtable, 1);
push_u16(&mut subtable, coverage_offset);
push_u16(&mut subtable, 1);
push_u16(&mut subtable, ligature_set_offset);
subtable.extend_from_slice(&ligature_set);
subtable.extend_from_slice(&coverage);
build_lookup_record(LookupType::LigatureSubstitution as u16, subtable)
}
#[cfg(feature = "layout")]
fn parse_gsub(buffer: Vec<u8>) -> crate::opentype::extentions::gsub::GSUB {
let mut reader = BytesReader::new(&buffer);
crate::opentype::extentions::gsub::GSUB::new(&mut reader, 0, buffer.len() as u32).unwrap()
}
#[test]
#[cfg(feature = "layout")]
fn lookup_extension_and_reverse_chain_parse_and_resolve() {
let lookup_list = parse_lookup_list(vec![
lookup_extension_subtable(0x0041, 4),
lookup_reverse_chain_subtable(0x0042, 0x0201, 0x0030, 0x0043, 0x0044),
]);
match &lookup_list.lookups[0].subtables[0] {
LookupSubstitution::ExtensionSubstitution(extension) => {
assert_eq!(extension.subst_format, 1);
assert_eq!(
extension.extension_lookup_type,
LookupType::SingleSubstitution as u16
);
assert_eq!(extension.extension_offset, 8);
match extension.subtable.as_ref() {
LookupSubstitution::Single(single) => {
assert_eq!(single.delta_glyph_id, 4);
assert_eq!(single.coverage.contains(0x0041), Some(0));
}
_ => panic!("expected nested single substitution"),
}
match extension.subtable.get_lookup(0x0041) {
LookupResult::Single(glyph_id) => assert_eq!(glyph_id, 4),
_ => panic!("expected single result"),
}
}
_ => panic!("expected extension substitution"),
}
match &lookup_list.lookups[1].subtables[0] {
LookupSubstitution::ReverseChainSingle(reverse) => {
assert_eq!(reverse.subst_format, 1);
assert_eq!(reverse.coverage.contains(0x0042), Some(0));
assert_eq!(reverse.backtrack_glyph_ids, vec![0x0030]);
assert_eq!(reverse.input_glyph_ids, vec![0x0043]);
assert_eq!(reverse.lookahead_glyph_ids, vec![0x0044]);
match lookup_list.lookups[1].subtables[0].get_lookup(0x0042) {
LookupResult::Single(glyph_id) => assert_eq!(glyph_id, 0x0201),
_ => panic!("expected single result"),
}
match lookup_list.lookups[1].subtables[0].get_lookup(0x0041) {
LookupResult::None => {}
_ => panic!("expected no result"),
}
}
_ => panic!("expected reverse chain substitution"),
}
}
#[test]
#[cfg(feature = "layout")]
fn gpos_pair_adjustment_format1_parses_and_resolves() {
let gpos = parse_gpos(build_gpos_table(
*b"kern",
2,
build_gpos_pair_format1_subtable(10, 20, -50),
));
let adjustment = gpos
.lookup_pair_adjustment(10, 20, false, None)
.expect("pair adjustment");
assert_eq!(adjustment.first.x_advance, -50);
assert_eq!(adjustment.second.x_advance, 0);
assert!(gpos.lookup_pair_adjustment(10, 21, false, None).is_none());
}
#[test]
#[cfg(feature = "layout")]
fn gpos_pair_adjustment_format2_parses_and_resolves() {
let gpos = parse_gpos(build_gpos_table(
*b"kern",
2,
build_gpos_pair_format2_subtable(30, 40, -80),
));
let adjustment = gpos
.lookup_pair_adjustment(30, 40, false, None)
.expect("class pair adjustment");
assert_eq!(adjustment.first.x_advance, -80);
assert!(gpos.lookup_pair_adjustment(31, 40, false, None).is_none());
}
#[test]
#[cfg(feature = "layout")]
fn gpos_locale_specific_script_and_required_feature_take_priority_over_dflt() {
let gpos = parse_gpos(build_gpos_table_with_scripted_features(
&[(*b"DFLT", 0xFFFF, &[0]), (*b"arab", 1, &[])],
&[(*b"kern", &[0]), (*b"kern", &[1])],
2,
vec![
build_gpos_pair_format1_subtable(10, 20, -10),
build_gpos_pair_format1_subtable(10, 20, -30),
],
));
let default_adjustment = gpos
.lookup_pair_adjustment(10, 20, false, Some("default"))
.expect("default pair adjustment");
assert_eq!(default_adjustment.first.x_advance, -10);
let arabic_adjustment = gpos
.lookup_pair_adjustment(10, 20, false, Some("ar"))
.expect("arabic pair adjustment");
assert_eq!(arabic_adjustment.first.x_advance, -30);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_lookup_once_supports_multiple_and_ligature_sequences() {
let multiple_lookup = Lookup {
lookup_type: LookupType::MultipleSubstitution as u16,
lookup_flag: 0,
subtables: vec![LookupSubstitution::Multiple(MultipleSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[10]),
sequence_count: 1,
sequence_tables: vec![SequenceTable {
glyph_count: 2,
substitute_glyph_ids: vec![20, 21],
}],
})],
};
let ligature_lookup = Lookup {
lookup_type: LookupType::LigatureSubstitution as u16,
lookup_flag: 0,
subtables: vec![LookupSubstitution::Ligature(LigatureSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[20]),
ligature_set_count: 1,
ligature_set: vec![LigatureSet {
ligature_count: 1,
ligature_table: vec![LigatureTable {
ligature_glyph: 99,
component_count: 2,
component_glyph_ids: vec![21],
}],
}],
})],
};
let mut glyphs = vec![(10usize, 0usize)];
assert!(crate::opentype::extentions::gsub::GSUB::apply_lookup_once(
&multiple_lookup,
&mut glyphs,
));
assert_eq!(glyphs, vec![(20, 0), (21, 0)]);
assert!(crate::opentype::extentions::gsub::GSUB::apply_lookup_once(
&ligature_lookup,
&mut glyphs,
));
assert_eq!(glyphs, vec![(99, 0)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_ccmp_sequence_supports_multiple_then_ligature() {
let gsub = parse_gsub(build_gsub_table(
*b"ccmp",
vec![
lookup_multiple_record(10, &[20, 21]),
lookup_ligature_record(20, &[21], 99),
],
));
let mut glyphs = vec![(10usize, 0usize)];
gsub.apply_ccmp_sequence(&mut glyphs);
assert_eq!(glyphs, vec![(99, 0)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_feature_sequence_supports_context_format3() {
let gsub = parse_gsub(build_gsub_table_with_feature_lookups(
*b"calt",
&[0],
vec![
lookup_context_format3_record(
&[coverage_table(&[10]), coverage_table(&[11])],
1,
1,
),
lookup_single_record(11, 77),
],
));
let mut glyphs = vec![(10usize, 0usize), (11usize, 1usize)];
gsub.apply_feature_sequence(&mut glyphs, None, &[*b"calt"]);
assert_eq!(glyphs, vec![(10, 0), (77, 1)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_feature_sequence_supports_context_format1_and_format2() {
let gsub = parse_gsub(build_gsub_table_with_feature_lookups(
*b"calt",
&[0, 1],
vec![
lookup_context_format1_record(10, &[11], &[2]),
lookup_context_format2_record(20, &[1, 2], &[2], 1, 3),
lookup_single_record(10, 70),
lookup_single_record(21, 99),
],
));
let mut format1_glyphs = vec![(10usize, 0usize), (11usize, 1usize)];
gsub.apply_feature_sequence(&mut format1_glyphs, None, &[*b"calt"]);
assert_eq!(format1_glyphs, vec![(70, 0), (11, 1)]);
let mut format2_glyphs = vec![(20usize, 0usize), (21usize, 1usize)];
gsub.apply_feature_sequence(&mut format2_glyphs, None, &[*b"calt"]);
assert_eq!(format2_glyphs, vec![(20, 0), (99, 1)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_feature_sequence_supports_chaining_context_format3() {
let gsub = parse_gsub(build_gsub_table_with_feature_lookups(
*b"calt",
&[0],
vec![
lookup_chaining_context_format3_record(
&[coverage_table(&[10])],
&[coverage_table(&[11])],
&[coverage_table(&[12])],
0,
1,
),
lookup_single_record(11, 88),
],
));
let mut glyphs = vec![(10usize, 0usize), (11usize, 1usize), (12usize, 2usize)];
gsub.apply_feature_sequence(&mut glyphs, None, &[*b"calt"]);
assert_eq!(glyphs, vec![(10, 0), (88, 1), (12, 2)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_feature_sequence_supports_chaining_context_format1() {
let gsub = parse_gsub(build_gsub_table_with_feature_lookups(
*b"calt",
&[0],
vec![
lookup_chaining_context_format1_record(11, &[10], &[], &[12], &[1]),
lookup_single_record(11, 66),
],
));
let mut glyphs = vec![(10usize, 0usize), (11usize, 1usize), (12usize, 2usize)];
gsub.apply_feature_sequence(&mut glyphs, None, &[*b"calt"]);
assert_eq!(glyphs, vec![(10, 0), (66, 1), (12, 2)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_feature_sequence_supports_chaining_context_format2() {
let gsub = parse_gsub(build_gsub_table_with_feature_lookups(
*b"calt",
&[0],
vec![
lookup_chaining_context_format2_record(20, &[1], &[2], &[1], 1, 1),
lookup_single_record(21, 123),
],
));
let mut glyphs = vec![
(19usize, 0usize),
(20usize, 1usize),
(21usize, 2usize),
(22usize, 3usize),
];
gsub.apply_feature_sequence(&mut glyphs, None, &[*b"calt"]);
assert_eq!(glyphs, vec![(19, 0), (20, 1), (123, 2), (22, 3)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_rtl_contextual_sequence_supports_rclt() {
let gsub = parse_gsub(build_gsub_table_with_feature_lookups(
*b"rclt",
&[0],
vec![
lookup_context_format3_record(
&[coverage_table(&[10]), coverage_table(&[11])],
1,
1,
),
lookup_single_record(11, 144),
],
));
let mut glyphs = vec![(10usize, 0usize), (11usize, 1usize)];
gsub.apply_rtl_contextual_sequence(&mut glyphs, None);
assert_eq!(glyphs, vec![(10, 0), (144, 1)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_apply_rtl_contextual_sequence_supports_clig() {
let gsub = parse_gsub(build_gsub_table_with_feature_lookups(
*b"clig",
&[0],
vec![lookup_ligature_record(20, &[21], 220)],
));
let mut glyphs = vec![(20usize, 0usize), (21usize, 1usize)];
gsub.apply_rtl_contextual_sequence(&mut glyphs, None);
assert_eq!(glyphs, vec![(220, 0)]);
}
#[test]
#[cfg(feature = "layout")]
fn gsub_locale_specific_script_lookups_take_priority_over_dflt() {
let gsub = parse_gsub(build_gsub_table_with_scripted_feature_lookups(
&[(*b"DFLT", &[0]), (*b"arab", &[1])],
*b"isol",
&[vec![0], vec![1]],
vec![lookup_single_record(10, 100), lookup_single_record(10, 200)],
));
let default_forms = gsub.lookup_joining_forms(10, None);
assert_eq!(default_forms.isolated, Some(100));
let arabic_forms = gsub.lookup_joining_forms(10, Some("ar"));
assert_eq!(arabic_forms.isolated, Some(200));
}
#[test]
#[cfg(feature = "layout")]
fn gsub_required_feature_is_applied_for_locale_specific_script() {
let gsub = parse_gsub(build_gsub_table_with_scripted_features(
&[(*b"DFLT", 0xFFFF, &[0]), (*b"arab", 1, &[])],
&[(*b"isol", &[0]), (*b"isol", &[1])],
vec![lookup_single_record(10, 100), lookup_single_record(10, 200)],
));
let default_forms = gsub.lookup_joining_forms(10, None);
assert_eq!(default_forms.isolated, Some(100));
let arabic_forms = gsub.lookup_joining_forms(10, Some("ar"));
assert_eq!(arabic_forms.isolated, Some(200));
}
#[test]
#[cfg(feature = "layout")]
fn gsub_language_specific_lookup_uses_full_locale_subtags() {
let script_list = build_script_list_with_language_systems(&[(
*b"arab",
&[
(0u32, 0xFFFF, &[0][..]),
(u32::from_be_bytes(*b"URD "), 0xFFFF, &[1][..]),
],
)]);
let feature_list = build_feature_list_with_entries(&[(*b"isol", &[0]), (*b"isol", &[1])]);
let lookup_list = build_lookup_list(vec![
lookup_single_record(10, 100),
lookup_single_record(10, 300),
]);
let script_list_offset = 10u16;
let feature_list_offset = script_list_offset + script_list.len() as u16;
let lookup_list_offset = feature_list_offset + feature_list.len() as u16;
let mut buffer = Vec::new();
push_u16(&mut buffer, 1);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, script_list_offset);
push_u16(&mut buffer, feature_list_offset);
push_u16(&mut buffer, lookup_list_offset);
buffer.extend_from_slice(&script_list);
buffer.extend_from_slice(&feature_list);
buffer.extend_from_slice(&lookup_list);
let gsub = parse_gsub(buffer);
let default_forms = gsub.lookup_joining_forms(10, Some("ar"));
assert_eq!(default_forms.isolated, Some(100));
let urdu_forms = gsub.lookup_joining_forms(10, Some("ur-Arab-PK"));
assert_eq!(urdu_forms.isolated, Some(300));
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn fontload_from_net_works() {
let path = sample_font_path();
let bytes = std::fs::read(&path).expect("read font bytes");
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind tcp listener");
let addr = listener.local_addr().expect("local addr");
let server = std::thread::spawn(move || {
let (mut socket, _) = listener.accept().expect("accept");
let mut request = Vec::new();
let mut buf = [0u8; 1024];
loop {
let read = std::io::Read::read(&mut socket, &mut buf).expect("read request");
if read == 0 {
break;
}
request.extend_from_slice(&buf[..read]);
if request.windows(4).any(|window| window == b"\r\n\r\n") {
break;
}
}
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
bytes.len()
);
std::io::Write::write_all(&mut socket, response.as_bytes()).expect("write header");
std::io::Write::write_all(&mut socket, &bytes).expect("write body");
});
let url = format!("http://127.0.0.1:{}/font.ttf", addr.port());
let font = crate::load_font_from_net(&url).expect("load from net");
assert!(font.font().get_font_count() >= 1);
server.join().expect("server thread");
}
#[test]
fn emoji_font_renders_svg() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("NotoColorEmoji-Regular.ttf");
let font = crate::fontload_file(&path).expect("load emoji font");
let svg = font.font().get_svg('😀', 32.0, "px").expect("emoji svg");
assert!(svg.contains("<svg"));
}
#[test]
#[cfg(feature = "layout")]
fn ligature_lookup_returns_multiple_alternatives() {
let ligature = LookupSubstitution::Ligature(LigatureSubstitutionFormat1 {
subst_format: 1,
coverage: coverage_format1(&[0x0066]),
ligature_set_count: 1,
ligature_set: vec![LigatureSet {
ligature_count: 2,
ligature_table: vec![
LigatureTable {
ligature_glyph: 0xfb01,
component_count: 2,
component_glyph_ids: vec![0x0069],
},
LigatureTable {
ligature_glyph: 0xfb02,
component_count: 2,
component_glyph_ids: vec![0x006c],
},
],
}],
});
match ligature.get_lookup(0x0066) {
LookupResult::Ligature(records) => {
assert_eq!(records.len(), 2);
assert_eq!(records[0].ligature_glyph, 0xfb01);
assert_eq!(records[1].ligature_glyph, 0xfb02);
}
_ => panic!("unexpected lookup result"),
}
}
use crate::opentype::requires::cmap::{self, CmapEncodings, CmapSubtable, EncodingRecord};
use bin_rs::reader::BytesReader;
fn push_u16(buffer: &mut Vec<u8>, value: u16) {
buffer.extend_from_slice(&value.to_be_bytes());
}
fn push_u24(buffer: &mut Vec<u8>, value: u32) {
buffer.push(((value >> 16) & 0xFF) as u8);
buffer.push(((value >> 8) & 0xFF) as u8);
buffer.push((value & 0xFF) as u8);
}
fn push_u32(buffer: &mut Vec<u8>, value: u32) {
buffer.extend_from_slice(&value.to_be_bytes());
}
fn build_cmap(records: Vec<(u16, u16, Vec<u8>)>) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 0);
push_u16(&mut buffer, records.len() as u16);
let header_len = 4 + records.len() * 8;
let mut offset = header_len as u32;
let mut tables = Vec::new();
for (platform_id, encoding_id, table) in records {
push_u16(&mut buffer, platform_id);
push_u16(&mut buffer, encoding_id);
push_u32(&mut buffer, offset);
offset += table.len() as u32;
tables.push(table);
}
for table in tables {
buffer.extend_from_slice(&table);
}
buffer
}
fn cmap_encodings(records: Vec<(u16, u16, Vec<u8>)>) -> CmapEncodings {
let buffer = build_cmap(records);
let mut reader = BytesReader::new(&buffer);
CmapEncodings::new(&mut reader, 0, buffer.len() as u32).unwrap()
}
fn cmap_subtable(table: Vec<u8>) -> CmapSubtable {
let record = EncodingRecord {
platform_id: 3,
encoding_id: 1,
subtable_offset: 0,
};
let buffer = table;
cmap::get_subtable(&record, &buffer)
}
fn format0_table() -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 0);
push_u16(&mut buffer, 262);
push_u16(&mut buffer, 0);
for gid in 0u8..=255 {
buffer.push(gid);
}
buffer
}
fn format2_table() -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 2);
push_u16(&mut buffer, 2566);
push_u16(&mut buffer, 0);
for _ in 0..256 {
push_u16(&mut buffer, 0);
}
for _ in 0..256 {
push_u16(&mut buffer, 0);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, 0);
}
buffer
}
fn format4_table(
start_code: u16,
end_code: u16,
delta: i16,
range_offset: u16,
glyphs: &[u16],
) -> Vec<u8> {
let seg_count = 2u16;
let seg_count_x2 = seg_count * 2;
let search_range = 4u16;
let entry_selector = 1u16;
let range_shift = 0u16;
let length = 16 + 8 * seg_count as usize + glyphs.len() * 2;
let mut buffer = Vec::new();
push_u16(&mut buffer, 4);
push_u16(&mut buffer, length as u16);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, seg_count_x2);
push_u16(&mut buffer, search_range);
push_u16(&mut buffer, entry_selector);
push_u16(&mut buffer, range_shift);
push_u16(&mut buffer, end_code);
push_u16(&mut buffer, 0xFFFF);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, start_code);
push_u16(&mut buffer, 0xFFFF);
push_u16(&mut buffer, delta as u16);
push_u16(&mut buffer, 1);
push_u16(&mut buffer, range_offset);
push_u16(&mut buffer, 0);
for glyph_id in glyphs {
push_u16(&mut buffer, *glyph_id);
}
buffer
}
fn format6_table(first_code: u16, glyphs: &[u16]) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 6);
push_u16(&mut buffer, (10 + glyphs.len() * 2) as u16);
push_u16(&mut buffer, 0);
push_u16(&mut buffer, first_code);
push_u16(&mut buffer, glyphs.len() as u16);
for glyph_id in glyphs {
push_u16(&mut buffer, *glyph_id);
}
buffer
}
fn format8_table() -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 8);
push_u16(&mut buffer, 0);
push_u32(&mut buffer, 8220);
push_u32(&mut buffer, 0);
buffer.extend_from_slice(&[0u8; 8192]);
push_u32(&mut buffer, 1);
push_u32(&mut buffer, 0x0001_F600);
push_u32(&mut buffer, 0x0001_F600);
push_u32(&mut buffer, 42);
buffer
}
fn format10_table(start_char_code: u32, glyphs: &[u16]) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 10);
push_u16(&mut buffer, 0);
push_u32(&mut buffer, (20 + glyphs.len() * 2) as u32);
push_u32(&mut buffer, 0);
push_u32(&mut buffer, start_char_code);
push_u32(&mut buffer, glyphs.len() as u32);
for glyph_id in glyphs {
push_u16(&mut buffer, *glyph_id);
}
buffer
}
fn format12_table(groups: &[(u32, u32, u32)]) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 12);
push_u16(&mut buffer, 0);
push_u32(&mut buffer, (16 + groups.len() * 12) as u32);
push_u32(&mut buffer, 0);
push_u32(&mut buffer, groups.len() as u32);
for (start_char_code, end_char_code, start_glyph_id) in groups {
push_u32(&mut buffer, *start_char_code);
push_u32(&mut buffer, *end_char_code);
push_u32(&mut buffer, *start_glyph_id);
}
buffer
}
fn format13_table(groups: &[(u32, u32, u32)]) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 13);
push_u16(&mut buffer, 0);
push_u32(&mut buffer, (16 + groups.len() * 12) as u32);
push_u32(&mut buffer, 0);
push_u32(&mut buffer, groups.len() as u32);
for (start_char_code, end_char_code, glyph_id) in groups {
push_u32(&mut buffer, *start_char_code);
push_u32(&mut buffer, *end_char_code);
push_u32(&mut buffer, *glyph_id);
}
buffer
}
fn format14_table(code: u32, selector: u32, glyph_id: u16) -> Vec<u8> {
let mut buffer = Vec::new();
push_u16(&mut buffer, 14);
push_u32(&mut buffer, 0);
push_u32(&mut buffer, 1);
let records_start = 10 + 11;
let non_default_offset = records_start as u32;
push_u24(&mut buffer, selector);
push_u32(&mut buffer, 0);
push_u32(&mut buffer, non_default_offset);
push_u32(&mut buffer, 1);
push_u24(&mut buffer, code);
push_u16(&mut buffer, glyph_id);
let length = buffer.len() as u32;
buffer[2..6].copy_from_slice(&length.to_be_bytes());
buffer
}
#[test]
fn cmap_format0_parses_byte_encoding() {
let table = cmap_subtable(format0_table());
assert_eq!(table.get_format(), 0);
let text = table.get_part_of_string(4);
assert!(text.contains("Format 0"));
assert!(text.contains("glyph_id_array"));
}
#[test]
fn cmap_format2_parses_high_byte_mapping() {
let table = cmap_subtable(format2_table());
match table {
CmapSubtable::Format2(format2) => {
assert_eq!(format2.format, 2);
assert_eq!(format2.length, 2566);
assert_eq!(format2.language, 0);
assert_eq!(format2.sub_header_keys.len(), 256);
assert_eq!(format2.sub_headers.len(), 256);
assert!(format2.glyph_id_array.is_empty());
}
_ => panic!("expected format 2"),
}
}
#[test]
fn cmap_format4_single_substitution_uses_delta() {
let cmap = cmap_encodings(vec![(3, 1, format4_table(0x0041, 0x0041, -60, 0, &[]))]);
assert_eq!(cmap.get_glyph_position(0x0041), 5);
assert_eq!(cmap.get_glyph_position(0x0042), 0);
}
#[test]
fn cmap_format4_single_substitution_uses_glyph_array() {
let cmap = cmap_encodings(vec![(3, 1, format4_table(0x0042, 0x0042, 0, 4, &[99]))]);
assert_eq!(cmap.get_glyph_position(0x0042), 99);
}
#[test]
fn cmap_format6_parses_trimmed_table() {
let table = cmap_subtable(format6_table(0x0030, &[10, 11, 12]));
match table {
CmapSubtable::Format6(format6) => {
assert_eq!(format6.format, 6);
assert_eq!(format6.first_code, 0x0030);
assert_eq!(format6.entry_count, 3);
assert_eq!(format6.glyph_id_array, vec![10, 11, 12]);
}
_ => panic!("expected format 6"),
}
}
#[test]
fn cmap_format8_parses_mixed_coverage() {
let table = cmap_subtable(format8_table());
match table {
CmapSubtable::Format8(format8) => {
assert_eq!(format8.format, 8);
assert_eq!(format8.reserved, 0);
assert_eq!(format8.num_groups, 1);
assert_eq!(format8.groups[0].start_char_code, 0x0001_F600);
assert_eq!(format8.groups[0].start_glyph_id, 42);
}
_ => panic!("expected format 8"),
}
}
#[test]
fn cmap_format10_parses_trimmed_array() {
let table = cmap_subtable(format10_table(0x0002_0000, &[7, 8, 9]));
match table {
CmapSubtable::Format10(format10) => {
assert_eq!(format10.format, 10);
assert_eq!(format10.start_char_code, 0x0002_0000);
assert_eq!(format10.num_chars, 3);
assert_eq!(format10.glyph_id_array, vec![7, 8, 9]);
}
_ => panic!("expected format 10"),
}
}
#[test]
fn cmap_format12_takes_priority_over_format4() {
let cmap = cmap_encodings(vec![
(3, 1, format4_table(0x0041, 0x0041, 4, 0, &[])),
(3, 10, format12_table(&[(0x0041, 0x0041, 200)])),
]);
assert_eq!(cmap.get_glyph_position(0x0041), 200);
}
#[test]
fn cmap_format13_maps_ranges_to_constant_glyphs() {
let cmap = cmap_encodings(vec![(3, 1, format13_table(&[(0x3400, 0x3402, 55)]))]);
assert_eq!(cmap.get_glyph_position(0x3401), 55);
}
#[test]
fn cmap_format14_resolves_unicode_variation_sequences() {
let cmap = cmap_encodings(vec![
(3, 1, format4_table(0x2764, 0x2764, 0, 4, &[20])),
(0, 5, format14_table(0x2764, 0xFE0F, 77)),
]);
assert_eq!(cmap.get_glyph_position(0x2764), 20);
assert_eq!(cmap.get_glyph_position_from_uvs(0x2764, 0xFE0F), 77);
assert_eq!(cmap.get_glyph_position_from_uvs(0x2764, 0xFE0E), 20);
}
fn test_fonts_dir() -> std::path::PathBuf {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let dot_dir = manifest_dir.join(".test_fonts");
if dot_dir.exists() {
dot_dir
} else {
manifest_dir.join("_test_fonts")
}
}
fn sample_font_path() -> std::path::PathBuf {
test_fonts_dir().join("ZenMaruGothic-Regular.ttf")
}
fn source_serif_variable_paths() -> Vec<std::path::PathBuf> {
existing_paths(vec![
test_fonts_dir()
.join("source")
.join("SourceSerif4-VariableFont_opsz,wght.ttf"),
test_fonts_dir()
.join("source")
.join("SourceSerif4-Italic-VariableFont_opsz,wght.ttf"),
])
}
fn source_serif_otf_paths() -> Vec<std::path::PathBuf> {
existing_paths(vec![test_fonts_dir()
.join("source-serif-4.005_Desktop")
.join("OTF")
.join("SourceSerif4-BlackIt.otf")])
}
fn woff2_font_path() -> std::path::PathBuf {
test_fonts_dir().join("notosanswoff2.woff2")
}
fn woff_font_path() -> std::path::PathBuf {
test_fonts_dir().join("MS-Gothic.ttf.woff")
}
fn fira_sans_black_path() -> std::path::PathBuf {
test_fonts_dir()
.join("Fira_Sans")
.join("FiraSans-Black.ttf")
}
fn fira_sans_regular_path() -> std::path::PathBuf {
test_fonts_dir()
.join("Fira_Sans")
.join("FiraSans-Regular.ttf")
}
fn segoe_emoji_font_path() -> std::path::PathBuf {
test_fonts_dir().join("windows").join("seguiemj.ttf")
}
fn noto_color_emoji_font_path() -> std::path::PathBuf {
test_fonts_dir().join("NotoColorEmoji-Regular.ttf")
}
fn twemoji_sbix_font_path() -> std::path::PathBuf {
test_fonts_dir()
.join("sbix")
.join("TwemojiMozilla-sbix.woff2")
}
fn collection_font_path() -> std::path::PathBuf {
test_fonts_dir().join("windows").join("msgothic.ttc")
}
fn yu_gothic_regular_collection_path() -> std::path::PathBuf {
test_fonts_dir().join("windows").join("YuGothR.ttc")
}
#[cfg(feature = "layout")]
fn yu_gothic_font() -> Option<crate::LoadedFont> {
let path = yu_gothic_regular_collection_path();
if !path.exists() {
return None;
}
crate::load_font_from_file(&path).ok()
}
fn rtl_font_path() -> std::path::PathBuf {
test_fonts_dir().join("windows").join("arial.ttf")
}
fn syriac_font_path() -> std::path::PathBuf {
test_fonts_dir()
.join("Noto_Sans_Syriac_Western")
.join("static")
.join("NotoSansSyriacWestern-Regular.ttf")
}
fn static_font_paths(dir: std::path::PathBuf) -> Vec<std::path::PathBuf> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut paths = entries
.filter_map(Result::ok)
.map(|entry| entry.path())
.filter(|path| {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| matches!(ext.to_ascii_lowercase().as_str(), "ttf" | "otf" | "ttc"))
.unwrap_or(false)
})
.collect::<Vec<_>>();
paths.sort();
paths
}
fn existing_paths(paths: Vec<std::path::PathBuf>) -> Vec<std::path::PathBuf> {
let mut existing = Vec::new();
for path in paths {
if path.exists() && !existing.contains(&path) {
existing.push(path);
}
}
existing
}
fn recursive_font_paths(dir: std::path::PathBuf) -> Vec<std::path::PathBuf> {
let mut stack = vec![dir];
let mut paths = Vec::new();
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if path
.components()
.any(|component| component.as_os_str().eq("error"))
{
continue;
}
stack.push(path);
continue;
}
let Some(ext) = path.extension().and_then(|ext| ext.to_str()) else {
continue;
};
if matches!(
ext.to_ascii_lowercase().as_str(),
"ttf" | "otf" | "ttc" | "woff" | "woff2"
) {
paths.push(path);
}
}
}
paths.sort();
paths
}
fn path_has_component(path: &std::path::Path, component: &str) -> bool {
path.components().any(|item| item.as_os_str().eq(component))
}
fn read_be_u16(data: &[u8], offset: usize) -> Option<u16> {
let bytes: [u8; 2] = data.get(offset..offset + 2)?.try_into().ok()?;
Some(u16::from_be_bytes(bytes))
}
fn read_be_u32(data: &[u8], offset: usize) -> Option<u32> {
let bytes: [u8; 4] = data.get(offset..offset + 4)?.try_into().ok()?;
Some(u32::from_be_bytes(bytes))
}
fn sfnt_face_offsets(data: &[u8]) -> Vec<usize> {
match data.get(0..4) {
Some(b"ttcf") => {
let Some(num_fonts) = read_be_u32(data, 8) else {
return Vec::new();
};
let mut offsets = Vec::new();
for index in 0..num_fonts as usize {
let offset_pos = 12 + index * 4;
if let Some(offset) = read_be_u32(data, offset_pos) {
offsets.push(offset as usize);
}
}
offsets
}
Some(_) => vec![0],
None => Vec::new(),
}
}
fn sfnt_face_has_table(data: &[u8], face_offset: usize, tag: &[u8; 4]) -> bool {
let Some(num_tables) = read_be_u16(data, face_offset + 4) else {
return false;
};
let mut offset = face_offset + 12;
for _ in 0..num_tables as usize {
let Some(table_tag) = data.get(offset..offset + 4) else {
return false;
};
if table_tag == tag {
return true;
}
offset += 16;
}
false
}
fn font_file_has_sfnt_table(path: &std::path::Path, tag: &[u8; 4]) -> bool {
let Ok(data) = std::fs::read(path) else {
return false;
};
sfnt_face_offsets(&data)
.into_iter()
.any(|offset| sfnt_face_has_table(&data, offset, tag))
}
fn fixture_font_corpus_paths() -> Vec<std::path::PathBuf> {
recursive_font_paths(test_fonts_dir())
}
fn fixture_engine_corpus_paths() -> Vec<std::path::PathBuf> {
let all = fixture_font_corpus_paths();
let test_root = test_fonts_dir();
let mut paths = Vec::new();
paths.extend(
all.iter()
.filter(|path| path.parent() == Some(test_root.as_path()))
.cloned(),
);
paths.extend(
all.iter()
.filter(|path| {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| matches!(ext.to_ascii_lowercase().as_str(), "otf" | "ttc"))
.unwrap_or(false)
})
.cloned(),
);
paths.extend(
all.iter()
.filter(|path| {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| matches!(ext.to_ascii_lowercase().as_str(), "woff" | "woff2"))
.unwrap_or(false)
&& !path_has_component(path, "noto-woff2")
})
.cloned(),
);
paths.extend(
all.iter()
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.contains("VariableFont"))
.unwrap_or(false)
})
.cloned(),
);
paths.extend(
all.iter()
.filter(|path| {
[
"Noto_Kufi_Arabic",
"Noto_Sans_Syriac_Western",
"Tibetan",
"windows",
"apple",
]
.iter()
.any(|component| path_has_component(path, component))
})
.cloned(),
);
let mut sampled_noto_woff2 = all
.iter()
.filter(|path| path_has_component(path, "noto-woff2"))
.cloned()
.collect::<Vec<_>>();
sampled_noto_woff2.truncate(32);
paths.extend(sampled_noto_woff2);
existing_paths(paths)
}
fn variable_font_fixture_paths() -> Vec<std::path::PathBuf> {
existing_paths(
fixture_font_corpus_paths()
.into_iter()
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.contains("VariableFont"))
.unwrap_or(false)
})
.collect(),
)
}
fn cff2_fixture_paths() -> Vec<std::path::PathBuf> {
existing_paths(
fixture_font_corpus_paths()
.into_iter()
.filter(|path| font_file_has_sfnt_table(path, b"CFF2"))
.collect(),
)
}
fn font_supports_text(font: &crate::LoadedFont, text: &str) -> bool {
let Some(cmap) = font.font().cmap.as_ref() else {
return false;
};
text.chars()
.filter(|ch| !ch.is_control() && !ch.is_whitespace())
.all(|ch| cmap.get_glyph_position(ch as u32) != 0)
}
fn public_api_smoke_sample(
face: &crate::FontFace,
) -> Option<(String, Option<&'static str>, crate::ShapingPolicy)> {
for ch in face.family().chars().chain(face.full_name().chars()) {
if ch.is_control() || ch.is_whitespace() {
continue;
}
let candidate = ch.to_string();
if font_supports_text(face, &candidate) {
return Some((candidate, None, crate::ShapingPolicy::LeftToRight));
}
}
let candidates = [
("A", None, crate::ShapingPolicy::LeftToRight),
("漢", Some("ja"), crate::ShapingPolicy::LeftToRight),
("あ", Some("ja"), crate::ShapingPolicy::LeftToRight),
("אב", Some("he-Hebr"), crate::ShapingPolicy::RightToLeft),
("اب", Some("ar"), crate::ShapingPolicy::RightToLeft),
("ܐܰ", Some("syr-Syrc"), crate::ShapingPolicy::RightToLeft),
("ཀ", None, crate::ShapingPolicy::LeftToRight),
("𐤀", None, crate::ShapingPolicy::RightToLeft),
("𐡀", None, crate::ShapingPolicy::RightToLeft),
("𐩠", None, crate::ShapingPolicy::RightToLeft),
("𐎀", None, crate::ShapingPolicy::LeftToRight),
("𔑀", None, crate::ShapingPolicy::LeftToRight),
("𗀀", None, crate::ShapingPolicy::LeftToRight),
("😀", None, crate::ShapingPolicy::LeftToRight),
];
for (text, locale, policy) in candidates {
if font_supports_text(face, text) {
return Some((text.to_string(), locale, policy));
}
}
None
}
#[cfg(debug_assertions)]
fn run_raw_dump_smoke_for_paths(paths: &[std::path::PathBuf]) -> (usize, Vec<String>) {
let mut dumped_faces = 0usize;
let mut failures = Vec::new();
for path in paths {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut font = crate::Font::get_font_from_file(path)
.map_err(|err| format!("get_font_from_file failed: {err}"))?;
let face_count = font.get_font_count();
if face_count == 0 {
return Err("face_count was zero".to_string());
}
for index in 0..face_count {
font.set_font(index)
.map_err(|err| format!("set_font({index}) failed: {err}"))?;
let _ = font.get_header_raw();
let _ = font.get_name_raw();
let _ = font.get_maxp_raw();
let _ = font.get_os2_raw();
let _ = font.get_hhea_raw();
let _ = font.get_cmap_raw();
let _ = font.get_post_raw();
let _ = font.get_sbix_raw();
let _ = font.get_svg_raw();
let _ = font.get_colr_raw();
let _ = font.get_cpal_raw();
#[cfg(feature = "layout")]
{
let _ = font.get_vhea_raw();
let _ = font.get_gdef_raw();
let _ = font.get_gsub_raw();
}
}
Ok::<usize, String>(face_count)
}));
match outcome {
Ok(Ok(face_count)) => dumped_faces += face_count,
Ok(Err(err)) if should_skip_corpus_error(path, &err) => {}
Ok(Err(err)) => failures.push(format!("{}: {}", path.display(), err)),
Err(_) => failures.push(format!("{}: panic", path.display())),
}
}
(dumped_faces, failures)
}
fn run_public_api_smoke_for_paths(
paths: &[std::path::PathBuf],
) -> (usize, usize, usize, Vec<String>) {
let mut shaped = 0usize;
let mut svg_successes = 0usize;
let mut skipped = 0usize;
let mut failures = Vec::new();
for path in paths {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let file = crate::FontFile::from_file(path)
.map_err(|err| format!("FontFile::from_file failed: {err}"))?;
let face = file
.current_face()
.map_err(|err| format!("current_face failed: {err}"))?;
let Some((sample, locale, policy)) = public_api_smoke_sample(&face) else {
return Ok::<(bool, bool), String>((false, false));
};
let mut engine = face
.engine()
.with_font_size(32.0)
.with_shaping_policy(policy);
if let Some(locale) = locale {
engine = engine.with_locale(locale);
}
let run = engine
.shape(&sample)
.map_err(|err| format!("shape({sample:?}) failed: {err}"))?;
if run.glyphs.is_empty() {
return Err(format!("shape({sample:?}) returned no glyphs"));
}
let width = engine
.measure(&sample)
.map_err(|err| format!("measure({sample:?}) failed: {err}"))?;
if width <= 0.0 {
return Err(format!("measure({sample:?}) returned non-positive width"));
}
let svg_result = match engine.render_svg(&sample) {
Ok(svg) => {
if !svg.contains("<svg") {
return Err(format!("render_svg({sample:?}) returned non-SVG output"));
}
true
}
Err(err)
if err.kind() == std::io::ErrorKind::Unsupported
&& err
.to_string()
.contains("SVG glyph layers are not supported yet") =>
{
false
}
Err(err) => {
return Err(format!("render_svg({sample:?}) failed: {err}"));
}
};
Ok((true, svg_result))
}));
match outcome {
Ok(Ok((false, _))) => skipped += 1,
Ok(Ok((true, svg_ok))) => {
shaped += 1;
if svg_ok {
svg_successes += 1;
}
}
Ok(Err(err)) if should_skip_corpus_error(path, &err) => skipped += 1,
Ok(Err(err)) => failures.push(format!("{}: {}", path.display(), err)),
Err(_) => failures.push(format!("{}: panic", path.display())),
}
}
(shaped, svg_successes, skipped, failures)
}
fn should_skip_corpus_error(path: &std::path::Path, error: &str) -> bool {
error.contains("SVG glyph layers are not supported yet")
|| (path
.file_name()
.and_then(|name| name.to_str())
.map(|name| name.eq_ignore_ascii_case("SansSerifCollection.ttf"))
.unwrap_or(false)
&& error.contains("ountbound call ptr"))
}
#[cfg(feature = "layout")]
fn arabic_boundary_font_paths() -> Vec<std::path::PathBuf> {
let mut paths = vec![
rtl_font_path(),
test_fonts_dir().join("windows").join("arialbd.ttf"),
test_fonts_dir().join("windows").join("ariali.ttf"),
test_fonts_dir().join("windows").join("arialbi.ttf"),
test_fonts_dir().join("windows").join("ariblk.ttf"),
test_fonts_dir().join("apple").join("Arial Unicode.ttf"),
test_fonts_dir()
.join("Noto_Kufi_Arabic")
.join("NotoKufiArabic-VariableFont_wght.ttf"),
];
paths.extend(static_font_paths(
test_fonts_dir().join("Noto_Kufi_Arabic").join("static"),
));
existing_paths(paths)
}
#[cfg(feature = "layout")]
fn syriac_boundary_font_paths() -> Vec<std::path::PathBuf> {
let mut paths = vec![test_fonts_dir()
.join("Noto_Sans_Syriac_Western")
.join("NotoSansSyriacWestern-VariableFont_wght.ttf")];
paths.extend(static_font_paths(
test_fonts_dir()
.join("Noto_Sans_Syriac_Western")
.join("static"),
));
existing_paths(paths)
}
#[cfg(feature = "layout")]
fn hebrew_boundary_font_paths() -> Vec<std::path::PathBuf> {
existing_paths(vec![
rtl_font_path(),
test_fonts_dir().join("windows").join("arialbd.ttf"),
test_fonts_dir().join("windows").join("ariali.ttf"),
test_fonts_dir().join("windows").join("arialbi.ttf"),
test_fonts_dir().join("windows").join("ARIALN.TTF"),
test_fonts_dir().join("apple").join("Arial Unicode.ttf"),
])
}
#[cfg(feature = "layout")]
fn tibetan_boundary_font_paths() -> Vec<std::path::PathBuf> {
existing_paths(vec![
test_fonts_dir()
.join("Tibetan")
.join("BabelStoneTibetan.ttf"),
test_fonts_dir()
.join("Tibetan")
.join("BabelStoneTibetanSlim.ttf"),
])
}
#[cfg(feature = "layout")]
fn rtl_contextual_font_paths() -> Vec<std::path::PathBuf> {
existing_paths(vec![
rtl_font_path(),
test_fonts_dir()
.join("Noto_Sans")
.join("static")
.join("NotoSans-Regular.ttf"),
test_fonts_dir().join("apple").join("Arial Unicode.ttf"),
])
}
#[cfg(feature = "layout")]
fn collapse_ligatures_like_text_api(
gsub: &crate::opentype::extentions::gsub::GSUB,
glyphs: &[(usize, usize)],
locale: Option<&str>,
is_right_to_left: bool,
) -> Vec<usize> {
const MAX_LIGATURE_COMPONENTS: usize = 8;
let glyph_ids: Vec<usize> = glyphs.iter().map(|glyph| glyph.0).collect();
let mut collapsed = Vec::new();
let mut index = 0;
while index < glyph_ids.len() {
let max_len = (glyph_ids.len() - index).min(MAX_LIGATURE_COMPONENTS);
let mut matched = None;
for len in (2..=max_len).rev() {
if is_right_to_left {
if let Some(glyph_id) =
gsub.lookup_rlig_sequence(&glyph_ids[index..index + len], locale)
{
matched = Some((glyph_id, len));
break;
}
}
if let Some(glyph_id) = gsub.lookup_liga_sequence(&glyph_ids[index..index + len]) {
matched = Some((glyph_id, len));
break;
}
}
if let Some((glyph_id, len)) = matched {
collapsed.push(glyph_id);
index += len;
} else {
collapsed.push(glyph_ids[index]);
index += 1;
}
}
collapsed
}
#[cfg(feature = "layout")]
fn first_real_arabic_joining_pair(font: &crate::LoadedFont) -> Option<(String, Vec<usize>)> {
let gsub = font.font().gsub.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
let candidates: Vec<char> = (0x0621u32..=0x064Au32)
.filter_map(char::from_u32)
.filter(|ch| cmap.get_glyph_position(*ch as u32) != 0)
.collect();
for left in candidates.iter().copied() {
let left_glyph = cmap.get_glyph_position(left as u32) as usize;
let left_forms = gsub.lookup_joining_forms(left_glyph, Some("ar"));
if !left_forms.can_join_to_next() {
continue;
}
for right in candidates.iter().copied() {
let right_glyph = cmap.get_glyph_position(right as u32) as usize;
let right_forms = gsub.lookup_joining_forms(right_glyph, Some("ar"));
if !right_forms.can_join_to_prev() {
continue;
}
let expected = vec![
left_forms.substitute(left_glyph, false, true),
right_forms.substitute(right_glyph, true, false),
];
if expected[0] != left_glyph || expected[1] != right_glyph {
return Some((format!("{left}{right}"), expected));
}
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_arabic_rlig_sequence(font: &crate::LoadedFont) -> Option<(String, usize)> {
let gsub = font.font().gsub.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
let candidates: Vec<char> = (0x0621u32..=0x064Au32)
.filter_map(char::from_u32)
.filter(|ch| cmap.get_glyph_position(*ch as u32) != 0)
.collect();
for left in candidates.iter().copied() {
let left_glyph = cmap.get_glyph_position(left as u32) as usize;
let left_forms = gsub.lookup_joining_forms(left_glyph, Some("ar"));
for right in candidates.iter().copied() {
let right_glyph = cmap.get_glyph_position(right as u32) as usize;
let right_forms = gsub.lookup_joining_forms(right_glyph, Some("ar"));
let joined = [
left_forms.substitute(
left_glyph,
false,
left_forms.can_join_to_next() && right_forms.can_join_to_prev(),
),
right_forms.substitute(
right_glyph,
left_forms.can_join_to_next() && right_forms.can_join_to_prev(),
false,
),
];
if let Some(ligature) = gsub.lookup_rlig_sequence(&joined, Some("ar")) {
return Some((format!("{left}{right}"), ligature));
}
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_arabic_contextual_sequence_in_font(
font: &crate::LoadedFont,
) -> Option<(String, Vec<usize>)> {
let gsub = font.font().gsub.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
let candidates: Vec<char> = (0x0621u32..=0x064Au32)
.filter_map(char::from_u32)
.filter(|ch| cmap.get_glyph_position(*ch as u32) != 0)
.collect();
let try_sequence = |chars: &[char]| -> Option<(String, Vec<usize>)> {
let mut joined = chars
.iter()
.enumerate()
.map(|(index, ch)| (cmap.get_glyph_position(*ch as u32) as usize, index))
.collect::<Vec<_>>();
gsub.apply_joining_sequence(&mut joined, Some("ar"));
let baseline = joined.iter().map(|glyph| glyph.0).collect::<Vec<_>>();
let mut contextual = joined.clone();
gsub.apply_feature_sequence(&mut contextual, Some("ar"), &[*b"rclt", *b"calt"]);
let contextual_ids = contextual.iter().map(|glyph| glyph.0).collect::<Vec<_>>();
if contextual_ids == baseline || contextual_ids.len() != baseline.len() {
return None;
}
let final_ids = collapse_ligatures_like_text_api(gsub, &contextual, Some("ar"), true);
if final_ids == baseline {
return None;
}
Some((chars.iter().collect::<String>(), final_ids))
};
for left in candidates.iter().copied() {
for right in candidates.iter().copied() {
if let Some(found) = try_sequence(&[left, right]) {
return Some(found);
}
}
}
for first in candidates.iter().copied() {
for second in candidates.iter().copied() {
for third in candidates.iter().copied() {
if let Some(found) = try_sequence(&[first, second, third]) {
return Some(found);
}
}
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_arabic_contextual_sequence() -> Option<(std::path::PathBuf, String, Vec<usize>)> {
for path in rtl_contextual_font_paths() {
if !path.exists() {
continue;
}
let Ok(font) = crate::load_font_from_file(&path) else {
continue;
};
if let Some((text, glyph_ids)) = first_real_arabic_contextual_sequence_in_font(&font) {
return Some((path, text, glyph_ids));
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_mark_attachment_cluster(
font: &crate::LoadedFont,
base_range: std::ops::RangeInclusive<u32>,
mark_range: std::ops::RangeInclusive<u32>,
) -> Option<String> {
let gdef = font.font().gdef.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
for base in base_range {
let Some(base_char) = char::from_u32(base) else {
continue;
};
let base_glyph = cmap.get_glyph_position(base) as u16;
if base_glyph == 0 {
continue;
}
for mark in mark_range.clone() {
let Some(mark_char) = char::from_u32(mark) else {
continue;
};
let mark_glyph = cmap.get_glyph_position(mark) as u16;
if mark_glyph == 0 {
continue;
}
if gdef.mark_attachment_class(mark_glyph).is_some()
|| gdef.has_attach_points(mark_glyph)
{
return Some(format!("{base_char}{mark_char}"));
}
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_mark_cluster(
font: &crate::LoadedFont,
base_range: std::ops::RangeInclusive<u32>,
mark_range: std::ops::RangeInclusive<u32>,
) -> Option<String> {
let cmap = font.font().cmap.as_ref()?;
for base in base_range {
let Some(base_char) = char::from_u32(base) else {
continue;
};
if cmap.get_glyph_position(base) == 0 {
continue;
}
for mark in mark_range.clone() {
let Some(mark_char) = char::from_u32(mark) else {
continue;
};
if cmap.get_glyph_position(mark) != 0 {
return Some(format!("{base_char}{mark_char}"));
}
}
}
None
}
#[cfg(feature = "layout")]
fn count_mark_boundary_successes(
paths: &[std::path::PathBuf],
locale: &str,
is_right_to_left: bool,
detector: fn(&crate::LoadedFont) -> Option<String>,
) -> usize {
let mut successes = 0usize;
for path in paths {
let Ok(Ok(font)) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::load_font_from_file(path)
})) else {
continue;
};
let Some(cluster) = detector(&font) else {
continue;
};
let regular = crate::load_font_from_file(fira_sans_regular_path())
.expect("load regular fira sans");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(font);
let text = format!("A{cluster}B");
let Ok(Ok(face_indices)) =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut options = family.options().with_font_size(32.0).with_locale(locale);
if is_right_to_left {
options = options.with_right_to_left();
}
family.debug_face_indices_for_text(&text, options)
}))
else {
continue;
};
if face_indices == vec![0, 1, 0] {
successes += 1;
}
}
successes
}
#[cfg(feature = "layout")]
fn detect_arabic_mark_cluster(font: &crate::LoadedFont) -> Option<String> {
first_real_mark_cluster(font, 0x0621..=0x064A, 0x0610..=0x065F)
}
#[cfg(feature = "layout")]
fn detect_syriac_mark_attachment_cluster(font: &crate::LoadedFont) -> Option<String> {
first_real_mark_attachment_cluster(font, 0x0710..=0x072C, 0x0730..=0x074A)
}
#[cfg(feature = "layout")]
fn first_real_gpos_mark_to_base_cluster(
font: &crate::LoadedFont,
locale: &str,
base_range: std::ops::RangeInclusive<u32>,
mark_range: std::ops::RangeInclusive<u32>,
) -> Option<(
String,
crate::opentype::extentions::gpos::MarkAttachmentAdjustment,
)> {
let gpos = font.font().gpos.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
for base in base_range {
let Some(base_char) = char::from_u32(base) else {
continue;
};
let base_glyph = cmap.get_glyph_position(base) as u16;
if base_glyph == 0 {
continue;
}
for mark in mark_range.clone() {
let Some(mark_char) = char::from_u32(mark) else {
continue;
};
let mark_glyph = cmap.get_glyph_position(mark) as u16;
if mark_glyph == 0 {
continue;
}
let Some(adjustment) =
gpos.lookup_mark_to_base_adjustment(base_glyph, mark_glyph, Some(locale))
else {
continue;
};
return Some((format!("{base_char}{mark_char}"), adjustment));
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_gpos_mark_stack_cluster(
font: &crate::LoadedFont,
locale: &str,
base_range: std::ops::RangeInclusive<u32>,
mark_range: std::ops::RangeInclusive<u32>,
) -> Option<(
String,
crate::opentype::extentions::gpos::MarkAttachmentAdjustment,
crate::opentype::extentions::gpos::MarkAttachmentAdjustment,
)> {
let gpos = font.font().gpos.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
for base in base_range {
let Some(base_char) = char::from_u32(base) else {
continue;
};
let base_glyph = cmap.get_glyph_position(base) as u16;
if base_glyph == 0 {
continue;
}
for mark1 in mark_range.clone() {
let Some(mark1_char) = char::from_u32(mark1) else {
continue;
};
let mark1_glyph = cmap.get_glyph_position(mark1) as u16;
if mark1_glyph == 0 {
continue;
}
let Some(base_adjustment) =
gpos.lookup_mark_to_base_adjustment(base_glyph, mark1_glyph, Some(locale))
else {
continue;
};
for mark2 in mark_range.clone() {
let Some(mark2_char) = char::from_u32(mark2) else {
continue;
};
let mark2_glyph = cmap.get_glyph_position(mark2) as u16;
if mark2_glyph == 0 {
continue;
}
let Some(mark_adjustment) =
gpos.lookup_mark_to_mark_adjustment(mark1_glyph, mark2_glyph, Some(locale))
else {
continue;
};
return Some((
format!("{base_char}{mark1_char}{mark2_char}"),
base_adjustment,
mark_adjustment,
));
}
}
}
None
}
#[cfg(feature = "layout")]
fn detect_hebrew_mark_cluster(font: &crate::LoadedFont) -> Option<String> {
first_real_mark_cluster(font, 0x05D0..=0x05EA, 0x0591..=0x05C7)
}
#[cfg(feature = "layout")]
fn detect_arabic_mark_stack_cluster(font: &crate::LoadedFont) -> Option<String> {
first_real_gpos_mark_stack_cluster(font, "ar", 0x0621..=0x064A, 0x0610..=0x065F)
.map(|(cluster, _, _)| cluster)
}
#[cfg(feature = "layout")]
fn detect_syriac_mark_stack_cluster(font: &crate::LoadedFont) -> Option<String> {
first_real_gpos_mark_stack_cluster(font, "syr-Syrc", 0x0710..=0x072C, 0x0730..=0x074A)
.map(|(cluster, _, _)| cluster)
}
#[cfg(feature = "layout")]
fn detect_tibetan_mark_cluster(font: &crate::LoadedFont) -> Option<String> {
let cmap = font.font().cmap.as_ref()?;
for base in 0x0F40..=0x0F6C {
let Some(base_char) = char::from_u32(base) else {
continue;
};
if cmap.get_glyph_position(base) == 0 {
continue;
}
for mark in 0x0F00..=0x0FFF {
let Some(mark_char) = char::from_u32(mark) else {
continue;
};
if cmap.get_glyph_position(mark) == 0 {
continue;
}
let text = format!("{base_char}{mark_char}");
let units = crate::fontreader::Font::parse_text_units_for_fallback(&text);
if units.len() != 1 {
continue;
}
if let crate::fontreader::ParsedTextUnit::Glyph { text: parsed, .. } = &units[0] {
if parsed == &text {
return Some(text);
}
}
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_kern_pair(font: &crate::LoadedFont) -> Option<(char, char, i16)> {
let gpos = font.font().gpos.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
let candidates = "AVWToYLT.,abcdefghijklmnopqrstuvwxyz";
for left in candidates.chars() {
let left_glyph = cmap.get_glyph_position(left as u32) as u16;
if left_glyph == 0 {
continue;
}
for right in candidates.chars() {
let right_glyph = cmap.get_glyph_position(right as u32) as u16;
if right_glyph == 0 {
continue;
}
let Some(adjustment) =
gpos.lookup_pair_adjustment(left_glyph, right_glyph, false, None)
else {
continue;
};
let total_advance = adjustment
.first
.x_advance
.saturating_add(adjustment.second.x_advance);
if total_advance != 0 {
return Some((left, right, total_advance));
}
}
}
None
}
#[cfg(feature = "cff")]
fn cff_font_path() -> std::path::PathBuf {
test_fonts_dir().join("NotoSansJP-Black.otf")
}
fn japanese_font_path() -> std::path::PathBuf {
test_fonts_dir().join("NotoSansJP-Regular.otf")
}
#[cfg(feature = "layout")]
fn japanese_layout_font_paths() -> Vec<std::path::PathBuf> {
vec![
japanese_font_path(),
test_fonts_dir().join("NotoSansCJK-Regular.ttc"),
test_fonts_dir().join("windows").join("msgothic.ttc"),
test_fonts_dir().join("windows").join("YuGothR.ttc"),
]
}
#[cfg(feature = "layout")]
fn first_real_variant_substitution(
font_variant: crate::FontVariant,
) -> Option<(std::path::PathBuf, char, usize, usize)> {
for path in japanese_layout_font_paths() {
if !path.exists() {
continue;
}
let Ok(font) = crate::load_font_from_file(&path) else {
continue;
};
let Some(cmap) = font.font().cmap.as_ref() else {
continue;
};
let Some(gsub) = font.font().gsub.as_ref() else {
continue;
};
for codepoint in 0x20u32..=0xFFFF {
let Some(ch) = char::from_u32(codepoint) else {
continue;
};
if ch.is_control() {
continue;
}
let glyph_id = cmap.get_glyph_position(codepoint) as usize;
if glyph_id == 0 {
continue;
}
let mut variant_glyphs = vec![(glyph_id, 0usize)];
gsub.apply_variant_sequence(&mut variant_glyphs, Some("ja-JP"), font_variant);
if let Some((variant_glyph_id, _)) = variant_glyphs.first().copied() {
if variant_glyph_id != glyph_id {
return Some((path, ch, glyph_id, variant_glyph_id));
}
}
}
}
None
}
#[cfg(feature = "layout")]
fn first_real_vertical_substitution(font: &crate::LoadedFont) -> Option<(char, u16, u16)> {
let gsub = font.font().gsub.as_ref()?;
let cmap = font.font().cmap.as_ref()?;
let candidates = [
'(', ')', '[', ']', '{', '}', '!', '?', ',', '.', ':', ';', '、', '。', '「', '」',
'(', ')', 'ー', '〜', '<', '>',
];
for ch in candidates {
let glyph_id = cmap.get_glyph_position(ch as u32) as u16;
if glyph_id == 0 {
continue;
}
let vertical = gsub.lookup_vertical(glyph_id).unwrap_or(glyph_id);
if vertical != glyph_id {
return Some((ch, glyph_id, vertical));
}
}
None
}
fn legacy_variation_selector_font_candidates() -> Vec<std::path::PathBuf> {
vec![
test_fonts_dir().join("windows").join("msgothic.ttc"),
test_fonts_dir().join("windows").join("msjh.ttc"),
test_fonts_dir().join("windows").join("msyh.ttc"),
test_fonts_dir().join("windows").join("YuGothR.ttc"),
test_fonts_dir().join("ZenMaruGothic-Regular.ttf"),
]
}
fn first_real_sbix_font_path() -> Option<std::path::PathBuf> {
let preferred_dir = test_fonts_dir().join("sbix");
if let Ok(entries) = std::fs::read_dir(&preferred_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() {
return Some(path);
}
}
}
let mut stack = vec![test_fonts_dir()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
continue;
}
let Some(ext) = path.extension().and_then(|ext| ext.to_str()) else {
continue;
};
if !matches!(
ext.to_ascii_lowercase().as_str(),
"ttf" | "otf" | "ttc" | "woff" | "woff2"
) {
continue;
}
let Ok(bytes) = std::fs::read(&path) else {
continue;
};
if bytes.windows(4).any(|window| window == b"sbix") {
return Some(path);
}
}
}
None
}
fn emoji_ligature_font_candidates() -> Vec<std::path::PathBuf> {
vec![
noto_color_emoji_font_path(),
segoe_emoji_font_path(),
twemoji_sbix_font_path(),
]
}
fn emoji_ligature_sequence_candidates() -> [&'static str; 7] {
["👩💻", "👨👩👧👦", "🏳️🌈", "❤️", "🇯🇵", "1️⃣", "👩🏽💻"]
}
#[cfg(feature = "svg-fonts")]
fn direct_svg_emoji_font_path(name: &str) -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join(name)
}
#[cfg(feature = "svg-fonts")]
fn assert_svg_font_supports_any_sequence(font_name: &str, sequences: &[&str], label: &str) {
let path = direct_svg_emoji_font_path(font_name);
let bytes = std::fs::read(&path)
.unwrap_or_else(|err| panic!("read {font_name} for {label}: {err}"));
let font = crate::load_font_from_buffer(&bytes)
.unwrap_or_else(|err| panic!("load {font_name} for {label}: {err}"));
for sequence in sequences {
let Ok(run) = crate::text2commands(
sequence,
crate::FontOptions::new(&font).with_font_size(32.0),
) else {
continue;
};
if run.glyphs.len() != 1 {
continue;
}
if run.glyphs[0].glyph.layers.iter().any(|layer| {
matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty())
|| matches!(layer, crate::GlyphLayer::Svg(layer) if !layer.document.is_empty())
}) {
let svg = font
.text2svg(sequence, 32.0, "px")
.unwrap_or_else(|err| panic!("render {font_name} {sequence:?}: {err}"));
assert!(svg.contains("<svg"), "expected svg output for {font_name} {sequence:?}");
return;
}
}
panic!(
"expected {font_name} to support at least one {label} sequence from {:?}",
sequences
);
}
#[cfg(feature = "svg-fonts")]
fn assert_svg_font_family_fallback_supports_any_sequence(
font_name: &str,
sequences: &[&str],
label: &str,
) {
let regular_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("Fira_Sans")
.join("FiraSans-Regular.ttf");
let regular = crate::load_font_from_file(®ular_path).expect("load regular fira sans");
let path = direct_svg_emoji_font_path(font_name);
let bytes = std::fs::read(&path)
.unwrap_or_else(|err| panic!("read {font_name} for fallback {label}: {err}"));
let emoji = crate::load_font_from_buffer(&bytes)
.unwrap_or_else(|err| panic!("load {font_name} for fallback {label}: {err}"));
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(emoji);
for sequence in sequences {
let text = format!("A{sequence}B");
let Ok(run) = crate::text2commands(
&text,
crate::FontOptions::from_family(&family)
.with_font_family("Fira Sans")
.with_font_size(32.0),
) else {
continue;
};
if run.glyphs.len() != 3 {
continue;
}
if run.glyphs[1].glyph.layers.iter().any(|layer| {
matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty())
|| matches!(layer, crate::GlyphLayer::Svg(layer) if !layer.document.is_empty())
}) {
let svg = family
.text2svg(&text, 32.0, "px")
.unwrap_or_else(|err| panic!("family svg {font_name} {sequence:?}: {err}"));
assert!(svg.starts_with("<svg"));
return;
}
}
panic!(
"expected fallback family with {font_name} to support at least one {label} sequence from {:?}",
sequences
);
}
#[cfg(feature = "svg-fonts")]
fn first_svg_gradient_payload(font_name: &str) -> Option<(String, String)> {
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path).ok()?;
let svg = font.font().svg.as_ref()?;
for sequence in emoji_ligature_sequence_candidates() {
let Ok(glyph_ids) = font.font().debug_shape_glyph_ids(sequence, None) else {
continue;
};
if glyph_ids.len() != 1 || glyph_ids[0] == 0 {
continue;
}
let layout = font.font().get_layout(glyph_ids[0], false);
let Some(document) = svg.get_glyph_document(glyph_ids[0] as u32, &layout) else {
continue;
};
if document.payload.contains("linearGradient")
|| document.payload.contains("radialGradient")
{
return Some((sequence.to_string(), document.payload));
}
}
None
}
#[cfg(feature = "svg-fonts")]
fn first_svg_inherited_gradient_payload(font_name: &str) -> Option<(String, String)> {
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path).ok()?;
let svg = font.font().svg.as_ref()?;
for sequence in emoji_ligature_sequence_candidates() {
let Ok(glyph_ids) = font.font().debug_shape_glyph_ids(sequence, None) else {
continue;
};
if glyph_ids.len() != 1 || glyph_ids[0] == 0 {
continue;
}
let layout = font.font().get_layout(glyph_ids[0], false);
let Some(document) = svg.get_glyph_document(glyph_ids[0] as u32, &layout) else {
continue;
};
let has_gradient = document.payload.contains("linearGradient")
|| document.payload.contains("radialGradient");
let has_reference = document.payload.contains("href=\"#")
|| document.payload.contains("xlink:href=\"#");
if has_gradient && has_reference {
return Some((sequence.to_string(), document.payload));
}
}
None
}
#[cfg(feature = "svg-fonts")]
fn payload_has_arc_path_command(payload: &str) -> bool {
let mut source = payload;
while let Some(path_start) = source.find("<path") {
source = &source[path_start + 5..];
let Some(d_start) = source.find("d=\"") else {
continue;
};
let d_source = &source[d_start + 3..];
let Some(d_end) = d_source.find('"') else {
break;
};
let d = &d_source[..d_end];
if d.chars().any(|ch| matches!(ch, 'A' | 'a')) {
return true;
}
source = &d_source[d_end..];
}
false
}
#[cfg(feature = "svg-fonts")]
fn first_svg_arc_payload(font_name: &str) -> Option<(String, String)> {
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path).ok()?;
let svg = font.font().svg.as_ref()?;
for sequence in emoji_ligature_sequence_candidates() {
let Ok(glyph_ids) = font.font().debug_shape_glyph_ids(sequence, None) else {
continue;
};
if glyph_ids.len() != 1 || glyph_ids[0] == 0 {
continue;
}
let layout = font.font().get_layout(glyph_ids[0], false);
let Some(document) = svg.get_glyph_document(glyph_ids[0] as u32, &layout) else {
continue;
};
if payload_has_arc_path_command(&document.payload) {
return Some((sequence.to_string(), document.payload));
}
}
None
}
#[cfg(feature = "svg-fonts")]
fn payload_has_transform(payload: &str) -> bool {
payload.contains("transform=\"")
&& (payload.contains("rotate(")
|| payload.contains("skewX(")
|| payload.contains("skewY(")
|| payload.contains("translate(")
|| payload.contains("scale(")
|| payload.contains("matrix("))
}
#[cfg(feature = "svg-fonts")]
fn first_svg_transform_payload(font_name: &str) -> Option<(String, String)> {
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path).ok()?;
let svg = font.font().svg.as_ref()?;
for sequence in emoji_ligature_sequence_candidates() {
let Ok(glyph_ids) = font.font().debug_shape_glyph_ids(sequence, None) else {
continue;
};
if glyph_ids.len() != 1 || glyph_ids[0] == 0 {
continue;
}
let layout = font.font().get_layout(glyph_ids[0], false);
let Some(document) = svg.get_glyph_document(glyph_ids[0] as u32, &layout) else {
continue;
};
if payload_has_transform(&document.payload) {
return Some((sequence.to_string(), document.payload));
}
}
None
}
#[cfg(feature = "svg-fonts")]
fn payload_has_unsupported_svg_constructs(payload: &str) -> bool {
crate::svgparse::svg_requires_svg_fallback(payload)
}
#[cfg(feature = "svg-fonts")]
fn first_svg_payload_requiring_fallback(font_name: &str) -> Option<(String, String)> {
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path).ok()?;
let svg = font.font().svg.as_ref()?;
for sequence in emoji_ligature_sequence_candidates() {
let Ok(glyph_ids) = font.font().debug_shape_glyph_ids(sequence, None) else {
continue;
};
if glyph_ids.len() != 1 || glyph_ids[0] == 0 {
continue;
}
let layout = font.font().get_layout(glyph_ids[0], false);
let Some(document) = svg.get_glyph_document(glyph_ids[0] as u32, &layout) else {
continue;
};
if payload_has_unsupported_svg_constructs(&document.payload) {
return Some((sequence.to_string(), document.payload));
}
}
None
}
#[cfg(feature = "svg-fonts")]
fn first_svg_gid_requiring_fallback(font_name: &str) -> Option<(usize, String)> {
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path).ok()?;
let svg = font.font().svg.as_ref()?;
let max_glyphs = font.font().maxp.as_ref()?.num_glyphs as usize;
for glyph_id in 1..max_glyphs {
if !svg.has_glyph(glyph_id as u32) {
continue;
}
let layout = font.font().get_layout(glyph_id, false);
let Some(document) = svg.get_glyph_document(glyph_id as u32, &layout) else {
continue;
};
if payload_has_unsupported_svg_constructs(&document.payload) {
return Some((glyph_id, document.payload));
}
}
None
}
fn first_real_emoji_ligature() -> Option<(std::path::PathBuf, &'static str)> {
for path in emoji_ligature_font_candidates() {
if !path.exists() {
continue;
}
let Ok(font) = crate::load_font_from_file(&path) else {
continue;
};
for sequence in emoji_ligature_sequence_candidates() {
let Ok(glyph_ids) = font.font().debug_shape_glyph_ids(sequence, None) else {
continue;
};
if glyph_ids.len() != 1 || glyph_ids[0] == 0 {
continue;
}
let Ok(run) = font.text2glyph_run(
sequence,
crate::FontOptions::from_font_ref(crate::FontRef::Loaded(&font))
.with_font_size(32.0),
) else {
continue;
};
if run.glyphs.len() == 1 && !run.glyphs[0].glyph.layers.is_empty() {
return Some((path, sequence));
}
}
}
None
}
fn first_real_emoji_ligature_for_legacy() -> Option<(std::path::PathBuf, &'static str)> {
for path in emoji_ligature_font_candidates() {
if !path.exists() {
continue;
}
let Ok(font) = crate::load_font_from_file(&path) else {
continue;
};
for sequence in emoji_ligature_sequence_candidates() {
let Ok(commands) = font.font().text2command(sequence) else {
continue;
};
if commands.len() == 1
&& (commands[0].bitmap.is_some() || !commands[0].commands.is_empty())
{
return Some((path, sequence));
}
}
}
None
}
fn first_truetype_variation_selector_font_path() -> Option<std::path::PathBuf> {
for path in legacy_variation_selector_font_candidates() {
if !path.exists() {
continue;
}
let Ok(Ok(font)) = std::panic::catch_unwind(|| crate::load_font_from_file(&path))
else {
continue;
};
if font.font().glyf.is_none() {
continue;
}
let Some(cmap) = font.font().cmap.as_ref() else {
continue;
};
let has_format14 = cmap.cmap_encodings.iter().any(|encoding| {
matches!(
encoding.cmap_subtable.as_ref(),
CmapSubtable::Format14(format14)
if !format14.var_selector_records.is_empty()
)
});
if has_format14 {
return Some(path);
}
}
None
}
fn real_variation_sequence(font: &crate::LoadedFont) -> (String, usize) {
let cmap = font.font().cmap.as_ref().expect("cmap");
let format14 = cmap
.cmap_encodings
.iter()
.find_map(|encoding| match encoding.cmap_subtable.as_ref() {
CmapSubtable::Format14(format14) => Some(format14),
_ => None,
})
.expect("expected format 14 cmap");
let var_selector_record = format14
.var_selector_records
.first()
.expect("expected at least one var selector record");
let mapping = var_selector_record
.non_default_uvs
.unicode_value_ranges
.first()
.expect("expected at least one UVS mapping");
let text = format!(
"{}{}",
char::from_u32(mapping.unicode_value).expect("unicode scalar"),
char::from_u32(var_selector_record.var_selector).expect("variation selector")
);
(text, mapping.glyph_id as usize)
}
#[test]
fn fontload_from_file_works() {
let path = sample_font_path();
let font = crate::fontload_file(&path).expect("load from file");
assert!(font.font().get_font_count() >= 1);
assert!(!font.font().get_info().is_err());
}
#[test]
fn fontload_from_buffer_works() {
let path = sample_font_path();
let bytes = std::fs::read(&path).expect("read font bytes");
let font = crate::fontload_buffer(&bytes).expect("load from buffer");
assert!(font.font().get_font_count() >= 1);
}
#[test]
fn load_font_from_buffer_alias_works() {
let path = sample_font_path();
let bytes = std::fs::read(&path).expect("read font bytes");
let font = crate::load_font_from_buffer(&bytes).expect("load from buffer alias");
assert!(font.font().get_font_count() >= 1);
}
#[test]
fn load_font_from_source_buffer_works() {
let path = sample_font_path();
let bytes = std::fs::read(&path).expect("read font bytes");
let font = crate::load_font(crate::FontSource::Buffer(&bytes)).expect("load source buffer");
assert!(font.font().get_font_count() >= 1);
}
#[test]
fn fontload_from_collection_file_works() {
let font = crate::fontload_file(collection_font_path()).expect("load font collection");
assert!(font.font().get_font_count() > 1);
}
#[test]
fn truetype_consecutive_off_curve_points_stay_quadratic() {
let parsed = ParsedGlyph {
number_of_contours: 1,
x_min: 0,
y_min: 0,
x_max: 30,
y_max: 10,
offset: 0,
length: 0,
end_pts_of_contours: vec![3],
instructions: Vec::new(),
flags: vec![0, 0, 0, 0],
xs: vec![0, 10, 10, 10],
ys: vec![0, 10, 0, -10],
on_curves: vec![true, false, false, true],
};
let commands = crate::opentype::outline::glyf::Glyph::to_path_commands_parsed(
&parsed,
&crate::fontreader::FontLayout::Unknown,
0.0,
0.0,
);
assert!(matches!(commands[0], crate::PathCommand::MoveTo { .. }));
assert!(matches!(commands[1], crate::PathCommand::QuadTo { .. }));
assert!(matches!(commands[2], crate::PathCommand::QuadTo { .. }));
assert!(matches!(commands[3], crate::PathCommand::ClosePath));
let svg = crate::opentype::outline::glyf::Glyph::get_svg_path_parsed(
&parsed,
&crate::fontreader::FontLayout::Unknown,
0.0,
0.0,
);
assert_eq!(svg.matches('Q').count(), 2);
assert!(!svg.contains('T'));
}
#[test]
fn fontload_from_collection_buffer_works() {
let bytes = std::fs::read(collection_font_path()).expect("read collection bytes");
let font = crate::fontload_buffer(&bytes).expect("load font collection from buffer");
assert!(font.font().get_font_count() > 1);
}
#[test]
fn font_family_add_loaded_font_expands_collection_faces() {
let path = yu_gothic_regular_collection_path();
if !path.exists() {
return;
}
let font = crate::load_font_from_file(&path).expect("load Yu Gothic TTC");
let face_count = font.font().get_font_count();
assert!(face_count > 1, "expected TTC with multiple faces");
let mut family = crate::FontFamily::new("Yu Gothic");
family.add_loaded_font(font);
assert_eq!(family.cached_faces_len(), face_count);
}
#[test]
fn font_family_resolves_yu_gothic_face_from_collection() {
let path = yu_gothic_regular_collection_path();
if !path.exists() {
return;
}
let font = crate::load_font_from_file(&path).expect("load Yu Gothic TTC");
let mut family = crate::FontFamily::new("Yu Gothic");
family.add_loaded_font(font);
let descriptor = family
.resolve_descriptor(
Some("Yu Gothic"),
None,
crate::FontWeight::NORMAL,
crate::FontStyle::Normal,
crate::FontStretch::default(),
)
.expect("resolve Yu Gothic face");
assert_eq!(descriptor.family_name, "Yu Gothic");
}
#[test]
#[cfg(feature = "layout")]
fn yu_gothic_does_not_replace_masu_with_square_masu_by_default() {
let Some(font) = yu_gothic_font() else {
return;
};
let glyph_ids = font
.font()
.debug_shape_glyph_ids("ます", Some("ja-JP"))
.expect("shape Yu Gothic glyph ids");
assert_eq!(
glyph_ids.len(),
2,
"default shaping should keep ます as two glyphs"
);
let cmap = font.font().cmap.as_ref().expect("Yu Gothic cmap");
let square_masu = cmap.get_glyph_position('〼' as u32) as usize;
assert_ne!(
glyph_ids[0], square_masu,
"must not substitute to 〼 by default"
);
}
#[test]
#[cfg(feature = "layout")]
fn yu_gothic_masu_ligature_is_discretionary_not_default() {
let Some(font) = yu_gothic_font() else {
return;
};
let Some(gsub) = font.font().gsub.as_ref() else {
return;
};
let cmap = font.font().cmap.as_ref().expect("Yu Gothic cmap");
let glyph_ids = vec![
cmap.get_glyph_position('ま' as u32) as usize,
cmap.get_glyph_position('す' as u32) as usize,
];
let square_masu = cmap.get_glyph_position('〼' as u32) as usize;
let default_liga = gsub.lookup_liga_sequence(&glyph_ids);
assert_ne!(default_liga, Some(square_masu));
let dlig = gsub.lookup_discretionary_liga_sequence(&glyph_ids);
if let Some(discretionary) = dlig {
assert_eq!(discretionary, square_masu);
}
}
#[test]
fn fontload_from_woff2_file_works() {
let path = woff2_font_path();
let font = crate::fontload_file(&path).expect("load woff2 from file");
let svg = font.text2svg("A", 24.0, "px").expect("render woff2 text");
assert!(svg.starts_with("<svg"));
}
#[test]
fn fira_sans_post_table_to_string_does_not_panic() {
let path = fira_sans_regular_path();
let font = crate::load_font_from_file(&path).expect("load Fira Sans");
let post = font.font().post.as_ref().expect("Fira Sans post table");
let dump = post.to_string();
assert!(dump.starts_with("post\n"));
assert!(dump.contains("Version"));
}
#[test]
fn fira_sans_svg_viewbox_keeps_padding_for_o() {
let path = fira_sans_black_path();
let font = crate::load_font_from_file(&path).expect("load Fira Sans Black");
let run = font
.text2glyph_run("O", crate::FontOptions::new(&font).with_font_size(64.0))
.expect("shape O");
let svg = font.text2svg("O", 64.0, "px").expect("svg O");
let view_box = svg
.split("viewBox=\"")
.nth(1)
.and_then(|rest| rest.split('"').next())
.expect("viewBox");
let values: Vec<f32> = view_box
.split_whitespace()
.map(|value| value.parse::<f32>().expect("numeric viewBox component"))
.collect();
assert_eq!(values.len(), 4);
let glyph = &run.glyphs[0];
let bounds = glyph.glyph.metrics.bounds.expect("glyph bounds");
let min_x = bounds.min_x + glyph.x;
let min_y = bounds.min_y + glyph.y;
let width = bounds.max_x - bounds.min_x;
let height = bounds.max_y - bounds.min_y;
assert!(values[0] < min_x, "viewBox should add left/right padding");
assert!(values[1] < min_y, "viewBox should add top/bottom padding");
assert!(
values[2] > width,
"viewBox width should exceed glyph bounds"
);
assert!(
values[3] > height,
"viewBox height should exceed glyph bounds"
);
}
#[test]
fn fontload_from_woff2_buffer_works() {
let path = woff2_font_path();
let bytes = std::fs::read(&path).expect("read woff2 bytes");
let font = crate::fontload_buffer(&bytes).expect("load woff2 from buffer");
let svg = font.text2svg("A", 24.0, "px").expect("render woff2 text");
assert!(svg.contains("<svg"));
}
#[test]
fn chunked_font_buffer_reports_missing_ranges() {
let mut buffer = crate::ChunkedFontBuffer::new(10).expect("create chunked buffer");
assert_eq!(buffer.missing_ranges(), vec![(0, 10)]);
buffer.append(2, &[1, 2, 3]).expect("append middle chunk");
assert_eq!(buffer.filled_len(), 3);
assert_eq!(buffer.missing_ranges(), vec![(0, 2), (5, 10)]);
buffer.append(0, &[9, 8]).expect("append front chunk");
buffer
.append(5, &[7, 6, 5, 4, 3])
.expect("append tail chunk");
assert!(buffer.is_complete());
assert!(buffer.missing_ranges().is_empty());
}
#[test]
fn chunked_font_buffer_reassembles_woff2_out_of_order() {
let bytes = std::fs::read(woff2_font_path()).expect("read woff2 bytes");
let mut buffer =
crate::ChunkedFontBuffer::new(bytes.len()).expect("create chunked font buffer");
let chunk_size = (bytes.len() / 5).max(1);
let mut chunks = Vec::new();
let mut offset = 0usize;
while offset < bytes.len() {
let end = (offset + chunk_size).min(bytes.len());
chunks.push((offset, bytes[offset..end].to_vec()));
offset = end;
}
for (offset, chunk) in chunks.into_iter().rev() {
buffer.append(offset, &chunk).expect("append chunk");
}
assert!(buffer.is_complete());
let font = buffer.into_loaded_font().expect("load reconstructed woff2");
let svg = font
.text2svg("A", 24.0, "px")
.expect("render reconstructed woff2");
assert!(svg.contains("<svg"));
}
#[test]
fn chunked_font_buffer_rejects_incomplete_decode() {
let bytes = std::fs::read(woff2_font_path()).expect("read woff2 bytes");
let mut buffer =
crate::ChunkedFontBuffer::new(bytes.len()).expect("create chunked font buffer");
let halfway = bytes.len() / 2;
buffer
.append(0, &bytes[..halfway])
.expect("append partial bytes");
match buffer.load_font() {
Ok(_) => panic!("incomplete buffer should not decode"),
Err(err) => assert_eq!(err.kind(), std::io::ErrorKind::WouldBlock),
}
}
#[test]
fn chunked_font_buffer_rejects_conflicting_overlaps() {
let mut buffer = crate::ChunkedFontBuffer::new(8).expect("create chunked buffer");
buffer.append(2, &[1, 2, 3]).expect("append initial bytes");
let err = buffer
.append(3, &[9, 3])
.expect_err("overlapping conflicting bytes should fail");
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
}
#[test]
fn font_family_selects_best_cached_face() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let black =
crate::load_font_from_file(fira_sans_black_path()).expect("load black fira sans");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_face(
crate::FontFaceDescriptor::new("Fira Sans")
.with_font_name("Fira Sans Regular")
.with_font_weight(crate::FontWeight::NORMAL),
regular,
);
family.add_face(
crate::FontFaceDescriptor::new("Fira Sans")
.with_font_name("Fira Sans Black")
.with_font_weight(crate::FontWeight::BLACK),
black,
);
let descriptor = family
.resolve_descriptor(
Some("Fira Sans"),
None,
crate::FontWeight::BLACK,
crate::FontStyle::Normal,
crate::FontStretch::NORMAL,
)
.expect("resolve cached face");
assert_eq!(descriptor.font_name.as_deref(), Some("Fira Sans Black"));
let run = crate::text2commands(
"A",
crate::FontOptions::from_family(&family)
.with_font_family("Fira Sans")
.with_font_weight(crate::FontWeight::BLACK)
.with_font_size(24.0),
)
.expect("render from family");
assert_eq!(run.glyphs.len(), 1);
let run = family
.text2commands(
"A",
family
.options()
.with_font_weight(crate::FontWeight::BLACK)
.with_font_size(24.0),
)
.expect("render from family method");
assert_eq!(run.glyphs.len(), 1);
let svg = family.text2svg("A", 24.0, "px").expect("svg from family");
assert!(svg.starts_with("<svg"));
let width = family.measure("A").expect("measure from family");
assert!(width > 0.0);
}
#[test]
fn font_family_promotes_chunked_face_into_cache() {
let bytes = std::fs::read(woff2_font_path()).expect("read woff2 bytes");
let mut family = crate::FontFamily::new("Noto Sans");
family
.begin_chunked_face(
"noto-regular",
crate::FontFaceDescriptor::new("Noto Sans")
.with_font_name("Noto Sans WOFF2")
.with_font_weight(crate::FontWeight::NORMAL),
bytes.len(),
)
.expect("begin chunked face");
let split = bytes.len() / 3;
family
.append_chunk("noto-regular", split, &bytes[split..split * 2])
.expect("append middle chunk");
assert!(!family
.missing_ranges("noto-regular")
.expect("missing ranges")
.is_empty());
family
.append_chunk("noto-regular", 0, &bytes[..split])
.expect("append first chunk");
family
.append_chunk("noto-regular", split * 2, &bytes[split * 2..])
.expect("append tail chunk");
let font = family
.finalize_chunked_face("noto-regular")
.expect("finalize chunked face");
let width = font.measure("A").expect("measure finalized font");
assert!(width > 0.0);
assert_eq!(family.pending_faces_len(), 0);
assert_eq!(family.cached_faces_len(), 1);
}
#[test]
fn font_family_falls_back_to_cached_face_per_glyph() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let emoji = crate::load_font_from_file(segoe_emoji_font_path()).expect("load segoe emoji");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(emoji);
let run = crate::text2commands(
"A🥺B",
crate::FontOptions::from_family(&family)
.with_font_family("Fira Sans")
.with_font_size(32.0),
)
.expect("render mixed fallback text");
assert_eq!(run.glyphs.len(), 3);
assert!(matches!(
run.glyphs[1].glyph.layers.first(),
Some(crate::GlyphLayer::Path(layer))
if !layer.commands.is_empty()
&& matches!(&layer.paint, crate::GlyphPaint::Solid(_))
));
}
#[test]
fn font_family_text2svg_uses_fallback_layers() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let emoji = crate::load_font_from_file(segoe_emoji_font_path()).expect("load segoe emoji");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(emoji);
let svg = family
.text2svg("A🥺B", 32.0, "px")
.expect("render mixed fallback svg");
assert!(svg.starts_with("<svg"));
assert!(svg.contains("fill=\"#"));
}
#[test]
fn font_family_prefers_primary_font_for_plain_digits_over_sbix_fallback() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let sbix = crate::load_font_from_file(twemoji_sbix_font_path()).expect("load twemoji sbix");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(sbix);
let run = crate::text2commands(
"123",
crate::FontOptions::from_family(&family)
.with_font_family("Fira Sans")
.with_font_size(32.0),
)
.expect("render plain digits with fallback family");
assert_eq!(run.glyphs.len(), 3);
for glyph in &run.glyphs {
match glyph.glyph.layers.first() {
Some(crate::GlyphLayer::Path(layer)) => assert!(!layer.commands.is_empty()),
Some(crate::GlyphLayer::Raster(_)) => {
panic!("plain digits should not fall back to sbix raster glyphs")
}
#[cfg(feature = "svg-fonts")]
Some(crate::GlyphLayer::Svg(_)) => {
panic!("plain digits should not fall back to SVG glyph layers")
}
None => panic!("expected digit layer"),
}
}
}
#[test]
#[cfg(feature = "svg-fonts")]
fn font_family_prefers_outline_face_for_plain_digits_over_svg_fallback_even_if_svg_face_is_first() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let bytes = std::fs::read(noto_color_emoji_font_path()).expect("read noto color emoji");
let svg_emoji = crate::load_font_from_buffer(&bytes).expect("load noto color emoji");
let mut family = crate::FontFamily::new("Mixed");
family.add_loaded_font(svg_emoji);
family.add_loaded_font(regular);
let options = crate::FontOptions::from_family(&family).with_font_size(32.0);
let face_indices = family
.debug_face_indices_for_text("123", options.clone())
.expect("resolve face indices");
assert_eq!(face_indices, vec![1, 1, 1]);
let run = crate::text2commands("123", options).expect("render plain digits with svg fallback family");
assert_eq!(run.glyphs.len(), 3);
for glyph in &run.glyphs {
match glyph.glyph.layers.first() {
Some(crate::GlyphLayer::Path(layer)) => assert!(!layer.commands.is_empty()),
Some(crate::GlyphLayer::Raster(_)) => {
panic!("plain digits should not fall back to raster glyph layers")
}
#[cfg(feature = "svg-fonts")]
Some(crate::GlyphLayer::Svg(_)) => {
panic!("plain digits should not fall back to SVG glyph layers")
}
None => panic!("expected digit layer"),
}
}
}
#[test]
#[cfg(feature = "svg-fonts")]
fn font_family_prefers_svg_face_for_keycap_cluster_even_if_outline_face_is_available() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let bytes = std::fs::read(noto_color_emoji_font_path()).expect("read noto color emoji");
let svg_emoji = crate::load_font_from_buffer(&bytes).expect("load noto color emoji");
let mut family = crate::FontFamily::new("Mixed");
family.add_loaded_font(svg_emoji);
family.add_loaded_font(regular);
let options = crate::FontOptions::from_family(&family).with_font_size(32.0);
let face_indices = family
.debug_face_indices_for_text("1️⃣", options.clone())
.expect("resolve keycap face indices");
assert_eq!(face_indices, vec![0]);
let run = crate::text2commands("1️⃣", options).expect("render keycap with svg fallback family");
assert_eq!(run.glyphs.len(), 1);
assert!(
run.glyphs[0].glyph.layers.iter().any(|layer| {
matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty())
|| matches!(layer, crate::GlyphLayer::Svg(svg) if !svg.document.is_empty())
}),
"expected keycap cluster to use svg emoji face"
);
}
#[test]
fn sbix_font_prefers_outline_for_plain_digit() {
let font = crate::load_font_from_file(twemoji_sbix_font_path()).expect("load twemoji sbix");
let plain_digit = font.font().text2command("1").expect("render plain digit");
assert_eq!(plain_digit.len(), 1);
assert!(plain_digit[0].bitmap.is_none());
assert!(!plain_digit[0].commands.is_empty());
}
#[test]
fn fontload_from_woff_file_works() {
let path = woff_font_path();
let font = crate::fontload_file(&path).expect("load woff font");
let svg = font.text2svg("A", 24.0, "px").expect("render woff text");
assert!(svg.contains("<svg"));
}
#[test]
#[cfg(feature = "cff")]
fn cff_cid_font_renders_svg() {
let font = crate::fontload_file(cff_font_path()).expect("load cff font");
let svg = font
.font()
.get_svg('漢', 24.0, "px")
.expect("render cff text");
assert!(svg.contains("<svg"));
assert!(svg.contains("<path"));
}
#[test]
fn fontload_from_source_file_works() {
let path = sample_font_path();
let font = crate::fontload(crate::FontSource::File(path.as_path())).expect("load source");
assert!(font.font().get_font_count() >= 1);
}
#[test]
fn text_to_command_and_svg_and_measure_work() {
let path = sample_font_path();
let font = crate::fontload_file(&path).expect("load font");
let commands = font.font().text2command("A").expect("text2command");
assert_eq!(commands.len(), 1);
assert!(commands[0].advance_width > 0.0);
assert!(!commands[0].commands.is_empty());
let svg = font.text2svg("A", 24.0, "px").expect("text2svg");
assert!(svg.starts_with("<svg"));
assert!(svg.contains("<path"));
let width = font.measure("A").expect("measure");
assert!(width > 0.0);
let two_line_width = font.measure("A\nB").expect("measure multiline");
assert!(two_line_width >= width);
}
#[test]
#[cfg(debug_assertions)]
fn raw_table_dump_smoke_across_fixture_corpus() {
let paths = fixture_engine_corpus_paths();
let (dumped_faces, failures) = run_raw_dump_smoke_for_paths(&paths);
assert!(
failures.is_empty(),
"raw table dump smoke failures:\n{}",
failures.join("\n")
);
assert!(
dumped_faces >= 32,
"expected to dump at least 32 faces, dumped {dumped_faces}"
);
}
#[test]
fn public_api_metadata_smoke_across_fixture_corpus() {
let paths = fixture_font_corpus_paths();
let mut checked_files = 0usize;
let mut checked_faces = 0usize;
let mut skipped = 0usize;
let mut failures = Vec::new();
for path in &paths {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let file = crate::FontFile::from_file(path)
.map_err(|err| format!("FontFile::from_file failed: {err}"))?;
if file.face_count() == 0 {
return Err("face_count was zero".to_string());
}
if !file.dump().contains("FontFile") {
return Err("dump() did not include FontFile header".to_string());
}
let faces = file
.faces()
.map_err(|err| format!("faces() failed: {err}"))?;
if faces.len() != file.face_count() {
return Err(format!(
"faces().len()={} did not match face_count()={}",
faces.len(),
file.face_count()
));
}
for face in &faces {
let family = face.family();
let full_name = face.full_name();
if family.trim().is_empty() && full_name.trim().is_empty() {
return Err("family() and full_name() were both empty".to_string());
}
if !face.dump().contains("FontFace") {
return Err("face.dump() did not include FontFace header".to_string());
}
}
Ok::<usize, String>(faces.len())
}));
match outcome {
Ok(Ok(face_count)) => {
checked_files += 1;
checked_faces += face_count;
}
Ok(Err(err)) if should_skip_corpus_error(path, &err) => skipped += 1,
Ok(Err(err)) => failures.push(format!("{}: {}", path.display(), err)),
Err(_) => failures.push(format!("{}: panic", path.display())),
}
}
assert!(
failures.is_empty(),
"public API metadata smoke failures:\n{}",
failures.join("\n")
);
assert_eq!(
checked_files + skipped,
paths.len(),
"expected every fixture font to either load or be skipped explicitly"
);
assert!(
checked_files >= 700,
"expected to load at least 700 fixture fonts, loaded {checked_files}"
);
assert!(
checked_faces >= checked_files,
"expected at least one face per loaded font file"
);
}
#[test]
fn public_api_engine_smoke_across_fixture_corpus() {
let paths = fixture_engine_corpus_paths();
let (shaped, svg_successes, skipped, failures) = run_public_api_smoke_for_paths(&paths);
assert!(
failures.is_empty(),
"public API engine smoke failures:\n{}",
failures.join("\n")
);
assert!(
shaped >= 96,
"expected to shape at least 96 fixture fonts, shaped {shaped}"
);
assert!(
svg_successes >= 64,
"expected SVG export to succeed for at least 64 fixture fonts, succeeded {svg_successes}"
);
assert!(
skipped <= 64,
"expected only a limited number of corpus fonts to be skipped, skipped {skipped}"
);
}
#[test]
#[cfg(feature = "cff")]
fn public_api_cff_smoke_across_otf_fixture_corpus() {
let paths = existing_paths(
fixture_font_corpus_paths()
.into_iter()
.filter(|path| {
path.extension()
.and_then(|ext| ext.to_str())
.map(|ext| ext.eq_ignore_ascii_case("otf"))
.unwrap_or(false)
})
.collect(),
);
let (shaped, svg_successes, skipped, failures) = run_public_api_smoke_for_paths(&paths);
assert!(
failures.is_empty(),
"public API CFF smoke failures:\n{}",
failures.join("\n")
);
assert!(
shaped >= 12,
"expected to shape at least 12 OTF fixtures, shaped {shaped}"
);
assert!(
svg_successes >= 10,
"expected SVG export for at least 10 OTF fixtures, succeeded {svg_successes}"
);
assert!(
skipped <= 4,
"expected at most 4 OTF fixtures to be skipped"
);
}
#[test]
#[cfg(feature = "cff")]
fn public_api_cff2_smoke_across_real_fixtures() {
let paths = cff2_fixture_paths();
if paths.is_empty() {
return;
}
let (shaped, svg_successes, skipped, failures) = run_public_api_smoke_for_paths(&paths);
assert!(
failures.is_empty(),
"public API CFF2 smoke failures:\n{}",
failures.join("\n")
);
assert!(
shaped >= 1,
"expected to shape at least one CFF2 fixture, shaped {shaped}"
);
assert!(
svg_successes >= 1,
"expected SVG export for at least one CFF2 fixture, succeeded {svg_successes}"
);
assert!(
skipped <= 1,
"expected at most one CFF2 fixture to be skipped"
);
}
#[test]
fn source_serif_variable_fonts_are_not_cff2_fixtures() {
let paths = source_serif_variable_paths();
assert!(
!paths.is_empty(),
"expected Source Serif variable fixtures to exist"
);
for path in paths {
assert!(
!font_file_has_sfnt_table(&path, b"CFF2"),
"{} should not be classified as CFF2",
path.display()
);
assert!(
font_file_has_sfnt_table(&path, b"glyf"),
"{} should expose a glyf table",
path.display()
);
assert!(
font_file_has_sfnt_table(&path, b"gvar"),
"{} should expose a gvar table",
path.display()
);
}
}
#[test]
fn public_api_metadata_smoke_across_variable_font_fixtures() {
let paths = variable_font_fixture_paths();
let mut loaded = 0usize;
let mut skipped = 0usize;
let mut failures = Vec::new();
for path in &paths {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let file = crate::FontFile::from_file(path)
.map_err(|err| format!("FontFile::from_file failed: {err}"))?;
if file.face_count() == 0 {
return Err("face_count was zero".to_string());
}
Ok::<(), String>(())
}));
match outcome {
Ok(Ok(())) => loaded += 1,
Ok(Err(err)) if should_skip_corpus_error(path, &err) => skipped += 1,
Ok(Err(err)) => failures.push(format!("{}: {}", path.display(), err)),
Err(_) => failures.push(format!("{}: panic", path.display())),
}
}
assert!(
failures.is_empty(),
"variable font metadata smoke failures:\n{}",
failures.join("\n")
);
assert_eq!(loaded + skipped, paths.len());
assert!(
loaded >= 8,
"expected to load at least 8 variable font fixtures, loaded {loaded}"
);
}
#[test]
fn public_api_engine_smoke_across_variable_font_fixtures() {
let paths = variable_font_fixture_paths();
let (shaped, svg_successes, skipped, failures) = run_public_api_smoke_for_paths(&paths);
assert!(
failures.is_empty(),
"variable font engine smoke failures:\n{}",
failures.join("\n")
);
assert!(
shaped >= 8,
"expected to shape at least 8 variable font fixtures, shaped {shaped}"
);
assert!(
svg_successes >= 6,
"expected SVG export for at least 6 variable font fixtures, succeeded {svg_successes}"
);
assert!(
skipped <= 8,
"expected at most 8 variable font fixtures to be skipped"
);
}
#[test]
fn public_api_exposes_variable_font_axes_for_real_fixtures() {
let paths = variable_font_fixture_paths();
let mut variable_faces = 0usize;
let mut named_axes = 0usize;
let mut failures = Vec::new();
for path in &paths {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let face = crate::FontFile::from_file(path)
.map_err(|err| format!("FontFile::from_file failed: {err}"))?
.current_face()
.map_err(|err| format!("current_face failed: {err}"))?;
let axes = face.variation_axes();
if !face.is_variable() {
return Err("face.is_variable() returned false".to_string());
}
if axes.is_empty() {
return Err("variation_axes() was empty".to_string());
}
if axes.iter().any(|axis| axis.tag.len() != 4) {
return Err("variation axis tag was not 4 bytes".to_string());
}
let named_axes = axes
.iter()
.filter(|axis| !axis.name.as_deref().unwrap_or("").trim().is_empty())
.count();
Ok::<(usize, usize), String>((1, named_axes))
}));
match outcome {
Ok(Ok((faces, axis_names))) => {
variable_faces += faces;
named_axes += axis_names;
}
Ok(Err(err)) => failures.push(format!("{}: {}", path.display(), err)),
Err(_) => failures.push(format!("{}: panic", path.display())),
}
}
assert!(
failures.is_empty(),
"variable axis exposure failures:\n{}",
failures.join("\n")
);
assert!(
variable_faces >= 8,
"expected at least 8 variable faces, found {variable_faces}"
);
assert!(
named_axes >= 4,
"expected some named variable axes, found {named_axes}"
);
}
#[test]
#[cfg(feature = "layout")]
fn raw_vhea_dump_returns_placeholder_when_table_is_missing() {
let path = test_fonts_dir()
.join("source")
.join("SourceSerif4-Italic-VariableFont_opsz,wght.ttf");
if !path.exists() {
return;
}
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut font = crate::Font::get_font_from_file(&path)
.map_err(|err| format!("get_font_from_file failed: {err}"))?;
font.set_font(0)
.map_err(|err| format!("set_font failed: {err}"))?;
Ok::<String, String>(font.get_vhea_raw())
}));
match outcome {
Ok(Ok(vhea)) => assert_eq!(vhea, "vhea is none"),
Ok(Err(err)) => panic!("failed to dump vhea table: {err}"),
Err(_) => panic!("get_vhea_raw panicked for {}", path.display()),
}
}
#[test]
fn raw_color_dump_returns_placeholders_when_tables_are_missing() {
let path = test_fonts_dir()
.join("source")
.join("SourceSerif4-Italic-VariableFont_opsz,wght.ttf");
if !path.exists() {
return;
}
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut font = crate::Font::get_font_from_file(&path)
.map_err(|err| format!("get_font_from_file failed: {err}"))?;
font.set_font(0)
.map_err(|err| format!("set_font failed: {err}"))?;
Ok::<(String, String), String>((font.get_colr_raw(), font.get_cpal_raw()))
}));
match outcome {
Ok(Ok((colr, cpal))) => {
assert_eq!(colr, "colr is none");
assert_eq!(cpal, "cpal is none");
}
Ok(Err(err)) => panic!("failed to dump color tables: {err}"),
Err(_) => panic!("color raw dump panicked for {}", path.display()),
}
}
#[test]
fn variable_width_axis_changes_measure_on_real_fonts() {
let paths = variable_font_fixture_paths();
let mut exercised = 0usize;
let mut failures = Vec::new();
for path in &paths {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let face = crate::FontFile::from_file(path)
.map_err(|err| format!("FontFile::from_file failed: {err}"))?
.current_face()
.map_err(|err| format!("current_face failed: {err}"))?;
let Some(axis) = face
.variation_axes()
.into_iter()
.find(|axis| axis.tag == "wdth")
else {
return Ok::<bool, String>(false);
};
if (axis.max_value - axis.min_value).abs() < 0.1 {
return Err("wdth axis had no measurable range".to_string());
}
let engine = face.engine().with_font_size(32.0);
let narrow = engine
.clone()
.with_variation("wdth", axis.min_value)
.measure("Hello")
.map_err(|err| format!("measure narrow failed: {err}"))?;
let wide = engine
.clone()
.with_variation("wdth", axis.max_value)
.measure("Hello")
.map_err(|err| format!("measure wide failed: {err}"))?;
if (wide - narrow).abs() <= 0.5 {
return Err(format!(
"wdth axis did not change measure enough: narrow={narrow} wide={wide}"
));
}
Ok(true)
}));
match outcome {
Ok(Ok(true)) => exercised += 1,
Ok(Ok(false)) => {}
Ok(Err(err)) => failures.push(format!("{}: {}", path.display(), err)),
Err(_) => failures.push(format!("{}: panic", path.display())),
}
}
assert!(
failures.is_empty(),
"variable width axis failures:\n{}",
failures.join("\n")
);
assert!(
exercised >= 3,
"expected to exercise at least 3 wdth variable fonts, exercised {exercised}"
);
}
#[test]
fn variable_axes_change_outline_signature_on_real_fonts() {
fn outline_signature(run: &crate::GlyphRun) -> Option<String> {
let mut signature = String::new();
let mut saw_path = false;
for glyph in &run.glyphs {
let Some(bounds) = glyph.glyph.metrics.bounds else {
continue;
};
for layer in &glyph.glyph.layers {
let crate::GlyphLayer::Path(path) = layer else {
continue;
};
saw_path = true;
signature.push_str(&format!(
"B{:.1},{:.1},{:.1},{:.1}|",
bounds.min_x, bounds.min_y, bounds.max_x, bounds.max_y
));
for command in &path.commands {
match command {
crate::Command::MoveTo(x, y) => {
signature.push_str(&format!("M{:.1},{:.1};", x, y));
}
crate::Command::Line(x, y) => {
signature.push_str(&format!("L{:.1},{:.1};", x, y));
}
crate::Command::Bezier((cx, cy), (x, y)) => {
signature
.push_str(&format!("Q{:.1},{:.1},{:.1},{:.1};", cx, cy, x, y));
}
crate::Command::CubicBezier((cx1, cy1), (cx2, cy2), (x, y)) => {
signature.push_str(&format!(
"C{:.1},{:.1},{:.1},{:.1},{:.1},{:.1};",
cx1, cy1, cx2, cy2, x, y
));
}
crate::Command::Close => signature.push_str("Z;"),
}
}
}
}
if saw_path {
Some(signature)
} else {
None
}
}
let paths = variable_font_fixture_paths();
let mut exercised = 0usize;
let mut failures = Vec::new();
for path in &paths {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let face = crate::FontFile::from_file(path)
.map_err(|err| format!("FontFile::from_file failed: {err}"))?
.current_face()
.map_err(|err| format!("current_face failed: {err}"))?;
let Some(axis) = face.variation_axes().into_iter().find(|axis| {
matches!(axis.tag.as_str(), "wdth" | "wght")
&& (axis.max_value - axis.min_value).abs() >= 0.1
}) else {
return Ok::<bool, String>(false);
};
let Some((sample, locale, policy)) = public_api_smoke_sample(&face) else {
return Ok(false);
};
let mut engine = face
.engine()
.with_font_size(72.0)
.with_shaping_policy(policy);
if let Some(locale) = locale {
engine = engine.with_locale(locale);
}
let low = engine
.clone()
.with_variation(&axis.tag, axis.min_value)
.shape(&sample)
.map_err(|err| format!("low variation shape failed: {err}"))?;
let high = engine
.clone()
.with_variation(&axis.tag, axis.max_value)
.shape(&sample)
.map_err(|err| format!("high variation shape failed: {err}"))?;
let low_signature = outline_signature(&low)
.ok_or_else(|| "missing outline at low value".to_string())?;
let high_signature = outline_signature(&high)
.ok_or_else(|| "missing outline at high value".to_string())?;
if low_signature == high_signature {
return Err(format!(
"{} axis did not change outline signature for sample {:?}",
axis.tag, sample
));
}
Ok(true)
}));
match outcome {
Ok(Ok(true)) => exercised += 1,
Ok(Ok(false)) => {}
Ok(Err(err)) => failures.push(format!("{}: {}", path.display(), err)),
Err(_) => failures.push(format!("{}: panic", path.display())),
}
}
assert!(
failures.is_empty(),
"variable outline signature failures:\n{}",
failures.join("\n")
);
assert!(
exercised >= 6,
"expected to exercise at least 6 variable fonts with outline changes, exercised {exercised}"
);
}
#[test]
fn source_serif_composite_gvar_changes_outline_signature() {
fn outline_signature(run: &crate::GlyphRun) -> Option<String> {
let mut signature = String::new();
let mut saw_path = false;
for glyph in &run.glyphs {
for layer in &glyph.glyph.layers {
let crate::GlyphLayer::Path(path) = layer else {
continue;
};
saw_path = true;
for command in &path.commands {
match command {
crate::Command::MoveTo(x, y) => {
signature.push_str(&format!("M{:.1},{:.1};", x, y));
}
crate::Command::Line(x, y) => {
signature.push_str(&format!("L{:.1},{:.1};", x, y));
}
crate::Command::Bezier((cx, cy), (x, y)) => {
signature
.push_str(&format!("Q{:.1},{:.1},{:.1},{:.1};", cx, cy, x, y));
}
crate::Command::CubicBezier((cx1, cy1), (cx2, cy2), (x, y)) => {
signature.push_str(&format!(
"C{:.1},{:.1},{:.1},{:.1},{:.1},{:.1};",
cx1, cy1, cx2, cy2, x, y
));
}
crate::Command::Close => signature.push_str("Z;"),
}
}
}
}
if saw_path {
Some(signature)
} else {
None
}
}
let path = test_fonts_dir()
.join("source")
.join("SourceSerif4-VariableFont_opsz,wght.ttf");
if !path.exists() {
return;
}
let mut raw_font = crate::Font::get_font_from_file(&path).expect("load Source Serif");
raw_font.set_font(0).expect("select Source Serif face");
let cmap = raw_font.cmap.as_ref().expect("Source Serif cmap");
let glyph_id = cmap.get_glyph_position('Á' as u32) as usize;
assert!(glyph_id > 0, "expected Source Serif to resolve Á");
let glyf = raw_font.glyf.as_ref().expect("Source Serif glyf");
let source_glyph = glyf.get_glyph(glyph_id).expect("Source Serif glyph");
assert!(
source_glyph.parse().number_of_contours < 0,
"expected Á to be a composite glyph in Source Serif"
);
let flattened = glyf
.parse_glyph(glyph_id)
.expect("flatten Source Serif composite glyph");
assert!(
flattened.number_of_contours > 0,
"expected flattened composite glyph to expose contours"
);
let face = crate::FontFile::from_file(&path)
.expect("load Source Serif as FontFile")
.current_face()
.expect("load Source Serif face");
let axis = face
.variation_axes()
.into_iter()
.find(|axis| axis.tag == "wght")
.expect("Source Serif wght axis");
let engine = face.engine().with_font_size(72.0);
let low = engine
.clone()
.with_variation("wght", axis.min_value)
.shape("Á")
.expect("shape low-weight composite glyph");
let high = engine
.clone()
.with_variation("wght", axis.max_value)
.shape("Á")
.expect("shape high-weight composite glyph");
let low_signature = outline_signature(&low).expect("low-weight outline signature");
let high_signature = outline_signature(&high).expect("high-weight outline signature");
assert_ne!(
low_signature, high_signature,
"expected Source Serif composite glyph outline to vary across weight axis"
);
}
#[test]
fn source_serif_variable_metrics_change_glyph_advance_and_bearing() {
let path = test_fonts_dir()
.join("source")
.join("SourceSerif4-VariableFont_opsz,wght.ttf");
if !path.exists() {
return;
}
assert!(
font_file_has_sfnt_table(&path, b"gvar"),
"{} should expose gvar",
path.display()
);
let face = crate::FontFile::from_file(&path)
.expect("load Source Serif as FontFile")
.current_face()
.expect("load Source Serif face");
let axis = face
.variation_axes()
.into_iter()
.find(|axis| axis.tag == "wght")
.expect("Source Serif wght axis");
let engine = face.engine().with_font_size(72.0);
let low = engine
.clone()
.with_variation("wght", axis.min_value)
.shape("A")
.expect("shape low-weight A");
let high = engine
.clone()
.with_variation("wght", axis.max_value)
.shape("A")
.expect("shape high-weight A");
let low_metrics = low.glyphs.first().expect("low-weight glyph").glyph.metrics;
let high_metrics = high
.glyphs
.first()
.expect("high-weight glyph")
.glyph
.metrics;
let low_advance = low_metrics.advance_x;
let high_advance = high_metrics.advance_x;
let low_lsb = low_metrics.bearing_x;
let high_lsb = high_metrics.bearing_x;
assert!(
low_advance != high_advance || low_lsb != high_lsb,
"expected variable metrics to change horizontal glyph metrics: low=({low_advance}, {low_lsb}) high=({high_advance}, {high_lsb})"
);
}
#[test]
fn source_serif_otf_metadata_loads_without_layout_panic() {
for path in source_serif_otf_paths() {
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let file = crate::FontFile::from_file(&path)
.map_err(|err| format!("FontFile::from_file failed: {err}"))?;
let face = file
.current_face()
.map_err(|err| format!("current_face failed: {err}"))?;
Ok::<(String, String), String>((face.family(), face.full_name()))
}));
match outcome {
Ok(Ok((family, full_name))) => {
assert!(
family.contains("Source Serif 4"),
"unexpected family for {}: {}",
path.display(),
family
);
assert!(
full_name.contains("Black"),
"unexpected full name for {}: {}",
path.display(),
full_name
);
}
Ok(Err(err)) => panic!("failed to load {}: {}", path.display(), err),
Err(_) => panic!("loading {} panicked", path.display()),
}
}
}
#[test]
fn text2command_supports_sbix_bitmap_glyphs() {
let path = first_real_sbix_font_path().expect("load real sbix font");
let bytes = std::fs::read(&path).expect("read sbix font");
let font = crate::load_font_from_buffer(&bytes).expect("load sbix font");
let commands = font.font().text2command("🥺").expect("text2command sbix");
assert_eq!(commands.len(), 1);
assert!(commands[0].commands.is_empty());
let bitmap = commands[0].bitmap.as_ref().expect("bitmap payload");
assert!(matches!(
bitmap.format,
crate::fontreader::BitmapGlyphFormat::Png | crate::fontreader::BitmapGlyphFormat::Jpeg
));
assert!(!bitmap.data.is_empty());
let svg = font.text2svg("🥺", 32.0, "px").expect("svg from sbix");
assert!(svg.contains("<image"));
assert!(svg.contains("data:image/"));
}
#[test]
fn glyph_run_sbix_bitmap_keeps_display_size() {
let path = first_real_sbix_font_path().expect("load real sbix font");
let bytes = std::fs::read(&path).expect("read sbix font");
let font = crate::load_font_from_buffer(&bytes).expect("load sbix font");
let run = font
.text2glyph_run(
"🥺",
crate::FontOptions::from_font_ref(crate::FontRef::Loaded(&font))
.with_font_size(32.0),
)
.expect("glyph run sbix");
assert_eq!(run.glyphs.len(), 1);
match run.glyphs[0].glyph.layers.first() {
Some(crate::GlyphLayer::Raster(layer)) => {
assert!(layer.width.unwrap_or(0) > 0);
assert!(layer.height.unwrap_or(0) > 0);
}
_ => panic!("expected raster glyph layer"),
}
}
#[test]
fn twemoji_sbix_woff2_loads_without_oob_and_renders_bitmap() {
let path = twemoji_sbix_font_path();
assert!(path.exists(), "missing Twemoji sbix fixture");
let bytes = std::fs::read(&path).expect("read Twemoji sbix font");
let font = crate::load_font_from_buffer(&bytes).expect("load Twemoji sbix font");
let mut rendered = None;
for sequence in emoji_ligature_sequence_candidates()
.into_iter()
.chain(["🥺", "😀", "👍", "❤️"].into_iter())
{
let Ok(commands) = font.font().text2command(sequence) else {
continue;
};
if commands.iter().any(|glyph| glyph.bitmap.is_some()) {
rendered = Some((sequence, commands));
break;
}
}
let (sequence, commands) = rendered.expect("render any bitmap glyph from Twemoji sbix");
assert!(!commands.is_empty());
assert!(commands.iter().any(|glyph| glyph.bitmap.is_some()));
let svg = font
.text2svg(sequence, 32.0, "px")
.expect("svg from Twemoji sbix");
assert!(svg.contains("data:image/"));
}
#[test]
fn parse_text_units_for_fallback_keeps_emoji_clusters_together() {
for text in ["👩💻", "👨👩👧👦", "🇯🇵", "1️⃣"] {
let units = crate::fontreader::Font::parse_text_units_for_fallback(text);
assert_eq!(units.len(), 1, "cluster should stay whole for {text:?}");
match &units[0] {
crate::fontreader::ParsedTextUnit::Glyph { text: parsed, .. } => {
assert_eq!(parsed, text);
}
_ => panic!("expected glyph unit for {text:?}"),
}
}
}
#[test]
fn parse_text_units_for_fallback_keeps_combining_mark_clusters_together() {
for text in ["e\u{0301}", "は\u{3099}", "ب\u{0650}", "\u{0F40}\u{0F72}"] {
let units = crate::fontreader::Font::parse_text_units_for_fallback(text);
assert_eq!(
units.len(),
1,
"combining cluster should stay whole for {text:?}"
);
match &units[0] {
crate::fontreader::ParsedTextUnit::Glyph { text: parsed, .. } => {
assert_eq!(parsed, text);
}
_ => panic!("expected glyph unit for {text:?}"),
}
}
}
#[test]
fn loaded_font_text2glyph_run_keeps_real_emoji_ligature_cluster() {
let (path, sequence) =
first_real_emoji_ligature().expect("find real emoji ligature fixture");
let font = crate::load_font_from_file(path).expect("load emoji ligature font");
let run = font
.text2glyph_run(
sequence,
crate::FontOptions::from_font_ref(crate::FontRef::Loaded(&font))
.with_font_size(32.0),
)
.expect("shape emoji ligature");
assert_eq!(run.glyphs.len(), 1, "expected a single ligature glyph");
assert!(!run.glyphs[0].glyph.layers.is_empty());
}
#[test]
fn legacy_text2command_keeps_real_emoji_ligature_cluster() {
let (path, sequence) =
first_real_emoji_ligature_for_legacy().expect("find legacy emoji ligature fixture");
let font = crate::load_font_from_file(path).expect("load emoji ligature font");
let commands = font
.font()
.text2command(sequence)
.expect("legacy emoji ligature");
assert_eq!(commands.len(), 1, "expected a single ligature glyph");
assert!(commands[0].bitmap.is_some() || !commands[0].commands.is_empty());
}
#[test]
fn font_family_fallback_keeps_real_emoji_ligature_cluster() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let (path, sequence) =
first_real_emoji_ligature().expect("find real emoji ligature fixture");
let emoji = crate::load_font_from_file(path).expect("load emoji ligature font");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(emoji);
let text = format!("A{}B", sequence);
let run = crate::text2commands(
&text,
crate::FontOptions::from_family(&family)
.with_font_family("Fira Sans")
.with_font_size(32.0),
)
.expect("render mixed fallback ligature text");
assert_eq!(
run.glyphs.len(),
3,
"emoji ligature cluster should stay a single glyph"
);
assert!(!run.glyphs[1].glyph.layers.is_empty());
}
#[test]
#[cfg(feature = "layout")]
fn loaded_font_text2glyph_run_shapes_police_officer_female_emoji_when_supported() {
let sequence = "👮🏻♀️";
for path in emoji_ligature_font_candidates() {
if !path.exists() {
continue;
}
let font = crate::load_font_from_file(&path).expect("load emoji ligature font");
let Ok(run) = font.text2glyph_run(
sequence,
crate::FontOptions::from_font_ref(crate::FontRef::Loaded(&font))
.with_font_size(32.0),
) else {
continue;
};
if run.glyphs.len() == 1 {
assert!(!run.glyphs[0].glyph.layers.is_empty());
return;
}
}
panic!("expected at least one real font to shape 👮🏻♀️ as a single glyph");
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_keeps_police_officer_female_emoji_cluster_when_supported() {
let sequence = "👮🏻♀️";
for path in emoji_ligature_font_candidates() {
if !path.exists() {
continue;
}
let regular = crate::load_font_from_file(fira_sans_regular_path())
.expect("load regular fira sans");
let emoji = crate::load_font_from_file(&path).expect("load emoji font");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(emoji);
let text = format!("A{}B", sequence);
let Ok(run) = crate::text2commands(
&text,
crate::FontOptions::from_family(&family)
.with_font_family("Fira Sans")
.with_font_size(32.0),
) else {
continue;
};
if run.glyphs.len() == 3 {
assert!(!run.glyphs[1].glyph.layers.is_empty());
return;
}
}
panic!("expected at least one fallback family to keep 👮🏻♀️ as a single glyph cluster");
}
#[test]
fn load_font_from_padded_woff2_buffer_uses_declared_length() {
let path = twemoji_sbix_font_path();
assert!(path.exists(), "missing Twemoji sbix fixture");
let mut bytes = std::fs::read(&path).expect("read woff2 font");
let original_len = bytes.len();
bytes.extend_from_slice(&[0xde, 0xad, 0xbe, 0xef, 0, 1, 2, 3]);
let font = crate::load_font_from_buffer(&bytes).expect("load padded woff2 buffer");
let commands = font
.font()
.text2command("🥺")
.expect("render from padded woff2 buffer");
assert!(!commands.is_empty());
assert!(commands.iter().any(|glyph| glyph.bitmap.is_some()));
assert!(bytes.len() > original_len);
}
#[test]
#[ignore]
fn investigate_police_officer_female_emoji_ligature() {
let sequence = "👮🏻♀️";
for path in emoji_ligature_font_candidates() {
if !path.exists() {
continue;
}
let font = crate::load_font_from_file(&path).expect("load candidate font");
let glyph_ids = font
.font()
.debug_shape_glyph_ids(sequence, None)
.expect("debug shape glyph ids");
let ccmp_applied = glyph_ids
.iter()
.copied()
.enumerate()
.map(|(index, glyph_id)| (glyph_id, index))
.collect::<Vec<_>>();
#[cfg(feature = "layout")]
let mut ccmp_applied = ccmp_applied;
#[cfg(feature = "layout")]
let liga = font.font().gsub.as_ref().and_then(|gsub| {
gsub.apply_ccmp_sequence(&mut ccmp_applied);
gsub.lookup_liga_sequence(
&ccmp_applied
.iter()
.map(|(glyph_id, _)| *glyph_id)
.collect::<Vec<_>>(),
)
});
#[cfg(feature = "layout")]
let rlig = font.font().gsub.as_ref().and_then(|gsub| {
gsub.lookup_rlig_sequence(
&ccmp_applied
.iter()
.map(|(glyph_id, _)| *glyph_id)
.collect::<Vec<_>>(),
None,
)
});
#[cfg(not(feature = "layout"))]
let liga: Option<usize> = None;
#[cfg(not(feature = "layout"))]
let rlig: Option<usize> = None;
let run = font.text2glyph_run(
sequence,
crate::FontOptions::from_font_ref(crate::FontRef::Loaded(&font))
.with_font_size(32.0),
);
let legacy = font.font().text2command(sequence);
println!("font: {}", path.display());
println!(" glyph_ids: {:?}", glyph_ids);
println!(
" ccmp_glyph_ids: {:?}",
ccmp_applied
.iter()
.map(|(glyph_id, _)| *glyph_id)
.collect::<Vec<_>>()
);
println!(" liga_lookup: {:?}", liga);
println!(" rlig_lookup: {:?}", rlig);
println!(
" glyph_run: {}",
match &run {
Ok(run) => format!(
"ok glyphs={} layers0={}",
run.glyphs.len(),
run.glyphs
.first()
.map(|glyph| glyph.glyph.layers.len())
.unwrap_or(0)
),
Err(err) => format!("err {err}"),
}
);
println!(
" legacy: {}",
match &legacy {
Ok(commands) => format!(
"ok glyphs={} first_bitmap={} first_cmds={}",
commands.len(),
commands
.first()
.and_then(|glyph| glyph.bitmap.as_ref())
.is_some(),
commands
.first()
.map(|glyph| glyph.commands.len())
.unwrap_or(0)
),
Err(err) => format!("err {err}"),
}
);
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(font);
let family_run = crate::text2commands(
&format!("A{}B", sequence),
crate::FontOptions::from_family(&family)
.with_font_family("Fira Sans")
.with_font_size(32.0),
);
println!(
" family glyph_run: {}",
match family_run {
Ok(run) => format!("ok glyphs={}", run.glyphs.len()),
Err(err) => format!("err {err}"),
}
);
}
}
#[test]
fn sniff_encoded_image_dimensions_supports_png_and_jpeg_headers() {
let png = vec![
0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a, 0, 0, 0, 13, b'I', b'H', b'D', b'R', 0,
0, 0, 16, 0, 0, 0, 32,
];
let jpeg = vec![
0xff, 0xd8, 0xff, 0xc0, 0x00, 0x11, 0x08, 0x00, 0x20, 0x00, 0x10, 0x03, 0x01, 0x11,
0x00, 0x02, 0x11, 0x00, 0x03, 0x11, 0x00, 0xff, 0xd9,
];
assert_eq!(
crate::util::sniff_encoded_image_dimensions(&png),
Some(("image/png", 16, 32))
);
assert_eq!(
crate::util::sniff_encoded_image_dimensions(&jpeg),
Some(("image/jpeg", 16, 32))
);
}
#[test]
fn glyph_run_from_truetype_outline_works() {
let path = sample_font_path();
let font = crate::load_font_from_file(&path).expect("load font");
let run = crate::text2commands("A", crate::FontOptions::new(&font).with_font_size(24.0))
.expect("glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(run.glyphs[0].glyph.metrics.advance_x > 0.0);
assert!(matches!(
run.glyphs[0].glyph.layers.first(),
Some(crate::GlyphLayer::Path(_))
));
}
#[test]
#[cfg(feature = "cff")]
fn glyph_run_from_cff_outline_works() {
let font = crate::load_font_from_file(cff_font_path()).expect("load cff font");
let run = crate::text2commands("漢", crate::FontOptions::new(&font).with_font_size(24.0))
.expect("glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(matches!(
run.glyphs[0].glyph.layers.first(),
Some(crate::GlyphLayer::Path(_))
));
}
#[test]
fn glyph_run_respects_line_height() {
let path = sample_font_path();
let font = crate::load_font_from_file(&path).expect("load font");
let run = crate::text2commands(
"A\nB",
crate::FontOptions::new(&font)
.with_font_size(24.0)
.with_line_height(40.0),
)
.expect("glyph run");
assert_eq!(run.glyphs.len(), 2);
assert_eq!(run.glyphs[0].y, 0.0);
assert_eq!(run.glyphs[1].y, 40.0);
}
#[test]
#[cfg(not(feature = "svg-fonts"))]
fn glyph_run_rejects_svg_glyphs_without_svg_fonts_feature() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("EmojiOneColor.otf");
let bytes = std::fs::read(path).expect("read svg font");
let font = crate::load_font_from_buffer(&bytes).expect("load svg font");
let err = crate::text2commands("😀", crate::FontOptions::new(&font).with_font_size(32.0))
.expect_err("svg glyphs should be rejected for now");
assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn glyph_run_supports_svg_glyphs_with_svg_fonts_feature() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("EmojiOneColor.otf");
let bytes = std::fs::read(path).expect("read svg font");
let font = crate::load_font_from_buffer(&bytes).expect("load svg font");
let run = crate::text2commands("😀", crate::FontOptions::new(&font).with_font_size(32.0))
.expect("svg glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(
run.glyphs[0].glyph.layers.iter().any(|layer| {
matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty())
|| matches!(layer, crate::GlyphLayer::Svg(layer)
if !layer.document.is_empty()
&& layer.view_box_width > 0.0
&& layer.view_box_height > 0.0)
}),
"expected svg glyph to expose a usable layer"
);
let svg = font.text2svg("😀", 32.0, "px").expect("svg font render");
assert!(svg.contains("<svg"));
}
#[test]
#[cfg(feature = "svg-fonts")]
fn glyph_run_supports_noto_color_emoji_svg_with_svg_fonts_feature() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("NotoColorEmoji-Regular.ttf");
let bytes = std::fs::read(path).expect("read noto color emoji");
let font = crate::load_font_from_buffer(&bytes).expect("load noto color emoji");
let run = crate::text2commands("😀", crate::FontOptions::new(&font).with_font_size(32.0))
.expect("noto color emoji glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(
run.glyphs[0].glyph.layers.iter().any(|layer| {
matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty())
|| matches!(layer, crate::GlyphLayer::Svg(layer)
if !layer.document.is_empty()
&& layer.view_box_width > 0.0
&& layer.view_box_height > 0.0)
}),
"expected svg glyph to expose a usable layer"
);
let svg = font
.text2svg("😀", 32.0, "px")
.expect("noto color emoji svg");
assert!(svg.contains("<svg"));
}
#[test]
#[cfg(feature = "svg-fonts")]
fn svg_font_plain_digit_no_longer_errors_when_svg_table_is_present() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("NotoColorEmoji-Regular.ttf");
let bytes = std::fs::read(path).expect("read noto color emoji");
let font = crate::load_font_from_buffer(&bytes).expect("load noto color emoji");
let run = crate::text2commands("1", crate::FontOptions::new(&font).with_font_size(32.0))
.expect("plain digit glyph run");
assert_eq!(run.glyphs.len(), 1);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn svg_font_zero_does_not_return_legacy_unsupported_error() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join(".test_fonts")
.join("NotoColorEmoji-Regular.ttf");
let bytes = std::fs::read(path).expect("read noto color emoji");
let face = crate::load_font_from_buffer(&bytes).expect("load noto color emoji");
let run = face
.engine()
.with_font_size(32.0)
.shape("0")
.expect("shape zero");
assert_eq!(run.glyphs.len(), 1);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_color_supports_some_zwj_svg_sequence() {
assert_svg_font_supports_any_sequence(
"EmojiOneColor.otf",
&["👩💻", "👨👩👧👦", "🏳️🌈", "👩🏽💻"],
"ZWJ",
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_color_supports_some_variation_svg_sequence() {
assert_svg_font_supports_any_sequence("EmojiOneColor.otf", &["❤️", "1️⃣"], "variation");
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_supports_some_zwj_svg_sequence() {
assert_svg_font_supports_any_sequence(
"NotoColorEmoji-Regular.ttf",
&["👩💻", "👨👩👧👦", "🏳️🌈", "👩🏽💻"],
"ZWJ",
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_supports_some_variation_svg_sequence() {
assert_svg_font_supports_any_sequence(
"NotoColorEmoji-Regular.ttf",
&["❤️", "1️⃣"],
"variation",
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_color_family_fallback_keeps_some_zwj_svg_sequence() {
assert_svg_font_family_fallback_supports_any_sequence(
"EmojiOneColor.otf",
&["👩💻", "👨👩👧👦", "🏳️🌈", "👩🏽💻"],
"ZWJ",
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_color_family_fallback_keeps_some_variation_svg_sequence() {
assert_svg_font_family_fallback_supports_any_sequence(
"EmojiOneColor.otf",
&["❤️", "1️⃣"],
"variation",
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_family_fallback_keeps_some_zwj_svg_sequence() {
assert_svg_font_family_fallback_supports_any_sequence(
"NotoColorEmoji-Regular.ttf",
&["👩💻", "👨👩👧👦", "🏳️🌈", "👩🏽💻"],
"ZWJ",
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_family_fallback_keeps_some_variation_svg_sequence() {
assert_svg_font_family_fallback_supports_any_sequence(
"NotoColorEmoji-Regular.ttf",
&["❤️", "1️⃣"],
"variation",
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn svg_glyph_run_exposes_some_path_layers_when_svg_payload_is_simple_enough() {
for font_name in ["EmojiOneColor.otf", "NotoColorEmoji-Regular.ttf"] {
let path = direct_svg_emoji_font_path(font_name);
let bytes = std::fs::read(&path)
.unwrap_or_else(|err| panic!("read {font_name} for svg path layers: {err}"));
let font = crate::load_font_from_buffer(&bytes)
.unwrap_or_else(|err| panic!("load {font_name} for svg path layers: {err}"));
let run =
crate::text2commands("😀", crate::FontOptions::new(&font).with_font_size(32.0))
.unwrap_or_else(|err| panic!("shape {font_name} for svg path layers: {err}"));
if run.glyphs[0].glyph.layers.iter().any(
|layer| matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty()),
) {
return;
}
}
panic!("expected at least one SVG emoji font to expose path layers");
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_and_noto_color_emoji_expose_real_gradient_payloads() {
for font_name in ["EmojiOneColor.otf", "NotoColorEmoji-Regular.ttf"] {
let Some((sequence, payload)) = first_svg_gradient_payload(font_name) else {
continue;
};
assert!(
payload.contains("Gradient") || payload.contains("gradient"),
"expected gradient payload in {font_name} for {sequence:?}"
);
return;
}
panic!("expected at least one real svg emoji font to expose a gradient payload");
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_or_noto_color_emoji_shapes_real_gradient_paint_layers() {
for font_name in ["EmojiOneColor.otf", "NotoColorEmoji-Regular.ttf"] {
let Some((sequence, _)) = first_svg_gradient_payload(font_name) else {
continue;
};
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path)
.unwrap_or_else(|err| panic!("load {font_name} for gradient paint: {err}"));
let run = crate::text2commands(&sequence, crate::FontOptions::new(&font).with_font_size(32.0))
.unwrap_or_else(|err| panic!("shape {font_name} {sequence:?}: {err}"));
assert_eq!(run.glyphs.len(), 1);
if run.glyphs[0].glyph.layers.iter().any(|layer| {
matches!(
layer,
crate::GlyphLayer::Path(path)
if matches!(
&path.paint,
crate::GlyphPaint::LinearGradient(_)
| crate::GlyphPaint::RadialGradient(_)
)
)
}) {
return;
}
}
panic!("expected at least one real svg emoji font to produce gradient paint layers");
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_or_noto_color_emoji_exposes_inherited_gradient_payloads_when_present() {
for font_name in ["EmojiOneColor.otf", "NotoColorEmoji-Regular.ttf"] {
let Some((sequence, payload)) = first_svg_inherited_gradient_payload(font_name) else {
continue;
};
assert!(
payload.contains("href=\"#") || payload.contains("xlink:href=\"#"),
"expected inherited gradient reference in {font_name} for {sequence:?}"
);
return;
}
}
#[test]
#[cfg(feature = "svg-fonts")]
fn emojione_or_noto_color_emoji_shapes_inherited_gradient_paint_layers_when_present() {
for font_name in ["EmojiOneColor.otf", "NotoColorEmoji-Regular.ttf"] {
let Some((sequence, _)) = first_svg_inherited_gradient_payload(font_name) else {
continue;
};
let path = direct_svg_emoji_font_path(font_name);
let font = crate::load_font_from_file(&path)
.unwrap_or_else(|err| panic!("load {font_name} for inherited gradient paint: {err}"));
let run = crate::text2commands(&sequence, crate::FontOptions::new(&font).with_font_size(32.0))
.unwrap_or_else(|err| panic!("shape {font_name} {sequence:?}: {err}"));
assert_eq!(run.glyphs.len(), 1);
assert!(
run.glyphs[0].glyph.layers.iter().any(|layer| {
matches!(
layer,
crate::GlyphLayer::Path(path)
if matches!(
&path.paint,
crate::GlyphPaint::LinearGradient(_)
| crate::GlyphPaint::RadialGradient(_)
)
)
}),
"expected inherited gradient paint layer for {font_name} {sequence:?}"
);
return;
}
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_exposes_arc_path_payload() {
let Some((sequence, payload)) = first_svg_arc_payload("NotoColorEmoji-Regular.ttf") else {
return;
};
assert!(
payload_has_arc_path_command(&payload),
"expected arc path payload in NotoColorEmoji-Regular.ttf for {sequence:?}"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_arc_payload_shapes_path_layers() {
let Some((sequence, _)) = first_svg_arc_payload("NotoColorEmoji-Regular.ttf") else {
return;
};
let path = direct_svg_emoji_font_path("NotoColorEmoji-Regular.ttf");
let font = crate::load_font_from_file(&path)
.unwrap_or_else(|err| panic!("load NotoColorEmoji-Regular.ttf for arc payload: {err}"));
let run = crate::text2commands(&sequence, crate::FontOptions::new(&font).with_font_size(32.0))
.unwrap_or_else(|err| panic!("shape NotoColorEmoji-Regular.ttf {sequence:?}: {err}"));
assert_eq!(run.glyphs.len(), 1);
assert!(
run.glyphs[0].glyph.layers.iter().any(|layer| {
matches!(
layer,
crate::GlyphLayer::Path(path)
if path.commands.iter().any(|command| matches!(command, crate::Command::CubicBezier(_, _, _)))
)
}),
"expected NotoColorEmoji arc payload {sequence:?} to shape into cubic path layers"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_exposes_transform_payload() {
let Some((sequence, payload)) = first_svg_transform_payload("NotoColorEmoji-Regular.ttf") else {
return;
};
assert!(
payload_has_transform(&payload),
"expected transform payload in NotoColorEmoji-Regular.ttf for {sequence:?}"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_transform_payload_shapes_path_layers() {
let Some((sequence, _)) = first_svg_transform_payload("NotoColorEmoji-Regular.ttf") else {
return;
};
let path = direct_svg_emoji_font_path("NotoColorEmoji-Regular.ttf");
let font = crate::load_font_from_file(&path)
.unwrap_or_else(|err| panic!("load NotoColorEmoji-Regular.ttf for transform payload: {err}"));
let run = crate::text2commands(&sequence, crate::FontOptions::new(&font).with_font_size(32.0))
.unwrap_or_else(|err| panic!("shape NotoColorEmoji-Regular.ttf {sequence:?}: {err}"));
assert_eq!(run.glyphs.len(), 1);
assert!(
run.glyphs[0]
.glyph
.layers
.iter()
.any(|layer| matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty())),
"expected NotoColorEmoji transform payload {sequence:?} to shape into path layers"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_exposes_payload_requiring_svg_fallback_when_present() {
let Some((sequence, payload)) =
first_svg_payload_requiring_fallback("NotoColorEmoji-Regular.ttf")
else {
return;
};
assert!(
payload_has_unsupported_svg_constructs(&payload),
"expected unsupported SVG constructs in NotoColorEmoji-Regular.ttf for {sequence:?}"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn noto_color_emoji_keeps_svg_layer_for_payload_requiring_fallback_when_present() {
let Some((sequence, _)) =
first_svg_payload_requiring_fallback("NotoColorEmoji-Regular.ttf")
else {
return;
};
let path = direct_svg_emoji_font_path("NotoColorEmoji-Regular.ttf");
let font = crate::load_font_from_file(&path).unwrap_or_else(|err| {
panic!("load NotoColorEmoji-Regular.ttf for fallback payload: {err}")
});
let run = crate::text2commands(&sequence, crate::FontOptions::new(&font).with_font_size(32.0))
.unwrap_or_else(|err| panic!("shape NotoColorEmoji-Regular.ttf {sequence:?}: {err}"));
assert_eq!(run.glyphs.len(), 1);
assert!(
run.glyphs[0]
.glyph
.layers
.iter()
.any(|layer| matches!(layer, crate::GlyphLayer::Svg(svg) if !svg.document.is_empty())),
"expected NotoColorEmoji payload {sequence:?} to keep raw Svg fallback layer"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
#[ignore = "diagnostic: scans all NotoColorEmoji SVG glyph ids for unsupported payloads"]
fn noto_color_emoji_glyph_id_requiring_fallback_when_present() {
let Some((glyph_id, payload)) =
first_svg_gid_requiring_fallback("NotoColorEmoji-Regular.ttf")
else {
return;
};
assert!(
payload_has_unsupported_svg_constructs(&payload),
"expected unsupported SVG constructs for glyph {glyph_id}"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
#[ignore = "diagnostic: scans all NotoColorEmoji SVG glyph ids for unsupported payloads"]
fn noto_color_emoji_glyph_id_keeps_svg_fallback_when_present() {
let Some((glyph_id, payload)) =
first_svg_gid_requiring_fallback("NotoColorEmoji-Regular.ttf")
else {
return;
};
let document = crate::opentype::color::svg::SvgGlyphDocument {
payload,
view_box_min_x: 0.0,
view_box_min_y: 0.0,
view_box_width: 32.0,
view_box_height: 32.0,
};
let layers = crate::fontreader::svg_document_to_glyph_layers(&document, 1.0, 1.0);
assert!(
layers
.iter()
.any(|layer| matches!(layer, crate::GlyphLayer::Svg(svg) if !svg.document.is_empty())),
"expected fallback payload for glyph {glyph_id} to keep raw Svg layer"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn svg_document_to_glyph_layers_keeps_svg_fallback_for_pattern_payload() {
let document = crate::opentype::color::svg::SvgGlyphDocument {
payload: concat!(
"<svg>",
"<defs><pattern id=\"p\"><rect x=\"0\" y=\"0\" width=\"2\" height=\"2\"/></pattern></defs>",
"<rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"#123456\"/>",
"<rect x=\"1\" y=\"1\" width=\"8\" height=\"8\" fill=\"url(#p)\"/>",
"</svg>"
)
.to_string(),
view_box_min_x: 0.0,
view_box_min_y: 0.0,
view_box_width: 10.0,
view_box_height: 10.0,
};
let layers = crate::fontreader::svg_document_to_glyph_layers(&document, 1.0, 1.0);
assert!(
layers
.iter()
.any(|layer| matches!(layer, crate::GlyphLayer::Path(path) if !path.commands.is_empty())),
"expected supported shapes to keep path layers"
);
assert!(
layers
.iter()
.any(|layer| matches!(layer, crate::GlyphLayer::Svg(svg) if !svg.document.is_empty())),
"expected unsupported pattern payload to keep raw Svg fallback"
);
}
#[test]
#[cfg(feature = "svg-fonts")]
fn svg_document_to_glyph_layers_uses_svg_only_when_no_path_layers_can_be_built() {
let document = crate::opentype::color::svg::SvgGlyphDocument {
payload: "<svg><defs><pattern id=\"p\"><rect x=\"0\" y=\"0\" width=\"2\" height=\"2\"/></pattern></defs><rect x=\"0\" y=\"0\" width=\"10\" height=\"10\" fill=\"url(#p)\"/></svg>".to_string(),
view_box_min_x: 0.0,
view_box_min_y: 0.0,
view_box_width: 10.0,
view_box_height: 10.0,
};
let layers = crate::fontreader::svg_document_to_glyph_layers(&document, 1.0, 1.0);
assert_eq!(layers.len(), 1);
assert!(matches!(layers[0], crate::GlyphLayer::Svg(_)));
}
#[test]
fn fira_sans_black_i_and_j_have_outline_commands() {
let font = crate::load_font_from_file(fira_sans_black_path()).expect("load fira sans");
for ch in ['i', 'j'] {
let commands = font
.font()
.text2command(&ch.to_string())
.expect("text2command");
assert_eq!(commands.len(), 1, "expected one glyph for {ch}");
assert!(
!commands[0].commands.is_empty(),
"expected outline commands for {ch}"
);
}
}
#[test]
fn glyph_run_fira_sans_black_keeps_outline_layers() {
let font = crate::load_font_from_file(fira_sans_black_path()).expect("load fira sans");
let run = crate::text2commands("ij", crate::FontOptions::new(&font).with_font_size(32.0))
.expect("glyph run");
assert_eq!(run.glyphs.len(), 2);
for (index, glyph) in run.glyphs.iter().enumerate() {
assert!(
glyph.glyph.metrics.bounds.is_some(),
"expected bounds for glyph {index}"
);
match glyph.glyph.layers.first() {
Some(crate::GlyphLayer::Path(path)) => {
assert!(
!path.commands.is_empty(),
"expected path commands for glyph {index}"
);
}
Some(crate::GlyphLayer::Raster(_)) => {
panic!("expected outline layer for Fira Sans glyph {index}");
}
#[cfg(feature = "svg-fonts")]
Some(crate::GlyphLayer::Svg(_)) => {
panic!("expected outline layer for Fira Sans glyph {index}");
}
None => panic!("expected at least one layer for glyph {index}"),
}
}
}
#[test]
fn glyph_run_colr_layers_keep_cpal_argb32_paint() {
let font = crate::load_font_from_file(segoe_emoji_font_path()).expect("load segoe emoji");
let inner = font.font();
let glyph_id = inner
.cmap
.as_ref()
.expect("cmap")
.get_glyph_position('🥺' as u32) as usize;
let expected_layers = inner
.colr
.as_ref()
.expect("colr")
.get_layer_record(glyph_id as u16);
let cpal = inner.cpal.as_ref().expect("cpal");
let run = crate::text2commands("🥺", crate::FontOptions::new(&font).with_font_size(32.0))
.expect("glyph run");
assert_eq!(run.glyphs.len(), 1);
assert_eq!(run.glyphs[0].glyph.layers.len(), expected_layers.len());
for (actual, expected) in run.glyphs[0]
.glyph
.layers
.iter()
.zip(expected_layers.iter())
{
let color = cpal.get_pallet(expected.palette_index as usize);
let expected_argb = ((color.alpha as u32) << 24)
| ((color.red as u32) << 16)
| ((color.green as u32) << 8)
| color.blue as u32;
match actual {
crate::GlyphLayer::Path(path) => match &path.paint {
crate::GlyphPaint::Solid(argb) => assert_eq!(*argb, expected_argb),
crate::GlyphPaint::CurrentColor => {
panic!("expected COLR glyph layer to keep CPAL color")
}
crate::GlyphPaint::LinearGradient(_) | crate::GlyphPaint::RadialGradient(_) => {
panic!("expected COLR glyph layer to keep solid CPAL color")
}
},
crate::GlyphLayer::Raster(_) => {
panic!("expected COLR glyph to use only path layers")
}
#[cfg(feature = "svg-fonts")]
crate::GlyphLayer::Svg(_) => {
panic!("expected COLR glyph to use only path layers")
}
}
}
}
#[test]
fn glyph_run_colr_layers_keep_non_empty_commands() {
let font = crate::load_font_from_file(segoe_emoji_font_path()).expect("load segoe emoji");
let run = crate::text2commands("🥺", crate::FontOptions::new(&font).with_font_size(32.0))
.expect("glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(
!run.glyphs[0].glyph.layers.is_empty(),
"expected at least one COLR layer"
);
for (index, layer) in run.glyphs[0].glyph.layers.iter().enumerate() {
match layer {
crate::GlyphLayer::Path(path) => {
assert!(
!path.commands.is_empty(),
"expected non-empty path commands for COLR layer {index}"
);
}
crate::GlyphLayer::Raster(_) => {
panic!("expected COLR glyph to use only path layers");
}
#[cfg(feature = "svg-fonts")]
crate::GlyphLayer::Svg(_) => {
panic!("expected COLR glyph to use only path layers");
}
}
}
}
#[test]
fn vertical_html_path_is_enabled() {
let path = sample_font_path();
let font = crate::fontload_file(&path).expect("load font");
let html = font
.font()
.get_html_vert("A", 24.0, "px")
.expect("html vert");
assert!(html.contains("writing-mode: vertical-rl"));
assert!(html.contains("<svg"));
}
#[test]
fn variation_selector_real_font_uses_format14() {
let font = crate::fontload_file(japanese_font_path()).expect("load uvs font");
let cmap = font.font().cmap.as_ref().expect("cmap");
let format14 = cmap
.cmap_encodings
.iter()
.find_map(|encoding| match encoding.cmap_subtable.as_ref() {
CmapSubtable::Format14(format14) => Some(format14),
_ => None,
})
.expect("expected format 14 cmap");
let var_selector_record = format14
.var_selector_records
.first()
.expect("expected at least one var selector record");
let mapping = var_selector_record
.non_default_uvs
.unicode_value_ranges
.first()
.expect("expected at least one UVS mapping");
let var_selector = var_selector_record.var_selector;
let unicode_value = mapping.unicode_value;
let glyph_id = mapping.glyph_id;
let base = cmap.get_glyph_position(unicode_value);
let uvs = cmap.get_glyph_position_from_uvs(unicode_value, var_selector);
assert_eq!(uvs, glyph_id);
assert!(base > 0);
}
#[test]
#[cfg(feature = "cff")]
fn glyph_run_uses_real_variation_selector_as_single_cluster() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load uvs font");
let (text, _) = real_variation_sequence(&font);
let run = crate::text2commands(&text, crate::FontOptions::new(&font).with_font_size(32.0))
.expect("glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(matches!(
run.glyphs[0].glyph.layers.first(),
Some(crate::GlyphLayer::Path(_))
));
}
#[test]
#[cfg(feature = "cff")]
fn measure_uses_real_variation_selector_glyph() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load uvs font");
let (text, expected_glyph_id) = real_variation_sequence(&font);
let glyph_ids = font
.font()
.debug_shape_glyph_ids(&text, None)
.expect("shape glyph ids with uvs");
assert_eq!(glyph_ids, vec![expected_glyph_id]);
let width = font.measure(&text).expect("measure with uvs");
assert!(width > 0.0);
}
#[test]
#[cfg(feature = "cff")]
fn html_uses_single_svg_for_variation_selector_cluster_and_ignores_stray_selector() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load uvs font");
let (text, _) = real_variation_sequence(&font);
let html = font
.font()
.get_html(&text, 32.0, "px")
.expect("html with uvs");
assert_eq!(html.matches("<svg").count(), 1);
let html_with_stray = font
.font()
.get_html(&format!("\u{FE0F}{text}"), 32.0, "px")
.expect("html with stray selector");
assert_eq!(html_with_stray.matches("<svg").count(), 1);
}
#[test]
#[cfg(feature = "cff")]
fn font_family_text2commands_keeps_real_variation_selector_cluster() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load uvs font");
let (text, expected_glyph_id) = real_variation_sequence(&font);
let mut family = crate::FontFamily::new("Noto Sans JP");
family.add_loaded_font(font);
let run = family
.text2commands(&text, family.options().with_font_size(32.0))
.expect("family glyph run with uvs");
assert_eq!(run.glyphs.len(), 1);
let glyph_ids = family
.resolve_loaded_font(
Some("Noto Sans JP"),
None,
crate::FontWeight::default(),
crate::FontStyle::default(),
crate::FontStretch::default(),
)
.expect("resolved family font")
.font()
.debug_shape_glyph_ids(&text, None)
.expect("shape glyph ids");
assert_eq!(glyph_ids, vec![expected_glyph_id]);
}
#[test]
fn legacy_text2command_keeps_real_variation_selector_cluster_for_truetype() {
let Some(path) = first_truetype_variation_selector_font_path() else {
return;
};
let font = crate::load_font_from_file(path).expect("load truetype uvs font");
let (text, expected_glyph_id) = real_variation_sequence(&font);
let commands = font
.font()
.text2command(&text)
.expect("legacy text2command with uvs");
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].glyph_id, expected_glyph_id);
assert!(!commands[0].commands.is_empty() || commands[0].bitmap.is_some());
}
#[test]
#[cfg(feature = "layout")]
fn lookup_locale_uses_real_japanese_locl_data() {
let path = japanese_font_path();
let font = crate::fontload_file(&path).expect("load japanese font");
let gsub = font.font().gsub.as_ref().expect("gsub");
let max_glyphs = font.font().maxp.as_ref().expect("maxp").num_glyphs as usize;
let locale = "ja-JP";
let mut found = None;
for glyph_id in 1..=max_glyphs {
let localized = gsub.lookup_locale(glyph_id, locale);
if localized != glyph_id {
found = Some((glyph_id, localized));
break;
}
}
let (glyph_id, localized) = found.expect("expected at least one locl substitution");
assert_ne!(glyph_id, localized);
}
#[test]
#[cfg(feature = "layout")]
fn text_api_uses_real_japanese_locl_substitution_when_requested() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load japanese font");
let cmap = font.font().cmap.as_ref().expect("cmap");
let gsub = font.font().gsub.as_ref().expect("gsub");
let locale = "ja-JP";
let mut found = None;
for codepoint in 0x20u32..=0xFFFF {
let Some(ch) = char::from_u32(codepoint) else {
continue;
};
if ch.is_control() {
continue;
}
let glyph_id = cmap.get_glyph_position(codepoint) as usize;
if glyph_id == 0 {
continue;
}
let localized = gsub.lookup_locale(glyph_id, locale);
if localized != glyph_id {
found = Some((ch, glyph_id, localized));
break;
}
}
let (ch, glyph_id, localized) = found.expect("expected locl-mapped character");
let glyph_ids = font
.font()
.debug_shape_glyph_ids(&ch.to_string(), Some(locale))
.expect("shape glyph ids");
assert_eq!(glyph_ids, vec![localized]);
assert_ne!(glyph_id, localized);
let run = crate::text2commands(
&ch.to_string(),
crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_locale(locale),
)
.expect("glyph run");
assert_eq!(run.glyphs.len(), 1);
}
#[test]
#[cfg(feature = "layout")]
fn text_api_uses_real_jp78_variant_when_requested() {
let Some((path, ch, glyph_id, variant_glyph_id)) =
first_real_variant_substitution(crate::FontVariant::Jis78)
else {
return;
};
let font = crate::load_font_from_file(&path).expect("load japanese variant font");
let default_ids = font
.font()
.debug_shape_glyph_ids_with_variant(
&ch.to_string(),
Some("ja-JP"),
crate::FontVariant::Normal,
)
.expect("default glyph ids");
assert_eq!(default_ids, vec![glyph_id]);
let variant_ids = font
.font()
.debug_shape_glyph_ids_with_variant(
&ch.to_string(),
Some("ja-JP"),
crate::FontVariant::Jis78,
)
.expect("jp78 glyph ids");
assert_eq!(variant_ids, vec![variant_glyph_id]);
assert_ne!(glyph_id, variant_glyph_id);
let run = crate::text2commands(
&ch.to_string(),
crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_locale("ja-JP")
.with_font_variant(crate::FontVariant::Jis78),
)
.expect("jp78 glyph run");
assert_eq!(run.glyphs.len(), 1);
}
#[test]
#[cfg(feature = "layout")]
fn font_engine_public_api_supports_font_variant_selection() {
let Some((path, ch, glyph_id, variant_glyph_id)) =
first_real_variant_substitution(crate::FontVariant::Jis78)
else {
return;
};
let face = crate::FontFile::from_file(&path)
.expect("load font file")
.current_face()
.expect("current face");
let engine = face
.engine()
.with_font_size(32.0)
.with_locale("ja-JP")
.with_jis78();
assert_eq!(engine.font_variant(), crate::FontVariant::Jis78);
let run = engine.shape(&ch.to_string()).expect("shape variant glyph");
assert_eq!(run.glyphs.len(), 1);
assert_ne!(glyph_id, variant_glyph_id);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_text2commands_uses_real_jp78_variant_when_requested() {
let Some((path, ch, _, _variant_glyph_id)) =
first_real_variant_substitution(crate::FontVariant::Jis78)
else {
return;
};
let font = crate::load_font_from_file(&path).expect("load japanese variant font");
let mut family = crate::FontFamily::new("JIS Variant");
family.add_loaded_font(font);
let run = family
.text2commands(
&ch.to_string(),
family
.options()
.with_font_size(32.0)
.with_locale("ja-JP")
.with_font_variant(crate::FontVariant::Jis78),
)
.expect("family jp78 glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(run.glyphs[0].glyph.metrics.advance_x > 0.0);
}
#[test]
#[cfg(feature = "layout")]
fn measure_uses_real_gpos_kern_pair_when_layout_enabled() {
let font = crate::load_font_from_file(fira_sans_regular_path()).expect("load fira sans");
let (left, right, total_adjustment) =
first_real_kern_pair(&font).expect("expected real kern pair in Fira Sans");
let pair = format!("{left}{right}");
let left_width = font.measure(&left.to_string()).expect("measure left");
let right_width = font.measure(&right.to_string()).expect("measure right");
let pair_width = font.measure(&pair).expect("measure kern pair");
let observed_delta = pair_width - (left_width + right_width);
assert!(
(observed_delta - total_adjustment as f64).abs() <= 1.0,
"expected delta {total_adjustment}, got {observed_delta} for {pair:?}",
);
}
#[test]
#[cfg(feature = "layout")]
fn glyph_run_uses_real_gpos_kern_pair_when_layout_enabled() {
let font = crate::load_font_from_file(fira_sans_regular_path()).expect("load fira sans");
let (left, right, total_adjustment) =
first_real_kern_pair(&font).expect("expected real kern pair in Fira Sans");
let options = crate::FontOptions::new(&font).with_font_size(32.0);
let pair = format!("{left}{right}");
let left_run =
crate::text2commands(&left.to_string(), options.clone()).expect("left glyph run");
let right_run =
crate::text2commands(&right.to_string(), options.clone()).expect("right glyph run");
let pair_run = crate::text2commands(&pair, options.clone()).expect("pair glyph run");
assert_eq!(pair_run.glyphs.len(), 2);
let sum_single = left_run.glyphs[0].glyph.metrics.advance_x
+ right_run.glyphs[0].glyph.metrics.advance_x;
let sum_pair = pair_run
.glyphs
.iter()
.map(|glyph| glyph.glyph.metrics.advance_x)
.sum::<f32>();
let hhea = font.font().hhea.as_ref().expect("hhea");
let default_line_height =
(hhea.get_accender() - hhea.get_descender() + hhea.get_line_gap()) as f32;
let scale_x = options.font_size / default_line_height.max(1.0);
let expected_delta = total_adjustment as f32 * scale_x;
let observed_delta = sum_pair - sum_single;
assert!(
(observed_delta - expected_delta).abs() <= 0.25,
"expected scaled delta {expected_delta}, got {observed_delta} for {pair:?}",
);
if expected_delta < 0.0 {
assert!(sum_pair < sum_single);
} else {
assert!(sum_pair > sum_single);
}
}
#[test]
#[cfg(feature = "layout")]
fn lookup_liga_sequence_uses_real_font_data() {
let path = japanese_font_path();
let font = crate::fontload_file(&path).expect("load japanese font");
let gsub = font.font().gsub.as_ref().expect("gsub");
let cmap = font.font().cmap.as_ref().expect("cmap");
let candidates = [
vec!['f', 'i'],
vec!['f', 'l'],
vec!['f', 'f'],
vec!['T', 'o'],
];
for candidate in candidates.iter() {
let glyph_ids: Vec<usize> = candidate
.iter()
.map(|ch| cmap.get_glyph_position(*ch as u32) as usize)
.collect();
if glyph_ids.iter().any(|glyph_id| *glyph_id == 0) {
continue;
}
if let Some(ligature_glyph) = gsub.lookup_liga_sequence(&glyph_ids) {
assert_ne!(ligature_glyph, glyph_ids[0]);
return;
}
}
panic!("expected at least one ligature sequence in real font data");
}
#[test]
#[cfg(feature = "layout")]
fn text2command_uses_real_ligature_glyph_when_layout_enabled() {
let font = crate::load_font_from_file(fira_sans_regular_path()).expect("load fira sans");
let cmap = font.font().cmap.as_ref().expect("cmap");
let gsub = font.font().gsub.as_ref().expect("gsub");
let glyph_ids = [
cmap.get_glyph_position('f' as u32) as usize,
cmap.get_glyph_position('i' as u32) as usize,
];
let ligature_glyph = gsub
.lookup_liga_sequence(&glyph_ids)
.expect("expected fi ligature in Fira Sans");
let commands = font.font().text2command("fi").expect("text2command");
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].glyph_id, ligature_glyph);
assert!(!commands[0].commands.is_empty());
}
#[test]
#[cfg(feature = "layout")]
fn glyph_run_uses_real_ligature_glyph_when_layout_enabled() {
let font = crate::load_font_from_file(fira_sans_regular_path()).expect("load fira sans");
let run = crate::text2commands("fi", crate::FontOptions::new(&font).with_font_size(32.0))
.expect("glyph run");
assert_eq!(run.glyphs.len(), 1);
assert!(run.glyphs[0].glyph.metrics.advance_x > 0.0);
match run.glyphs[0].glyph.layers.first() {
Some(crate::GlyphLayer::Path(path)) => assert!(!path.commands.is_empty()),
Some(crate::GlyphLayer::Raster(_)) => panic!("expected outline ligature glyph"),
#[cfg(feature = "svg-fonts")]
Some(crate::GlyphLayer::Svg(_)) => panic!("expected outline ligature glyph"),
None => panic!("expected ligature layer"),
}
}
#[test]
#[cfg(feature = "layout")]
fn vertical_lookup_uses_real_font_data() {
let font = crate::fontload_file(japanese_font_path()).expect("load japanese font");
let (ch, horizontal, vertical) = first_real_vertical_substitution(&font)
.expect("expected at least one vertical substitution");
assert_ne!(horizontal, vertical, "vertical form should differ for {ch}");
}
#[test]
#[cfg(feature = "layout")]
fn glyph_run_uses_vertical_flow_and_positions_glyphs_vertically() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load japanese font");
let (ch, horizontal, vertical) =
first_real_vertical_substitution(&font).expect("expected vertical substitution");
let text = format!("{ch}{ch}");
let options = crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_vertical_flow();
let run = crate::text2commands(&text, options).expect("vertical glyph run");
assert_eq!(run.glyphs.len(), 2);
let first_font = run.glyphs[0].glyph.font.expect("font metrics");
assert_eq!(first_font.flow, crate::GlyphFlow::Vertical);
assert!(run.glyphs[0].glyph.metrics.advance_y > 0.0);
assert_ne!(horizontal, vertical);
assert!(run.glyphs[1].y > run.glyphs[0].y);
}
#[test]
#[cfg(feature = "layout")]
fn font_engine_public_api_supports_vertical_svg_output() {
let face = crate::FontFile::from_file(japanese_font_path())
.expect("load font file")
.current_face()
.expect("current face");
let (ch, _, _) =
first_real_vertical_substitution(&face).expect("expected vertical substitution");
let text = format!("{ch}{ch}");
let engine = face.engine().with_font_size(32.0).with_vertical_flow();
assert_eq!(engine.shaping_policy(), crate::ShapingPolicy::TopToBottom);
let run = engine.shape(&text).expect("vertical shape");
assert_eq!(run.glyphs.len(), 2);
assert!(run.glyphs[1].y > run.glyphs[0].y);
let svg = engine
.render_svg_vertical(&text)
.expect("render vertical svg via public api");
assert!(svg.contains("<svg"));
}
#[test]
#[cfg(feature = "layout")]
fn measure_with_vertical_flow_reports_positive_inline_extent() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load japanese font");
let (ch, _, _) =
first_real_vertical_substitution(&font).expect("expected vertical substitution");
let text = format!("{ch}{ch}");
let options = crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_vertical_flow();
let run = crate::text2commands(&text, options.clone()).expect("vertical glyph run");
let measure = font
.measure_with_options(&text, options.clone())
.expect("measure vertical flow");
assert!(measure > 0.0);
let expected_min = run.glyphs[1].y as f64 + run.glyphs[1].glyph.metrics.advance_y as f64;
assert!(measure >= expected_min - 1.0);
}
#[test]
fn glyph_run_positions_hebrew_text_right_to_left() {
let font = crate::load_font_from_file(rtl_font_path()).expect("load rtl font");
let text = "אבג";
let cmap = font.font().cmap.as_ref().expect("cmap");
for ch in text.chars() {
assert_ne!(
cmap.get_glyph_position(ch as u32),
0,
"missing glyph for {ch}"
);
}
let ltr_options = crate::FontOptions::new(&font).with_font_size(32.0);
let rtl_options = crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_right_to_left();
let ltr_run = crate::text2commands(text, ltr_options.clone()).expect("ltr glyph run");
let rtl_run = crate::text2commands(text, rtl_options.clone()).expect("rtl glyph run");
assert_eq!(rtl_run.glyphs.len(), 3);
assert_eq!(
rtl_run.glyphs[0].glyph.font.expect("font metrics").flow,
crate::GlyphFlow::Horizontal
);
assert!(rtl_run.glyphs[0].x > rtl_run.glyphs[1].x);
assert!(rtl_run.glyphs[1].x > rtl_run.glyphs[2].x);
let ltr_measure = font
.measure_with_options(text, ltr_options.clone())
.expect("measure ltr");
let rtl_measure = font
.measure_with_options(text, rtl_options.clone())
.expect("measure rtl");
assert!(rtl_measure > 0.0);
assert!((ltr_measure - rtl_measure).abs() <= 1.0);
assert!(ltr_run.glyphs[0].x < ltr_run.glyphs[1].x);
}
#[test]
#[cfg(feature = "layout")]
fn rtl_shaping_uses_real_arabic_joining_forms() {
let font = crate::load_font_from_file(rtl_font_path()).expect("load rtl font");
let (text, expected_glyph_ids) =
first_real_arabic_joining_pair(&font).expect("expected arabic joining pair");
let glyph_ids = font
.font()
.debug_shape_glyph_ids_with_direction(&text, Some("ar"), true)
.expect("shape rtl glyph ids");
assert_eq!(glyph_ids, expected_glyph_ids);
let run = crate::text2commands(
&text,
crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("rtl arabic glyph run");
assert_eq!(run.glyphs.len(), expected_glyph_ids.len());
assert!(run.glyphs[0].x > run.glyphs[1].x);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_text2commands_uses_real_arabic_joining_forms() {
let font = crate::load_font_from_file(rtl_font_path()).expect("load rtl font");
let (text, expected_glyph_ids) =
first_real_arabic_joining_pair(&font).expect("expected arabic joining pair");
let mut family = crate::FontFamily::new("Arial");
family.add_loaded_font(font);
let run = family
.text2commands(
&text,
family
.options()
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("family rtl arabic glyph run");
assert_eq!(run.glyphs.len(), expected_glyph_ids.len());
assert!(run.glyphs[0].x > run.glyphs[1].x);
}
#[test]
#[cfg(feature = "layout")]
fn rtl_shaping_uses_real_arabic_required_ligature() {
let font = crate::load_font_from_file(rtl_font_path()).expect("load rtl font");
let (text, expected_ligature) =
first_real_arabic_rlig_sequence(&font).expect("expected arabic required ligature");
let glyph_ids = font
.font()
.debug_shape_glyph_ids_with_direction(&text, Some("ar"), true)
.expect("shape rtl glyph ids");
assert_eq!(glyph_ids, vec![expected_ligature]);
let run = crate::text2commands(
&text,
crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("rtl arabic ligature glyph run");
assert_eq!(run.glyphs.len(), 1);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_text2commands_uses_real_arabic_required_ligature() {
let font = crate::load_font_from_file(rtl_font_path()).expect("load rtl font");
let (text, expected_ligature) =
first_real_arabic_rlig_sequence(&font).expect("expected arabic required ligature");
let mut family = crate::FontFamily::new("Arial");
family.add_loaded_font(font);
let run = family
.text2commands(
&text,
family
.options()
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("family rtl arabic ligature glyph run");
assert_eq!(run.glyphs.len(), 1);
let glyph_ids = family
.resolve_loaded_font(
Some("Arial"),
None,
crate::FontWeight::default(),
crate::FontStyle::default(),
crate::FontStretch::default(),
)
.expect("resolved family font")
.font()
.debug_shape_glyph_ids_with_direction(&text, Some("ar"), true)
.expect("shape rtl glyph ids");
assert_eq!(glyph_ids, vec![expected_ligature]);
}
#[test]
#[cfg(feature = "layout")]
fn rtl_shaping_uses_real_arabic_contextual_sequence() {
let Some((path, text, expected_glyph_ids)) = first_real_arabic_contextual_sequence() else {
return;
};
let font = crate::load_font_from_file(&path).expect("load rtl contextual font");
let glyph_ids = font
.font()
.debug_shape_glyph_ids_with_direction(&text, Some("ar"), true)
.expect("shape rtl contextual glyph ids");
assert_eq!(glyph_ids, expected_glyph_ids);
let run = crate::text2commands(
&text,
crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("rtl arabic contextual glyph run");
assert_eq!(run.glyphs.len(), expected_glyph_ids.len());
}
#[test]
#[cfg(feature = "layout")]
fn font_family_text2commands_uses_real_arabic_contextual_sequence() {
let Some((path, text, expected_glyph_ids)) = first_real_arabic_contextual_sequence() else {
return;
};
let font = crate::load_font_from_file(&path).expect("load rtl contextual font");
let mut family = crate::FontFamily::new("RTL Contextual");
family.add_loaded_font(font);
let run = family
.text2commands(
&text,
family
.options()
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("family rtl arabic contextual glyph run");
assert_eq!(run.glyphs.len(), expected_glyph_ids.len());
let glyph_ids = family
.resolve_loaded_font(
Some("RTL Contextual"),
None,
crate::FontWeight::default(),
crate::FontStyle::default(),
crate::FontStretch::default(),
)
.expect("resolved family font")
.font()
.debug_shape_glyph_ids_with_direction(&text, Some("ar"), true)
.expect("shape rtl contextual glyph ids");
assert_eq!(glyph_ids, expected_glyph_ids);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_keeps_real_arabic_contextual_run_on_secondary_face() {
let Some((path, text, _)) = first_real_arabic_contextual_sequence() else {
return;
};
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let arabic = crate::load_font_from_file(&path).expect("load arabic contextual font");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(arabic);
let text = format!("A{text}B");
let face_indices = family
.debug_face_indices_for_text(
&text,
family
.options()
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("resolve fallback face indices");
assert_eq!(face_indices.first(), Some(&0));
assert_eq!(face_indices.last(), Some(&0));
assert!(face_indices[1..face_indices.len() - 1]
.iter()
.all(|face_index| *face_index == 1));
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_keeps_real_arabic_mark_cluster_on_secondary_face() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let arabic = crate::load_font_from_file(rtl_font_path()).expect("load arabic font");
let cluster = first_real_mark_cluster(&arabic, 0x0621..=0x064A, 0x0610..=0x065F)
.expect("expected arabic mark cluster");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(arabic);
let text = format!("A{cluster}B");
let face_indices = family
.debug_face_indices_for_text(
&text,
family
.options()
.with_font_size(32.0)
.with_locale("ar")
.with_right_to_left(),
)
.expect("resolve arabic mark fallback face indices");
assert_eq!(face_indices, vec![0, 1, 0]);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_keeps_real_syriac_mark_cluster_on_secondary_face() {
let regular =
crate::load_font_from_file(fira_sans_regular_path()).expect("load regular fira sans");
let syriac = crate::load_font_from_file(syriac_font_path()).expect("load syriac font");
let cluster = first_real_mark_attachment_cluster(&syriac, 0x0710..=0x072C, 0x0730..=0x074A)
.expect("expected syriac mark attachment cluster");
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular);
family.add_loaded_font(syriac);
let text = format!("A{cluster}B");
let face_indices = family
.debug_face_indices_for_text(
&text,
family
.options()
.with_font_size(32.0)
.with_locale("syr-Syrc")
.with_right_to_left(),
)
.expect("resolve syriac mark fallback face indices");
assert_eq!(face_indices, vec![0, 1, 0]);
}
#[test]
#[cfg(feature = "layout")]
fn rtl_shaping_applies_real_gpos_mark_to_base_attachment() {
let font = crate::load_font_from_file(syriac_font_path()).expect("load syriac font");
let Some((text, attachment)) = first_real_gpos_mark_to_base_cluster(
&font,
"syr-Syrc",
0x0710..=0x072C,
0x0730..=0x074A,
) else {
return;
};
let options = crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_locale("syr-Syrc")
.with_right_to_left();
let run = crate::text2commands(&text, options.clone()).expect("syriac mark glyph run");
assert_eq!(run.glyphs.len(), 2);
let base_char = text.chars().next().expect("base char");
let base_glyph_id = font
.font()
.cmap
.as_ref()
.expect("cmap")
.get_glyph_position(base_char as u32) as usize;
let base_layout = font
.font()
.get_layout_with_options(base_glyph_id, false, &options);
let scale = match base_layout {
crate::fontreader::FontLayout::Horizontal(ref layout) if layout.advance_width != 0 => {
run.glyphs[0].glyph.metrics.advance_x / layout.advance_width as f32
}
_ => 1.0,
};
let expected_dx = attachment.x_placement as f32 * scale;
let expected_dy = attachment.y_placement as f32 * scale;
let actual_dx = run.glyphs[1].x - run.glyphs[0].x;
let actual_dy = run.glyphs[1].y - run.glyphs[0].y;
assert!((actual_dx - expected_dx).abs() <= 1.0);
assert!((actual_dy - expected_dy).abs() <= 1.0);
assert_eq!(run.glyphs[1].glyph.metrics.advance_x, 0.0);
assert_eq!(run.glyphs[1].glyph.metrics.advance_y, 0.0);
}
#[test]
#[cfg(feature = "layout")]
fn rtl_shaping_applies_real_gpos_mark_to_mark_attachment() {
let font = crate::load_font_from_file(syriac_font_path()).expect("load syriac font");
let Some((text, base_attachment, mark_attachment)) =
first_real_gpos_mark_stack_cluster(&font, "syr-Syrc", 0x0710..=0x072C, 0x0730..=0x074A)
else {
return;
};
let options = crate::FontOptions::new(&font)
.with_font_size(32.0)
.with_locale("syr-Syrc")
.with_right_to_left();
let run = crate::text2commands(&text, options.clone()).expect("syriac stacked marks");
assert_eq!(run.glyphs.len(), 3);
let base_char = text.chars().next().expect("base char");
let base_glyph_id = font
.font()
.cmap
.as_ref()
.expect("cmap")
.get_glyph_position(base_char as u32) as usize;
let base_layout = font
.font()
.get_layout_with_options(base_glyph_id, false, &options);
let scale = match base_layout {
crate::fontreader::FontLayout::Horizontal(ref layout) if layout.advance_width != 0 => {
run.glyphs[0].glyph.metrics.advance_x / layout.advance_width as f32
}
_ => 1.0,
};
let first_mark_dx = run.glyphs[1].x - run.glyphs[0].x;
let first_mark_dy = run.glyphs[1].y - run.glyphs[0].y;
assert!((first_mark_dx - base_attachment.x_placement as f32 * scale).abs() <= 1.0);
assert!((first_mark_dy - base_attachment.y_placement as f32 * scale).abs() <= 1.0);
let second_mark_dx = run.glyphs[2].x - run.glyphs[1].x;
let second_mark_dy = run.glyphs[2].y - run.glyphs[1].y;
assert!((second_mark_dx - mark_attachment.x_placement as f32 * scale).abs() <= 1.0);
assert!((second_mark_dy - mark_attachment.y_placement as f32 * scale).abs() <= 1.0);
assert_eq!(run.glyphs[1].glyph.metrics.advance_x, 0.0);
assert_eq!(run.glyphs[2].glyph.metrics.advance_x, 0.0);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_checks_arabic_mark_boundaries_across_real_fonts() {
let successes = count_mark_boundary_successes(
&arabic_boundary_font_paths(),
"ar",
true,
detect_arabic_mark_cluster,
);
assert!(
successes >= 1,
"expected at least one Arabic mark font to pass boundary checks"
);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_checks_arabic_mark_stack_boundaries_across_real_fonts() {
let successes = count_mark_boundary_successes(
&arabic_boundary_font_paths(),
"ar",
true,
detect_arabic_mark_stack_cluster,
);
assert!(
successes >= 1,
"expected at least one Arabic stacked-mark font to pass boundary checks"
);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_checks_syriac_mark_boundaries_across_real_fonts() {
let successes = count_mark_boundary_successes(
&syriac_boundary_font_paths(),
"syr-Syrc",
true,
detect_syriac_mark_attachment_cluster,
);
assert!(
successes >= 1,
"expected at least one Syriac mark font to pass boundary checks"
);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_checks_syriac_mark_stack_boundaries_across_real_fonts() {
let successes = count_mark_boundary_successes(
&syriac_boundary_font_paths(),
"syr-Syrc",
true,
detect_syriac_mark_stack_cluster,
);
assert!(
successes >= 1,
"expected at least one Syriac stacked-mark font to pass boundary checks"
);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_checks_hebrew_mark_boundaries_across_real_fonts() {
let successes = count_mark_boundary_successes(
&hebrew_boundary_font_paths(),
"he-Hebr",
true,
detect_hebrew_mark_cluster,
);
assert!(
successes >= 1,
"expected at least one Hebrew mark font to pass boundary checks"
);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_fallback_checks_tibetan_mark_boundaries_across_real_fonts() {
let regular = crate::load_font_from_file(rtl_font_path()).expect("load latin base font");
let mut successes = 0usize;
let mut diagnostics = Vec::new();
for path in tibetan_boundary_font_paths() {
let font = crate::load_font_from_file(&path)
.unwrap_or_else(|err| panic!("load tibetan font {}: {err}", path.display()));
let Some(cluster) = detect_tibetan_mark_cluster(&font) else {
diagnostics.push(format!("{}: no cluster", path.display()));
continue;
};
let mut family = crate::FontFamily::new("Fira Sans");
family.add_loaded_font(regular.clone());
family.add_loaded_font(font);
let text = format!("A{cluster}B");
match family.debug_face_indices_for_text(
&text,
family.options().with_font_size(32.0).with_locale("bo-Tibt"),
) {
Ok(face_indices) if face_indices == vec![0, 1, 0] => successes += 1,
Ok(face_indices) => diagnostics.push(format!(
"{}: cluster={cluster:?} face_indices={face_indices:?}",
path.display()
)),
Err(err) => diagnostics.push(format!(
"{}: cluster={cluster:?} error={err}",
path.display()
)),
}
}
assert!(
successes >= 1,
"expected at least one Tibetan mark font to pass boundary checks: {}",
diagnostics.join(" | ")
);
}
#[test]
#[cfg(feature = "layout")]
fn font_family_text2commands_supports_vertical_flow() {
let font = crate::load_font_from_file(japanese_font_path()).expect("load japanese font");
let (ch, _, _) =
first_real_vertical_substitution(&font).expect("expected vertical substitution");
let mut family = crate::FontFamily::new("Noto Sans JP");
family.add_loaded_font(font);
let run = family
.text2commands(
&format!("{ch}{ch}"),
family.options().with_font_size(32.0).with_vertical_flow(),
)
.expect("family vertical glyph run");
assert_eq!(run.glyphs.len(), 2);
assert_eq!(
run.glyphs[0].glyph.font.expect("font metrics").flow,
crate::GlyphFlow::Vertical
);
assert!(run.glyphs[1].y > run.glyphs[0].y);
}
#[test]
fn font_family_text2commands_supports_right_to_left() {
let font = crate::load_font_from_file(rtl_font_path()).expect("load rtl font");
let mut family = crate::FontFamily::new("Arial");
family.add_loaded_font(font);
let run = family
.text2commands(
"אבג",
family.options().with_font_size(32.0).with_right_to_left(),
)
.expect("family rtl glyph run");
assert_eq!(run.glyphs.len(), 3);
assert!(run.glyphs[0].x > run.glyphs[1].x);
assert!(run.glyphs[1].x > run.glyphs[2].x);
let measure = family
.measure_with_options(
"אבג",
family.options().with_font_size(32.0).with_right_to_left(),
)
.expect("family rtl measure");
assert!(measure > 0.0);
}
}