use oxideav_ttf::Font;
use oxideav_ttf::{
script_lang_tags, META_DATA_MAP_LEN, META_HEADER_LEN, META_TAG_DLNG, META_TAG_SLNG,
META_VERSION_1,
};
const DEJAVU_MONO: &[u8] = include_bytes!("fixtures/DejaVuSansMono.ttf");
const DEJAVU_SANS: &[u8] = include_bytes!("fixtures/DejaVuSans.ttf");
const INTER_VARIABLE: &[u8] = include_bytes!("fixtures/InterVariable.ttf");
const NOTO_ARABIC: &[u8] = include_bytes!("fixtures/NotoSansArabic-Regular.ttf");
#[test]
fn shipped_fixtures_omit_meta_table() {
for bytes in [DEJAVU_MONO, DEJAVU_SANS, INTER_VARIABLE, NOTO_ARABIC] {
let font = Font::from_bytes(bytes).expect("fixture parses");
assert!(!font.has_meta(), "expected no meta on this fixture");
assert!(font.meta_table().is_none());
assert!(font.meta_record(b"dlng").is_none());
assert!(font.meta_design_languages().is_none());
assert!(font.meta_supported_languages().is_none());
}
}
fn build_minimal_sfnt(tables: &[(&[u8; 4], Vec<u8>)]) -> Vec<u8> {
let n = tables.len() as u16;
let mut out = Vec::new();
out.extend_from_slice(&0x00010000u32.to_be_bytes()); out.extend_from_slice(&n.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&0u16.to_be_bytes());
let dir_size = 16usize * n as usize;
let mut payload_offset = 12usize + dir_size;
let mut records = Vec::with_capacity(n as usize);
for (tag, payload) in tables {
records.push((*tag, payload_offset as u32, payload.len() as u32));
payload_offset += payload.len();
while payload_offset % 4 != 0 {
payload_offset += 1;
}
}
for (tag, offset, length) in &records {
out.extend_from_slice(tag.as_slice());
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&offset.to_be_bytes());
out.extend_from_slice(&length.to_be_bytes());
}
for (_tag, payload) in tables {
out.extend_from_slice(payload);
while out.len() % 4 != 0 {
out.push(0);
}
}
out
}
fn make_head(units_per_em: u16, index_to_loc_format: i16) -> Vec<u8> {
let mut h = vec![0u8; 54];
h[0..4].copy_from_slice(&0x00010000u32.to_be_bytes());
h[4..8].copy_from_slice(&0x00010000u32.to_be_bytes());
h[12..16].copy_from_slice(&0x5F0F3CF5u32.to_be_bytes());
h[18..20].copy_from_slice(&units_per_em.to_be_bytes());
h[50..52].copy_from_slice(&index_to_loc_format.to_be_bytes());
h
}
fn make_hhea(num_h_metrics: u16) -> Vec<u8> {
let mut h = vec![0u8; 36];
h[0..4].copy_from_slice(&0x00010000u32.to_be_bytes());
h[34..36].copy_from_slice(&num_h_metrics.to_be_bytes());
h
}
fn make_maxp(num_glyphs: u16) -> Vec<u8> {
let mut h = vec![0u8; 6];
h[0..4].copy_from_slice(&0x00005000u32.to_be_bytes()); h[4..6].copy_from_slice(&num_glyphs.to_be_bytes());
h
}
fn make_cmap_empty() -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&3u16.to_be_bytes()); out.extend_from_slice(&1u16.to_be_bytes()); out.extend_from_slice(&12u32.to_be_bytes());
let mut sub = Vec::new();
sub.extend_from_slice(&4u16.to_be_bytes()); sub.extend_from_slice(&32u16.to_be_bytes()); sub.extend_from_slice(&0u16.to_be_bytes());
sub.extend_from_slice(&2u16.to_be_bytes()); sub.extend_from_slice(&2u16.to_be_bytes());
sub.extend_from_slice(&0u16.to_be_bytes());
sub.extend_from_slice(&0u16.to_be_bytes());
sub.extend_from_slice(&0xFFFFu16.to_be_bytes()); sub.extend_from_slice(&0u16.to_be_bytes()); sub.extend_from_slice(&0xFFFFu16.to_be_bytes()); sub.extend_from_slice(&1u16.to_be_bytes()); sub.extend_from_slice(&0u16.to_be_bytes()); let total = sub.len() as u16;
sub[2..4].copy_from_slice(&total.to_be_bytes());
out.extend_from_slice(&sub);
out
}
fn make_name_empty() -> Vec<u8> {
let mut out = vec![0u8; 6];
out[4..6].copy_from_slice(&6u16.to_be_bytes()); out
}
fn make_hmtx(num_h_metrics: u16, num_glyphs: u16) -> Vec<u8> {
let mut out = Vec::new();
for _ in 0..num_h_metrics {
out.extend_from_slice(&0u16.to_be_bytes());
out.extend_from_slice(&0i16.to_be_bytes());
}
for _ in num_h_metrics..num_glyphs {
out.extend_from_slice(&0i16.to_be_bytes());
}
out
}
fn make_loca_short(num_glyphs: u16) -> Vec<u8> {
let mut out = Vec::new();
for _ in 0..=num_glyphs {
out.extend_from_slice(&0u16.to_be_bytes());
}
out
}
fn make_glyf_empty() -> Vec<u8> {
vec![0u8; 2]
}
fn make_meta(records: &[(&[u8; 4], &[u8])]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&META_VERSION_1.to_be_bytes());
out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&(records.len() as u32).to_be_bytes()); let payload_base = META_HEADER_LEN + records.len() * META_DATA_MAP_LEN;
let mut cur = payload_base;
for (tag, payload) in records {
out.extend_from_slice(tag.as_slice());
out.extend_from_slice(&(cur as u32).to_be_bytes());
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
cur += payload.len();
}
for (_tag, payload) in records {
out.extend_from_slice(payload);
}
out
}
fn build_font_with_meta(meta_payload: Vec<u8>) -> Vec<u8> {
let num_glyphs = 4u16;
let tables: Vec<(&[u8; 4], Vec<u8>)> = vec![
(b"head", make_head(1000, 0)),
(b"hhea", make_hhea(1)),
(b"maxp", make_maxp(num_glyphs)),
(b"cmap", make_cmap_empty()),
(b"name", make_name_empty()),
(b"hmtx", make_hmtx(1, num_glyphs)),
(b"loca", make_loca_short(num_glyphs)),
(b"glyf", make_glyf_empty()),
(b"meta", meta_payload),
];
build_minimal_sfnt(&tables)
}
#[test]
fn synth_font_round_trips_dlng_and_slng() {
let payload = make_meta(&[
(b"dlng", b"Latn"),
(b"slng", b"Latn, Cyrl, Grek"),
(b"XYZ1", b"vendor-blob"),
]);
let font_bytes = build_font_with_meta(payload);
let font = Font::from_bytes(&font_bytes).expect("synth font parses");
assert!(font.has_meta(), "synthesised font must surface meta");
let table = font.meta_table().expect("meta_table");
assert_eq!(table.version(), META_VERSION_1);
assert_eq!(table.flags(), 0);
assert_eq!(table.records().len(), 3);
assert_eq!(font.meta_design_languages(), Some("Latn"));
assert_eq!(font.meta_supported_languages(), Some("Latn, Cyrl, Grek"));
let dlng = font.meta_record(&META_TAG_DLNG).expect("dlng record");
assert_eq!(&dlng.tag, &META_TAG_DLNG);
assert_eq!(dlng.payload, b"Latn");
let slng = font.meta_record(&META_TAG_SLNG).expect("slng record");
assert_eq!(slng.payload_as_str(), Some("Latn, Cyrl, Grek"));
let vendor = font.meta_record(b"XYZ1").expect("vendor record");
assert_eq!(vendor.payload, b"vendor-blob");
}
#[test]
fn script_lang_tag_splitter_on_synth_font_slng_payload() {
let payload = make_meta(&[(b"slng", b"Latn, sr-Cyrl, en-Dsrt, Hant-HK")]);
let font_bytes = build_font_with_meta(payload);
let font = Font::from_bytes(&font_bytes).expect("synth font parses");
let slng = font
.meta_supported_languages()
.expect("supported languages decoded");
let tags: Vec<_> = script_lang_tags(slng).map(|t| t.raw).collect();
assert_eq!(
tags,
vec!["Latn", "sr-Cyrl", "en-Dsrt", "Hant-HK"],
"splitter must preserve order and strip whitespace"
);
}
#[test]
fn meta_table_with_no_records_round_trips_as_empty() {
let payload = make_meta(&[]);
let font_bytes = build_font_with_meta(payload);
let font = Font::from_bytes(&font_bytes).expect("synth font parses");
assert!(font.has_meta());
let table = font.meta_table().unwrap();
assert_eq!(table.records().len(), 0);
assert!(font.meta_design_languages().is_none());
assert!(font.meta_supported_languages().is_none());
assert!(font.meta_record(&META_TAG_DLNG).is_none());
}
#[test]
fn malformed_meta_table_rejects_at_parse_time() {
let mut payload = make_meta(&[(b"dlng", b"Latn")]);
payload[0..4].copy_from_slice(&2u32.to_be_bytes());
let font_bytes = build_font_with_meta(payload);
assert!(
Font::from_bytes(&font_bytes).is_err(),
"version != 1 must surface as parse error"
);
}