use ttf_parser::Face;
use super::msdf::{MsdfGlyph, build_glyph_msdf, glyph_advance};
const MAGIC: &[u8; 4] = b"DMSF";
const FORMAT_VERSION: u16 = 2;
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SnapshotHeader {
pub base_em: u32,
pub spread: f64,
pub error_correction: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SnapshotGlyph {
Outline(MsdfGlyph),
Empty {
advance: f32,
},
}
pub type SnapshotSection = (u64, Vec<(u16, u16, SnapshotGlyph)>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SnapshotError {
BadMagic,
UnsupportedVersion(u16),
ParamMismatch,
Truncated,
}
impl std::fmt::Display for SnapshotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SnapshotError::BadMagic => write!(f, "not an MSDF snapshot (bad magic)"),
SnapshotError::UnsupportedVersion(v) => {
write!(f, "unsupported MSDF snapshot version {v}")
}
SnapshotError::ParamMismatch => {
write!(f, "snapshot bake parameters differ from the atlas")
}
SnapshotError::Truncated => write!(f, "MSDF snapshot is truncated"),
}
}
}
impl std::error::Error for SnapshotError {}
fn push_u16(buf: &mut Vec<u8>, v: u16) {
buf.extend_from_slice(&v.to_le_bytes());
}
fn push_u32(buf: &mut Vec<u8>, v: u32) {
buf.extend_from_slice(&v.to_le_bytes());
}
fn push_u64(buf: &mut Vec<u8>, v: u64) {
buf.extend_from_slice(&v.to_le_bytes());
}
fn push_f32(buf: &mut Vec<u8>, v: f32) {
buf.extend_from_slice(&v.to_le_bytes());
}
pub fn encode_snapshot(header: SnapshotHeader, sections: &[SnapshotSection]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(MAGIC);
push_u16(&mut buf, FORMAT_VERSION);
push_u32(&mut buf, header.base_em);
push_u64(&mut buf, header.spread.to_bits());
buf.push(header.error_correction as u8);
push_u32(&mut buf, sections.len() as u32);
for (token, glyphs) in sections {
push_u64(&mut buf, *token);
push_u32(&mut buf, glyphs.len() as u32);
for (glyph_id, weight, g) in glyphs {
push_u16(&mut buf, *glyph_id);
push_u16(&mut buf, *weight);
match g {
SnapshotGlyph::Empty { advance } => {
buf.push(0);
push_f32(&mut buf, *advance);
}
SnapshotGlyph::Outline(m) => {
buf.push(1);
push_u32(&mut buf, m.width);
push_u32(&mut buf, m.height);
push_f32(&mut buf, m.bearing_x);
push_f32(&mut buf, m.bearing_y);
push_f32(&mut buf, m.advance);
push_f32(&mut buf, m.spread);
push_u32(&mut buf, m.rgba.len() as u32);
buf.extend_from_slice(&m.rgba);
}
}
}
}
buf
}
struct Reader<'a> {
b: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
fn take(&mut self, n: usize) -> Result<&'a [u8], SnapshotError> {
let end = self.pos.checked_add(n).ok_or(SnapshotError::Truncated)?;
let s = self.b.get(self.pos..end).ok_or(SnapshotError::Truncated)?;
self.pos = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8, SnapshotError> {
Ok(self.take(1)?[0])
}
fn u16(&mut self) -> Result<u16, SnapshotError> {
Ok(u16::from_le_bytes(self.take(2)?.try_into().unwrap()))
}
fn u32(&mut self) -> Result<u32, SnapshotError> {
Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
fn u64(&mut self) -> Result<u64, SnapshotError> {
Ok(u64::from_le_bytes(self.take(8)?.try_into().unwrap()))
}
fn f32(&mut self) -> Result<f32, SnapshotError> {
Ok(f32::from_le_bytes(self.take(4)?.try_into().unwrap()))
}
}
pub fn decode_snapshot(
bytes: &[u8],
expected: SnapshotHeader,
) -> Result<Vec<SnapshotSection>, SnapshotError> {
let mut r = Reader { b: bytes, pos: 0 };
if r.take(4)? != MAGIC {
return Err(SnapshotError::BadMagic);
}
let version = r.u16()?;
if version != FORMAT_VERSION {
return Err(SnapshotError::UnsupportedVersion(version));
}
let base_em = r.u32()?;
let spread = f64::from_bits(r.u64()?);
let error_correction = r.u8()? != 0;
if base_em != expected.base_em
|| spread != expected.spread
|| error_correction != expected.error_correction
{
return Err(SnapshotError::ParamMismatch);
}
let section_count = r.u32()? as usize;
let mut sections = Vec::with_capacity(section_count);
for _ in 0..section_count {
let token = r.u64()?;
let glyph_count = r.u32()? as usize;
let mut glyphs = Vec::with_capacity(glyph_count);
for _ in 0..glyph_count {
let glyph_id = r.u16()?;
let weight = r.u16()?;
let tag = r.u8()?;
let g = match tag {
0 => SnapshotGlyph::Empty { advance: r.f32()? },
1 => {
let width = r.u32()?;
let height = r.u32()?;
let bearing_x = r.f32()?;
let bearing_y = r.f32()?;
let advance = r.f32()?;
let spread = r.f32()?;
let rgba_len = r.u32()? as usize;
let rgba = r.take(rgba_len)?.to_vec();
SnapshotGlyph::Outline(MsdfGlyph {
rgba,
width,
height,
bearing_x,
bearing_y,
advance,
spread,
})
}
_ => return Err(SnapshotError::Truncated),
};
glyphs.push((glyph_id, weight, g));
}
sections.push((token, glyphs));
}
Ok(sections)
}
pub fn font_token_hash(font_bytes: &[u8]) -> u64 {
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut h = OFFSET;
for &b in font_bytes {
h ^= b as u64;
h = h.wrapping_mul(PRIME);
}
h
}
pub fn bake_font_section(
face: &Face<'_>,
token: u64,
chars: &[char],
weight: u16,
base_em: u32,
spread: f64,
correct_error: bool,
) -> SnapshotSection {
let mut glyphs = Vec::new();
for &ch in chars {
let Some(glyph_id) = face.glyph_index(ch) else {
continue;
};
let gid = glyph_id.0;
let g = match build_glyph_msdf(face, gid, base_em, spread, correct_error) {
Some(m) => SnapshotGlyph::Outline(m),
None => SnapshotGlyph::Empty {
advance: glyph_advance(face, gid, base_em),
},
};
glyphs.push((gid, weight, g));
}
(token, glyphs)
}
#[cfg(all(test, feature = "inter"))]
mod tests {
use super::*;
fn header() -> SnapshotHeader {
SnapshotHeader {
base_em: 48,
spread: 6.0,
error_correction: false,
}
}
#[test]
fn round_trips_a_baked_section() {
let face = Face::parse(damascene_fonts::INTER_VARIABLE, 0).unwrap();
let chars: Vec<char> = "Ag @".chars().collect(); let section = bake_font_section(&face, 7, &chars, 500, 48, 6.0, false);
let bytes = encode_snapshot(header(), &[section]);
let decoded = decode_snapshot(&bytes, header()).expect("decodes");
assert_eq!(decoded.len(), 1);
let (token, glyphs) = &decoded[0];
assert_eq!(*token, 7);
assert_eq!(glyphs.len(), 4);
let empties = glyphs
.iter()
.filter(|(_, _, g)| matches!(g, SnapshotGlyph::Empty { .. }))
.count();
assert_eq!(empties, 1, "space is the one empty glyph");
for (_, weight, g) in glyphs {
assert_eq!(*weight, 500, "weight survives the round trip");
if let SnapshotGlyph::Outline(m) = g {
assert_eq!(m.rgba.len() as u32, m.width * m.height * 4);
}
}
}
#[test]
fn font_token_hash_is_stable_and_distinct() {
let a = font_token_hash(damascene_fonts::INTER_VARIABLE);
assert_eq!(
a,
font_token_hash(damascene_fonts::INTER_VARIABLE),
"deterministic"
);
assert_ne!(a, font_token_hash(b"not a font"), "distinct inputs differ");
}
#[test]
fn rejects_param_mismatch() {
let face = Face::parse(damascene_fonts::INTER_VARIABLE, 0).unwrap();
let section = bake_font_section(&face, 0, &['A'], 0, 48, 6.0, false);
let bytes = encode_snapshot(header(), &[section]);
let wrong = SnapshotHeader {
base_em: 32,
..header()
};
assert_eq!(
decode_snapshot(&bytes, wrong),
Err(SnapshotError::ParamMismatch)
);
}
#[test]
fn rejects_bad_magic_and_truncation() {
assert_eq!(
decode_snapshot(b"xxxx....", header()),
Err(SnapshotError::BadMagic)
);
let face = Face::parse(damascene_fonts::INTER_VARIABLE, 0).unwrap();
let bytes = encode_snapshot(
header(),
&[bake_font_section(&face, 0, &['A'], 0, 48, 6.0, false)],
);
assert_eq!(
decode_snapshot(&bytes[..bytes.len() - 4], header()),
Err(SnapshotError::Truncated)
);
}
}