use crate::parser::{read_i16, read_u16, read_u32};
use crate::Error;
#[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,
}
#[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 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());
}
}