pub mod atlas;
pub mod header;
use alloc::vec::Vec;
pub use crate::font::atlas::{AtlasHeader, GlyphMetric, HEADER_LEN, METRIC_LEN, SUPPORTED_VERSION};
pub use crate::font::atlas::{read_header, read_metric, write_header, write_metric};
pub use crate::font::header::{FONT_CHUNK_HEADER_LEN, FontChunkHeader, FontChunkKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FontDecodeError {
PayloadTooShort,
UnknownChunkKind(u8),
UnsupportedVersion(u16),
InvalidBitDepth(u8),
InvalidGeometry,
OffsetsOutOfBounds,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Font {
pub chunk_header: FontChunkHeader,
pub atlas: AtlasHeader,
pub metrics: Vec<GlyphMetric>,
pub data: Vec<u8>,
}
impl Font {
pub fn decode(payload: &[u8]) -> Result<Self, FontDecodeError> {
let prefix = FontChunkHeader::parse(payload).ok_or(FontDecodeError::PayloadTooShort)?;
let body = payload
.get(FONT_CHUNK_HEADER_LEN..)
.ok_or(FontDecodeError::PayloadTooShort)?;
if body.len() < HEADER_LEN {
return Err(FontDecodeError::PayloadTooShort);
}
let atlas = read_header(&body[..HEADER_LEN]);
if atlas.version != SUPPORTED_VERSION {
return Err(FontDecodeError::UnsupportedVersion(atlas.version));
}
match prefix.kind {
FontChunkKind::Sdf => {
if atlas.bit_depth != 4 && atlas.bit_depth != 8 {
return Err(FontDecodeError::InvalidBitDepth(atlas.bit_depth));
}
}
FontChunkKind::Grayscale => {
if !matches!(atlas.bit_depth, 1 | 2 | 4 | 8) {
return Err(FontDecodeError::InvalidBitDepth(atlas.bit_depth));
}
}
}
let source = atlas.source_size as u32;
if source == 0 {
return Err(FontDecodeError::InvalidGeometry);
}
let expected_per_glyph = (source * source * atlas.bit_depth as u32).div_ceil(8);
if atlas.bytes_per_glyph != expected_per_glyph {
return Err(FontDecodeError::InvalidGeometry);
}
let metric_off = atlas.metric_offset as usize;
let data_off = atlas.data_offset as usize;
let metric_end = metric_off
.checked_add(
(atlas.glyph_count as usize)
.checked_mul(METRIC_LEN)
.ok_or(FontDecodeError::OffsetsOutOfBounds)?,
)
.ok_or(FontDecodeError::OffsetsOutOfBounds)?;
let data_end = data_off
.checked_add(
(atlas.glyph_count as usize)
.checked_mul(atlas.bytes_per_glyph as usize)
.ok_or(FontDecodeError::OffsetsOutOfBounds)?,
)
.ok_or(FontDecodeError::OffsetsOutOfBounds)?;
if metric_end > body.len() || data_end > body.len() {
return Err(FontDecodeError::OffsetsOutOfBounds);
}
if metric_off < HEADER_LEN || data_off < HEADER_LEN {
return Err(FontDecodeError::OffsetsOutOfBounds);
}
let mut metrics = Vec::with_capacity(atlas.glyph_count as usize);
for i in 0..atlas.glyph_count as usize {
let off = metric_off + i * METRIC_LEN;
metrics.push(read_metric(&body[off..off + METRIC_LEN]));
}
let data = body[data_off..data_end].to_vec();
Ok(Self {
chunk_header: prefix,
atlas,
metrics,
data,
})
}
pub fn encode(&self) -> Vec<u8> {
let metric_offset = HEADER_LEN as u32;
let data_offset = metric_offset + (self.metrics.len() * METRIC_LEN) as u32;
let total = FONT_CHUNK_HEADER_LEN + data_offset as usize + self.data.len();
let mut out = alloc::vec![0u8; total];
let mut prefix_buf = [0u8; FONT_CHUNK_HEADER_LEN];
self.chunk_header.write(&mut prefix_buf);
out[..FONT_CHUNK_HEADER_LEN].copy_from_slice(&prefix_buf);
let body_start = FONT_CHUNK_HEADER_LEN;
let mut atlas = self.atlas;
atlas.metric_offset = metric_offset;
atlas.data_offset = data_offset;
write_header(&mut out[body_start..body_start + HEADER_LEN], &atlas);
for (i, m) in self.metrics.iter().enumerate() {
let off = body_start + metric_offset as usize + i * METRIC_LEN;
write_metric(&mut out[off..off + METRIC_LEN], m);
}
out[body_start + data_offset as usize..].copy_from_slice(&self.data);
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_font(kind: FontChunkKind, bit_depth: u8) -> Font {
let source_size: u16 = 4;
let bytes_per_glyph = (source_size as u32 * source_size as u32 * bit_depth as u32) / 8;
let atlas = AtlasHeader {
version: SUPPORTED_VERSION,
bit_depth,
_pad0: 0,
source_size,
spread: 2,
glyph_count: 2,
metric_offset: HEADER_LEN as u32,
data_offset: (HEADER_LEN + 2 * METRIC_LEN) as u32,
bytes_per_glyph,
ascender: 3,
descender: 1,
line_height: 4,
_pad1: 0,
};
let metrics = alloc::vec![
GlyphMetric {
codepoint: 'A' as u32,
advance: 4,
bearing_x: 0,
bearing_y: 3
},
GlyphMetric {
codepoint: 'B' as u32,
advance: 4,
bearing_x: 0,
bearing_y: 3
},
];
let data = alloc::vec![0u8; (bytes_per_glyph * 2) as usize];
Font {
chunk_header: FontChunkHeader {
kind,
format: bit_depth,
size: source_size,
},
atlas,
metrics,
data,
}
}
#[test]
fn sdf_round_trips() {
let font = sample_font(FontChunkKind::Sdf, 4);
let bytes = font.encode();
let back = Font::decode(&bytes).unwrap();
assert_eq!(back, font);
}
#[test]
fn gray_round_trips() {
let font = sample_font(FontChunkKind::Grayscale, 8);
let bytes = font.encode();
let back = Font::decode(&bytes).unwrap();
assert_eq!(back, font);
}
#[test]
fn rejects_unknown_kind() {
let mut font = sample_font(FontChunkKind::Sdf, 4);
font.chunk_header.kind = FontChunkKind::Grayscale;
font.chunk_header.kind = FontChunkKind::from_u8(9).unwrap_or(FontChunkKind::Sdf);
let bytes = font.encode();
let _ = Font::decode(&bytes);
}
#[test]
fn rejects_short_payload() {
assert!(matches!(
Font::decode(&[0, 0, 0, 0]),
Err(FontDecodeError::PayloadTooShort)
));
}
}