use alloc::rc::Rc;
use alloc::vec::Vec;
use super::chunk::{FontChunkHeader, FontChunkKind};
use super::sdf::{AtlasHeader, GlyphMetric, HEADER_LEN, METRIC_LEN, SUPPORTED_VERSION};
use super::{Font, FontBackend, FontMetrics, FontProvider, Glyph, GlyphKind};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrayFontError {
PayloadTooShort,
NotGrayscale,
UnsupportedVersion(u16),
InvalidBitDepth(u8),
InvalidGeometry,
OffsetsOutOfBounds,
}
#[derive(Clone, Debug)]
pub struct GrayFontProvider {
header: AtlasHeader,
metrics: Vec<GlyphMetric>,
data: &'static [u8],
}
impl GrayFontProvider {
pub fn from_mirx_chunk(payload: &'static [u8]) -> Result<Self, GrayFontError> {
let prefix = FontChunkHeader::parse(payload).ok_or(GrayFontError::PayloadTooShort)?;
if prefix.kind != FontChunkKind::Grayscale {
return Err(GrayFontError::NotGrayscale);
}
let body = &payload[mirx::FONT_CHUNK_HEADER_LEN..];
if body.len() < HEADER_LEN {
return Err(GrayFontError::PayloadTooShort);
}
let header = mirx::read_header(&body[..HEADER_LEN]);
if header.version != SUPPORTED_VERSION {
return Err(GrayFontError::UnsupportedVersion(header.version));
}
if !matches!(header.bit_depth, 1 | 2 | 4 | 8) {
return Err(GrayFontError::InvalidBitDepth(header.bit_depth));
}
let source = header.source_size as u32;
if source == 0 {
return Err(GrayFontError::InvalidGeometry);
}
let expected_per_glyph = (source * source * header.bit_depth as u32).div_ceil(8);
if header.bytes_per_glyph != expected_per_glyph {
return Err(GrayFontError::InvalidGeometry);
}
let metric_off = header.metric_offset as usize;
let data_off = header.data_offset as usize;
let metric_end = metric_off
.checked_add(
(header.glyph_count as usize)
.checked_mul(METRIC_LEN)
.ok_or(GrayFontError::OffsetsOutOfBounds)?,
)
.ok_or(GrayFontError::OffsetsOutOfBounds)?;
let data_end = data_off
.checked_add(
(header.glyph_count as usize)
.checked_mul(header.bytes_per_glyph as usize)
.ok_or(GrayFontError::OffsetsOutOfBounds)?,
)
.ok_or(GrayFontError::OffsetsOutOfBounds)?;
if metric_end > body.len() || data_end > body.len() {
return Err(GrayFontError::OffsetsOutOfBounds);
}
if metric_off < HEADER_LEN || data_off < HEADER_LEN {
return Err(GrayFontError::OffsetsOutOfBounds);
}
let mut metrics = Vec::with_capacity(header.glyph_count as usize);
for i in 0..header.glyph_count as usize {
let off = metric_off + i * METRIC_LEN;
metrics.push(mirx::read_metric(&body[off..off + METRIC_LEN]));
}
let data = &body[data_off..data_end];
Ok(Self {
header,
metrics,
data,
})
}
pub fn header(&self) -> &AtlasHeader {
&self.header
}
}
impl FontProvider for GrayFontProvider {
fn glyph(&self, ch: char, _requested_size: u16) -> Option<Glyph> {
let cp = ch as u32;
let idx = self
.metrics
.binary_search_by_key(&cp, |m| m.codepoint)
.ok()?;
let m = self.metrics[idx];
let stride = self.header.bytes_per_glyph as usize;
let start = idx * stride;
let coverage = &self.data[start..start + stride];
let cell = self.header.source_size.min(255) as u8;
Some(Glyph {
advance: m.advance,
kind: GlyphKind::Grayscale {
coverage,
bpp: self.header.bit_depth,
w: cell,
h: cell,
bearing_x: m.bearing_x,
bearing_y: m.bearing_y,
},
})
}
fn metrics(&self) -> FontMetrics {
FontMetrics {
ascender: self.header.ascender,
descender: self.header.descender,
line_height: self.header.line_height,
}
}
}
pub fn font_from_mirx_chunk(
family: &'static str,
payload: &'static [u8],
) -> Result<Font, GrayFontError> {
let provider = GrayFontProvider::from_mirx_chunk(payload)?;
let size = provider.header.source_size;
Ok(Font {
family,
size,
backend: FontBackend::Custom(Rc::new(provider)),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::font::chunk::FONT_CHUNK_HEADER_LEN;
use alloc::vec;
fn make_gray_atlas(glyphs: &[(u32, u16, i8, i8)]) -> Vec<u8> {
let source_size: u16 = 4;
let bit_depth: u8 = 4;
let bytes_per_glyph =
(source_size as u32 * source_size as u32 * bit_depth as u32).div_ceil(8);
let glyph_count = glyphs.len() as u32;
let metric_offset = HEADER_LEN as u32;
let data_offset = metric_offset + glyph_count * METRIC_LEN as u32;
let body_len = data_offset as usize + (glyph_count as usize) * bytes_per_glyph as usize;
let mut body = vec![0u8; body_len];
let header = AtlasHeader {
version: SUPPORTED_VERSION,
bit_depth,
_pad0: 0,
source_size,
spread: 0,
glyph_count,
metric_offset,
data_offset,
bytes_per_glyph,
ascender: 3,
descender: 1,
line_height: 4,
_pad1: 0,
};
unsafe {
core::ptr::write_unaligned(body.as_mut_ptr() as *mut AtlasHeader, header);
}
let mut sorted = glyphs.to_vec();
sorted.sort_by_key(|g| g.0);
for (i, (cp, advance, bx, by)) in sorted.iter().enumerate() {
let off = metric_offset as usize + i * METRIC_LEN;
let m = GlyphMetric {
codepoint: *cp,
advance: *advance,
bearing_x: *bx,
bearing_y: *by,
};
unsafe {
core::ptr::write_unaligned(body.as_mut_ptr().add(off) as *mut GlyphMetric, m);
}
}
for i in 0..glyph_count as usize {
let start = data_offset as usize + i * bytes_per_glyph as usize;
for b in &mut body[start..start + bytes_per_glyph as usize] {
*b = ((i + 1) as u8).wrapping_mul(0x11);
}
}
let mut payload = vec![0u8; FONT_CHUNK_HEADER_LEN + body.len()];
FontChunkHeader {
kind: FontChunkKind::Grayscale,
format: bit_depth,
size: source_size,
}
.write(&mut payload[..FONT_CHUNK_HEADER_LEN]);
payload[FONT_CHUNK_HEADER_LEN..].copy_from_slice(&body);
payload
}
fn leak(bytes: Vec<u8>) -> &'static [u8] {
Vec::leak(bytes)
}
#[test]
fn parses_prefixed_grayscale_and_resolves_glyph() {
let bytes = leak(make_gray_atlas(&[
(b'A' as u32, 5, 1, 2),
(b'B' as u32, 6, -1, 0),
]));
let p = GrayFontProvider::from_mirx_chunk(bytes).expect("parse");
assert_eq!(p.header().glyph_count, 2);
let g = p.glyph('A', 16).expect("A");
assert_eq!(g.advance, 5);
match g.kind {
GlyphKind::Grayscale {
bpp,
w,
h,
coverage,
bearing_x,
bearing_y,
} => {
assert_eq!(bpp, 4);
assert_eq!((w, h), (4, 4));
assert_eq!(coverage.len(), 8);
assert_eq!((bearing_x, bearing_y), (1, 2));
}
other => panic!("expected Grayscale, got {other:?}"),
}
}
#[test]
fn missing_codepoint_returns_none() {
let bytes = leak(make_gray_atlas(&[(b'A' as u32, 5, 0, 0)]));
let p = GrayFontProvider::from_mirx_chunk(bytes).expect("parse");
assert!(p.glyph('Z', 16).is_none());
}
#[test]
fn rejects_sdf_prefix() {
let mut bytes = make_gray_atlas(&[(b'A' as u32, 5, 0, 0)]);
bytes[0] = FontChunkKind::Sdf.to_u8();
assert!(matches!(
GrayFontProvider::from_mirx_chunk(leak(bytes)),
Err(GrayFontError::NotGrayscale)
));
}
#[test]
fn rejects_short_payload() {
assert!(matches!(
GrayFontProvider::from_mirx_chunk(leak(vec![0u8; 2])),
Err(GrayFontError::PayloadTooShort)
));
}
#[test]
fn font_wrapper_reports_source_size() {
let bytes = leak(make_gray_atlas(&[(b'A' as u32, 5, 0, 0)]));
let font = font_from_mirx_chunk("gray-test", bytes).expect("font");
assert_eq!(font.family, "gray-test");
assert_eq!(font.size, 4);
}
}