use crate::parser::{read_i16, read_u16, read_u32};
use crate::Error;
pub const MAX_DUPE_DEPTH: usize = 8;
#[derive(Debug, Clone, Copy)]
pub struct SbixGlyph<'a> {
pub graphic_type: [u8; 4],
pub bytes: &'a [u8],
pub origin_x: i16,
pub origin_y: i16,
}
impl<'a> SbixGlyph<'a> {
pub fn is_dupe(&self) -> bool {
&self.graphic_type == b"dupe"
}
pub fn dupe_target(&self) -> Option<u16> {
if !self.is_dupe() {
return None;
}
if self.bytes.len() < 2 {
return None;
}
Some(u16::from_be_bytes([self.bytes[0], self.bytes[1]]))
}
}
#[derive(Debug, Clone)]
pub struct SbixTable<'a> {
bytes: &'a [u8],
num_strikes: u32,
strike_offsets: Vec<u32>,
num_glyphs: u16,
}
impl<'a> SbixTable<'a> {
pub fn parse(bytes: &'a [u8], num_glyphs: u16) -> Result<Self, Error> {
if bytes.len() < 8 {
return Err(Error::UnexpectedEof);
}
let version = read_u16(bytes, 0)?;
if version != 1 {
return Err(Error::BadStructure("sbix: version must be 1"));
}
let _flags = read_u16(bytes, 2)?;
let num_strikes = read_u32(bytes, 4)?;
if num_strikes > 4096 {
return Err(Error::BadStructure("sbix: numStrikes implausibly large"));
}
let table_end = 8u64 + num_strikes as u64 * 4;
if table_end > bytes.len() as u64 {
return Err(Error::UnexpectedEof);
}
let mut strike_offsets = Vec::with_capacity(num_strikes as usize);
let strike_data_min = 4u64 + (num_glyphs as u64 + 1) * 4;
for i in 0..num_strikes as usize {
let off = read_u32(bytes, 8 + i * 4)?;
let end = (off as u64)
.checked_add(strike_data_min)
.ok_or(Error::BadOffset)?;
if end > bytes.len() as u64 {
return Err(Error::BadOffset);
}
strike_offsets.push(off);
}
Ok(Self {
bytes,
num_strikes,
strike_offsets,
num_glyphs,
})
}
pub fn num_strikes(&self) -> u32 {
self.num_strikes
}
pub fn strike_ppem(&self, strike_index: u32) -> Option<u16> {
let off = *self.strike_offsets.get(strike_index as usize)? as usize;
read_u16(self.bytes, off).ok()
}
pub fn strike_size(&self, strike_index: u32) -> Option<(u16, u16)> {
let off = *self.strike_offsets.get(strike_index as usize)? as usize;
let ppem = read_u16(self.bytes, off).ok()?;
let ppi = read_u16(self.bytes, off + 2).ok()?;
Some((ppem, ppi))
}
pub fn all_ppems(&self) -> Vec<u16> {
(0..self.num_strikes)
.filter_map(|i| self.strike_ppem(i))
.collect()
}
pub fn all_ppems_unique_sorted(&self) -> Vec<u16> {
let mut v = self.all_ppems();
v.sort_unstable();
v.dedup();
v
}
pub fn glyph(&self, strike_index: u32, glyph_id: u16) -> Option<SbixGlyph<'a>> {
if glyph_id >= self.num_glyphs {
return None;
}
let strike_off = *self.strike_offsets.get(strike_index as usize)? as usize;
let g = glyph_id as usize;
let off_lo = read_u32(self.bytes, strike_off + 4 + g * 4).ok()?;
let off_hi = read_u32(self.bytes, strike_off + 4 + (g + 1) * 4).ok()?;
if off_lo == off_hi {
return None;
}
let abs_lo = strike_off.checked_add(off_lo as usize)?;
let abs_hi = strike_off.checked_add(off_hi as usize)?;
if abs_hi > self.bytes.len() || abs_hi < abs_lo + 8 {
return None;
}
let blob = &self.bytes[abs_lo..abs_hi];
let origin_x = read_i16(blob, 0).ok()?;
let origin_y = read_i16(blob, 2).ok()?;
let mut graphic_type = [0u8; 4];
graphic_type.copy_from_slice(&blob[4..8]);
let data = &blob[8..];
Some(SbixGlyph {
graphic_type,
bytes: data,
origin_x,
origin_y,
})
}
pub fn resolve_dupe_chain(&self, strike_index: u32, glyph_id: u16) -> Option<SbixGlyph<'a>> {
let mut visited: [u16; MAX_DUPE_DEPTH] = [0; MAX_DUPE_DEPTH];
let mut visited_len = 0usize;
let mut cur = glyph_id;
for _ in 0..MAX_DUPE_DEPTH {
if visited[..visited_len].contains(&cur) {
return None;
}
if visited_len < MAX_DUPE_DEPTH {
visited[visited_len] = cur;
visited_len += 1;
}
let entry = self.glyph(strike_index, cur)?;
if !entry.is_dupe() {
return Some(entry);
}
cur = entry.dupe_target()?;
}
None
}
pub fn lookup_best_fit_resolved(
&self,
glyph_id: u16,
target_ppem: u16,
) -> Option<SbixGlyph<'a>> {
let mut best: Option<(u32, u32)> = None; for i in 0..self.num_strikes {
let ppem = match self.strike_ppem(i) {
Some(p) => p,
None => continue,
};
if self.glyph(i, glyph_id).is_none() {
continue;
}
let dist = (ppem as i32 - target_ppem as i32).unsigned_abs();
match best {
None => best = Some((i, dist)),
Some((bi, bd)) => {
if dist < bd
|| (dist == bd
&& self.strike_ppem(i).unwrap_or(0) > self.strike_ppem(bi).unwrap_or(0))
{
best = Some((i, dist));
}
}
}
}
let (strike_idx, _) = best?;
self.resolve_dupe_chain(strike_idx, glyph_id)
}
pub fn lookup_best_fit(&self, glyph_id: u16, target_ppem: u16) -> Option<SbixGlyph<'a>> {
let mut best: Option<(u32, u32)> = None; for i in 0..self.num_strikes {
let ppem = match self.strike_ppem(i) {
Some(p) => p,
None => continue,
};
if self.glyph(i, glyph_id).is_none() {
continue;
}
let dist = (ppem as i32 - target_ppem as i32).unsigned_abs();
match best {
None => best = Some((i, dist)),
Some((bi, bd)) => {
if dist < bd
|| (dist == bd
&& self.strike_ppem(i).unwrap_or(0) > self.strike_ppem(bi).unwrap_or(0))
{
best = Some((i, dist));
}
}
}
}
best.and_then(|(i, _)| self.glyph(i, glyph_id))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_sbix(num_glyphs: u16) -> Vec<u8> {
assert_eq!(num_glyphs, 3);
let strike_header_len = 4 + (num_glyphs as usize + 1) * 4; let glyph_payload = 5usize;
let glyph_blob = 8 + glyph_payload; let strike_data = num_glyphs as usize * glyph_blob; let strike_total = strike_header_len + strike_data;
let header_len = 8 + 2 * 4; let strike0 = header_len; let strike1 = strike0 + strike_total; let total = strike1 + strike_total;
let mut bytes = vec![0u8; total];
bytes[0..2].copy_from_slice(&1u16.to_be_bytes()); bytes[2..4].copy_from_slice(&1u16.to_be_bytes()); bytes[4..8].copy_from_slice(&2u32.to_be_bytes()); bytes[8..12].copy_from_slice(&(strike0 as u32).to_be_bytes());
bytes[12..16].copy_from_slice(&(strike1 as u32).to_be_bytes());
bytes[strike0..strike0 + 2].copy_from_slice(&32u16.to_be_bytes());
bytes[strike0 + 2..strike0 + 4].copy_from_slice(&96u16.to_be_bytes());
for i in 0..=num_glyphs as usize {
let off = strike_header_len + i * glyph_blob;
let dst = strike0 + 4 + i * 4;
bytes[dst..dst + 4].copy_from_slice(&(off as u32).to_be_bytes());
}
for g in 0..num_glyphs as usize {
let blob_off = strike0 + strike_header_len + g * glyph_blob;
bytes[blob_off..blob_off + 2].copy_from_slice(&((g as i16 + 1) as u16).to_be_bytes());
bytes[blob_off + 2..blob_off + 4]
.copy_from_slice(&(-(g as i16 + 10) as u16).to_be_bytes());
bytes[blob_off + 4..blob_off + 8].copy_from_slice(b"png ");
bytes[blob_off + 8..blob_off + 13].copy_from_slice(&[g as u8, 0xAA, 0xBB, 0xCC, 0xDD]);
}
bytes[strike1..strike1 + 2].copy_from_slice(&64u16.to_be_bytes());
bytes[strike1 + 2..strike1 + 4].copy_from_slice(&192u16.to_be_bytes());
for i in 0..=num_glyphs as usize {
let off = strike_header_len + i * glyph_blob;
let dst = strike1 + 4 + i * 4;
bytes[dst..dst + 4].copy_from_slice(&(off as u32).to_be_bytes());
}
for g in 0..num_glyphs as usize {
let blob_off = strike1 + strike_header_len + g * glyph_blob;
bytes[blob_off..blob_off + 2].copy_from_slice(&((g as i16 + 100) as u16).to_be_bytes());
bytes[blob_off + 2..blob_off + 4]
.copy_from_slice(&(-(g as i16 + 50) as u16).to_be_bytes());
bytes[blob_off + 4..blob_off + 8].copy_from_slice(b"png ");
bytes[blob_off + 8..blob_off + 13].copy_from_slice(&[
g as u8 ^ 0xFF,
0x11,
0x22,
0x33,
0x44,
]);
}
bytes
}
#[test]
fn parses_header_and_strikes() {
let bytes = synth_sbix(3);
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
assert_eq!(sbix.num_strikes(), 2);
assert_eq!(sbix.strike_ppem(0), Some(32));
assert_eq!(sbix.strike_ppem(1), Some(64));
assert_eq!(sbix.strike_ppem(2), None);
assert_eq!(sbix.strike_size(0), Some((32, 96)));
assert_eq!(sbix.strike_size(1), Some((64, 192)));
}
#[test]
fn glyph_lookup_strike0() {
let bytes = synth_sbix(3);
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
for g in 0u16..3 {
let entry = sbix.glyph(0, g).expect("entry");
assert_eq!(entry.graphic_type, *b"png ");
assert_eq!(entry.origin_x, g as i16 + 1);
assert_eq!(entry.origin_y, -(g as i16 + 10));
assert_eq!(entry.bytes.len(), 5);
assert_eq!(entry.bytes[0], g as u8);
}
}
#[test]
fn glyph_lookup_strike1() {
let bytes = synth_sbix(3);
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
let entry = sbix.glyph(1, 2).expect("entry");
assert_eq!(entry.origin_x, 102);
assert_eq!(entry.origin_y, -52);
assert_eq!(entry.bytes[0], 2u8 ^ 0xFF);
}
#[test]
fn out_of_range_returns_none() {
let bytes = synth_sbix(3);
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
assert!(sbix.glyph(0, 99).is_none()); assert!(sbix.glyph(99, 0).is_none()); }
#[test]
fn rejects_bad_version() {
let mut bytes = vec![0u8; 16];
bytes[0..2].copy_from_slice(&2u16.to_be_bytes()); bytes[4..8].copy_from_slice(&0u32.to_be_bytes());
assert!(matches!(
SbixTable::parse(&bytes, 0),
Err(Error::BadStructure(_))
));
}
#[test]
fn ppems_unique_sorted() {
let bytes = synth_sbix(3);
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
assert_eq!(sbix.all_ppems_unique_sorted(), vec![32u16, 64u16]);
}
#[test]
fn best_fit_picks_closest_strike() {
let bytes = synth_sbix(3);
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
let e = sbix.lookup_best_fit(0, 30).expect("entry");
assert_eq!(e.bytes[0], 0u8);
let e = sbix.lookup_best_fit(0, 70).expect("entry");
assert_eq!(e.bytes[0], 0xFFu8);
let e = sbix.lookup_best_fit(1, 48).expect("entry");
assert_eq!(e.bytes[0], 1u8 ^ 0xFF);
}
#[test]
fn zero_length_entry_returns_none() {
let bytes = {
let mut b = synth_sbix(3);
let strike0 = 16usize;
let off1 = read_u32(&b, strike0 + 4 + 4).unwrap();
b[strike0 + 4 + 2 * 4..strike0 + 4 + 2 * 4 + 4].copy_from_slice(&off1.to_be_bytes());
b
};
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
assert!(sbix.glyph(0, 1).is_none());
assert!(sbix.glyph(0, 0).is_some());
}
fn synth_sbix_with_entries(entries: &[([u8; 4], [u8; 2])]) -> Vec<u8> {
let num_glyphs = entries.len() as u16;
let strike_header_len = 4 + (num_glyphs as usize + 1) * 4;
let payload_len = 2usize;
let blob_len = 8 + payload_len;
let strike_data = num_glyphs as usize * blob_len;
let strike_total = strike_header_len + strike_data;
let header_len = 8 + 4; let strike0 = header_len;
let total = strike0 + strike_total;
let mut bytes = vec![0u8; total];
bytes[0..2].copy_from_slice(&1u16.to_be_bytes()); bytes[2..4].copy_from_slice(&1u16.to_be_bytes()); bytes[4..8].copy_from_slice(&1u32.to_be_bytes()); bytes[8..12].copy_from_slice(&(strike0 as u32).to_be_bytes());
bytes[strike0..strike0 + 2].copy_from_slice(&32u16.to_be_bytes());
bytes[strike0 + 2..strike0 + 4].copy_from_slice(&96u16.to_be_bytes());
for i in 0..=num_glyphs as usize {
let off = strike_header_len + i * blob_len;
let dst = strike0 + 4 + i * 4;
bytes[dst..dst + 4].copy_from_slice(&(off as u32).to_be_bytes());
}
for (g, (tag, payload)) in entries.iter().enumerate() {
let blob_off = strike0 + strike_header_len + g * blob_len;
bytes[blob_off + 4..blob_off + 8].copy_from_slice(tag);
bytes[blob_off + 8..blob_off + 10].copy_from_slice(payload);
}
bytes
}
#[test]
fn dupe_predicates_decode_target_glyph_id() {
let bytes = synth_sbix_with_entries(&[(*b"png ", [0x12, 0x34]), (*b"dupe", [0x00, 0x00])]);
let sbix = SbixTable::parse(&bytes, 2).expect("parse");
let g0 = sbix.glyph(0, 0).unwrap();
let g1 = sbix.glyph(0, 1).unwrap();
assert!(!g0.is_dupe());
assert_eq!(g0.dupe_target(), None);
assert!(g1.is_dupe());
assert_eq!(g1.dupe_target(), Some(0));
}
#[test]
fn resolve_dupe_chain_follows_indirection_to_real_entry() {
let bytes = synth_sbix_with_entries(&[
(*b"png ", [0xAA, 0xBB]),
(*b"dupe", [0x00, 0x00]),
(*b"dupe", [0x00, 0x01]),
]);
let sbix = SbixTable::parse(&bytes, 3).expect("parse");
let r0 = sbix.resolve_dupe_chain(0, 0).expect("entry");
assert_eq!(&r0.graphic_type, b"png ");
let r1 = sbix.resolve_dupe_chain(0, 1).expect("entry");
assert_eq!(&r1.graphic_type, b"png ");
assert_eq!(r1.bytes[0..2], [0xAA, 0xBB]);
let r2 = sbix.resolve_dupe_chain(0, 2).expect("entry");
assert_eq!(&r2.graphic_type, b"png ");
}
#[test]
fn resolve_dupe_chain_detects_two_glyph_cycle() {
let bytes = synth_sbix_with_entries(&[(*b"dupe", [0x00, 0x01]), (*b"dupe", [0x00, 0x00])]);
let sbix = SbixTable::parse(&bytes, 2).expect("parse");
assert!(sbix.resolve_dupe_chain(0, 0).is_none());
assert!(sbix.resolve_dupe_chain(0, 1).is_none());
}
#[test]
fn resolve_dupe_chain_detects_self_cycle() {
let bytes = synth_sbix_with_entries(&[(*b"dupe", [0x00, 0x00])]);
let sbix = SbixTable::parse(&bytes, 1).expect("parse");
assert!(sbix.resolve_dupe_chain(0, 0).is_none());
}
#[test]
fn resolve_dupe_chain_caps_depth() {
let n = MAX_DUPE_DEPTH + 1;
let mut entries: Vec<([u8; 4], [u8; 2])> = Vec::with_capacity(n);
for g in 0..n - 1 {
let next = (g + 1) as u16;
entries.push((*b"dupe", next.to_be_bytes()));
}
entries.push((*b"dupe", (n as u16 + 100).to_be_bytes()));
let bytes = synth_sbix_with_entries(&entries);
let sbix = SbixTable::parse(&bytes, n as u16).expect("parse");
assert!(sbix.resolve_dupe_chain(0, 0).is_none());
}
#[test]
fn resolve_dupe_chain_returns_none_for_oob_dupe_target() {
let bytes = synth_sbix_with_entries(&[(*b"dupe", [0x00, 0x63])]);
let sbix = SbixTable::parse(&bytes, 1).expect("parse");
assert!(sbix.resolve_dupe_chain(0, 0).is_none());
}
#[test]
fn lookup_best_fit_resolved_walks_through_dupe() {
let bytes = synth_sbix_with_entries(&[(*b"png ", [0xAB, 0xCD]), (*b"dupe", [0x00, 0x00])]);
let sbix = SbixTable::parse(&bytes, 2).expect("parse");
let raw = sbix.lookup_best_fit(1, 32).expect("entry");
assert!(raw.is_dupe());
assert_eq!(raw.dupe_target(), Some(0));
let res = sbix.lookup_best_fit_resolved(1, 32).expect("entry");
assert!(!res.is_dupe());
assert_eq!(&res.graphic_type, b"png ");
assert_eq!(res.bytes[0..2], [0xAB, 0xCD]);
}
#[test]
fn dupe_predicates_return_none_on_short_payload() {
let num_glyphs: u16 = 1;
let strike_header_len = 4 + (num_glyphs as usize + 1) * 4;
let blob_len = 8; let strike_data = num_glyphs as usize * blob_len;
let strike_total = strike_header_len + strike_data;
let header_len = 8 + 4;
let strike0 = header_len;
let total = strike0 + strike_total;
let mut bytes = vec![0u8; total];
bytes[0..2].copy_from_slice(&1u16.to_be_bytes());
bytes[2..4].copy_from_slice(&1u16.to_be_bytes());
bytes[4..8].copy_from_slice(&1u32.to_be_bytes());
bytes[8..12].copy_from_slice(&(strike0 as u32).to_be_bytes());
bytes[strike0..strike0 + 2].copy_from_slice(&32u16.to_be_bytes());
bytes[strike0 + 2..strike0 + 4].copy_from_slice(&96u16.to_be_bytes());
for i in 0..=num_glyphs as usize {
let off = strike_header_len + i * blob_len;
let dst = strike0 + 4 + i * 4;
bytes[dst..dst + 4].copy_from_slice(&(off as u32).to_be_bytes());
}
let blob_off = strike0 + strike_header_len;
bytes[blob_off + 4..blob_off + 8].copy_from_slice(b"dupe");
let sbix = SbixTable::parse(&bytes, 1).expect("parse");
let g = sbix.glyph(0, 0).expect("entry");
assert!(g.is_dupe());
assert_eq!(g.dupe_target(), None);
assert!(sbix.resolve_dupe_chain(0, 0).is_none());
}
}