mirx 0.45.0

MIRX binary image format — flat & chunk-based container for ahead-of-time UI assets
Documentation
use core::{iter::FusedIterator, slice::ChunksExact};

use crate::wire::{read_u16_le, read_u32_le};

pub(in crate::font) const CMAP_INDEX_RECORD_LEN: usize = 6;
pub(in crate::font) const GLYPH_ID_RECORD_LEN: usize = 2;

#[repr(transparent)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct GlyphId(u16);

impl GlyphId {
    pub const NOTDEF: Self = Self(0);

    pub const fn new(value: u16) -> Self {
        Self(value)
    }

    pub const fn get(self) -> u16 {
        self.0
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CmapEntry {
    scalar: char,
    glyph_id: GlyphId,
}

impl CmapEntry {
    pub const fn new(scalar: char, glyph_id: GlyphId) -> Self {
        Self { scalar, glyph_id }
    }

    pub const fn scalar(self) -> char {
        self.scalar
    }

    pub const fn glyph_id(self) -> GlyphId {
        self.glyph_id
    }

    pub(in crate::font) fn encode_record(self) -> [u8; CMAP_INDEX_RECORD_LEN] {
        let mut record = [0; CMAP_INDEX_RECORD_LEN];
        record[..4].copy_from_slice(&(self.scalar as u32).to_le_bytes());
        record[4..].copy_from_slice(&self.glyph_id.get().to_le_bytes());
        record
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CmapIndex<'a> {
    bytes: &'a [u8],
}

impl<'a> CmapIndex<'a> {
    pub fn open(bytes: &'a [u8]) -> Result<Self, CmapIndexError> {
        if bytes.len() % CMAP_INDEX_RECORD_LEN != 0 {
            return Err(CmapIndexError::PartialRecord {
                byte_len: bytes.len(),
            });
        }
        let mut previous = None;
        for (index, record) in bytes.chunks_exact(CMAP_INDEX_RECORD_LEN).enumerate() {
            let value = read_u32_le(record, 0).expect("complete cmap record");
            let scalar =
                char::from_u32(value).ok_or(CmapIndexError::InvalidScalar { index, value })?;
            if let Some(previous) = previous
                && scalar <= previous
            {
                return Err(CmapIndexError::NotSorted {
                    index,
                    previous,
                    current: scalar,
                });
            }
            previous = Some(scalar);
        }
        Ok(Self { bytes })
    }

    pub const fn as_bytes(self) -> &'a [u8] {
        self.bytes
    }

    pub const fn len(self) -> usize {
        self.bytes.len() / CMAP_INDEX_RECORD_LEN
    }

    pub const fn is_empty(self) -> bool {
        self.bytes.is_empty()
    }

    pub fn get(self, index: usize) -> Option<CmapEntry> {
        let offset = index.checked_mul(CMAP_INDEX_RECORD_LEN)?;
        let record = self.bytes.get(offset..offset + CMAP_INDEX_RECORD_LEN)?;
        Some(CmapEntry {
            scalar: char::from_u32(read_u32_le(record, 0)?).expect("validated cmap scalar"),
            glyph_id: GlyphId::new(read_u16_le(record, 4)?),
        })
    }

    pub fn lookup(self, scalar: char) -> Option<GlyphId> {
        let mut start = 0;
        let mut end = self.len();
        while start < end {
            let middle = start + (end - start) / 2;
            let entry = self.get(middle).expect("validated cmap index");
            match entry.scalar.cmp(&scalar) {
                core::cmp::Ordering::Less => start = middle + 1,
                core::cmp::Ordering::Greater => end = middle,
                core::cmp::Ordering::Equal => return Some(entry.glyph_id),
            }
        }
        None
    }

    pub fn iter(self) -> CmapIter<'a> {
        CmapIter {
            records: self.bytes.chunks_exact(CMAP_INDEX_RECORD_LEN),
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum CmapIndexError {
    PartialRecord {
        byte_len: usize,
    },
    InvalidScalar {
        index: usize,
        value: u32,
    },
    NotSorted {
        index: usize,
        previous: char,
        current: char,
    },
}

#[derive(Clone, Debug)]
pub struct CmapIter<'a> {
    records: ChunksExact<'a, u8>,
}

impl Iterator for CmapIter<'_> {
    type Item = CmapEntry;

    fn next(&mut self) -> Option<Self::Item> {
        self.records.next().map(cmap_entry)
    }

    fn nth(&mut self, n: usize) -> Option<Self::Item> {
        self.records.nth(n).map(cmap_entry)
    }

    fn size_hint(&self) -> (usize, Option<usize>) {
        self.records.size_hint()
    }

    fn count(self) -> usize {
        self.records.len()
    }
}

impl DoubleEndedIterator for CmapIter<'_> {
    fn next_back(&mut self) -> Option<Self::Item> {
        self.records.next_back().map(cmap_entry)
    }
}

impl ExactSizeIterator for CmapIter<'_> {
    fn len(&self) -> usize {
        self.records.len()
    }
}

impl FusedIterator for CmapIter<'_> {}

fn cmap_entry(record: &[u8]) -> CmapEntry {
    CmapEntry {
        scalar: char::from_u32(read_u32_le(record, 0).expect("complete cmap record"))
            .expect("validated cmap scalar"),
        glyph_id: GlyphId::new(read_u16_le(record, 4).expect("complete cmap record")),
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GlyphIds<'a> {
    bytes: &'a [u8],
}

impl<'a> GlyphIds<'a> {
    pub fn open(bytes: &'a [u8]) -> Result<Self, GlyphIdsError> {
        if bytes.len() % GLYPH_ID_RECORD_LEN != 0 {
            return Err(GlyphIdsError::PartialRecord {
                byte_len: bytes.len(),
            });
        }
        let mut previous = None;
        for (index, record) in bytes.chunks_exact(GLYPH_ID_RECORD_LEN).enumerate() {
            let current = read_u16_le(record, 0).expect("complete glyph ID record");
            if let Some(previous) = previous
                && current <= previous
            {
                return Err(GlyphIdsError::NotSorted {
                    index,
                    previous: GlyphId::new(previous),
                    current: GlyphId::new(current),
                });
            }
            previous = Some(current);
        }
        Ok(Self { bytes })
    }

    pub const fn as_bytes(self) -> &'a [u8] {
        self.bytes
    }

    pub const fn len(self) -> usize {
        self.bytes.len() / GLYPH_ID_RECORD_LEN
    }

    pub const fn is_empty(self) -> bool {
        self.bytes.is_empty()
    }

    pub fn get(self, ordinal: usize) -> Option<GlyphId> {
        let offset = ordinal.checked_mul(GLYPH_ID_RECORD_LEN)?;
        read_u16_le(self.bytes, offset).map(GlyphId::new)
    }

    pub fn ordinal(self, glyph_id: GlyphId) -> Result<usize, usize> {
        let mut start = 0;
        let mut end = self.len();
        while start < end {
            let middle = start + (end - start) / 2;
            match self
                .get(middle)
                .expect("validated glyph ID index")
                .cmp(&glyph_id)
            {
                core::cmp::Ordering::Less => start = middle + 1,
                core::cmp::Ordering::Greater => end = middle,
                core::cmp::Ordering::Equal => return Ok(middle),
            }
        }
        Err(start)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum GlyphIdsError {
    PartialRecord {
        byte_len: usize,
    },
    NotSorted {
        index: usize,
        previous: GlyphId,
        current: GlyphId,
    },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cmap_maps_sorted_scalars_to_repeating_glyphs() {
        let mut bytes = [0; 3 * CMAP_INDEX_RECORD_LEN];
        for (record, (scalar, glyph)) in bytes.chunks_exact_mut(CMAP_INDEX_RECORD_LEN).zip([
            ('A', 7_u16),
            ('B', 7),
            ('\u{10ffff}', 23),
        ]) {
            record[..4].copy_from_slice(&(scalar as u32).to_le_bytes());
            record[4..].copy_from_slice(&glyph.to_le_bytes());
        }
        let cmap = CmapIndex::open(&bytes).unwrap();
        assert_eq!(cmap.lookup('A'), Some(GlyphId::new(7)));
        assert_eq!(cmap.lookup('C'), None);
        assert_eq!(cmap.get(2).unwrap().scalar(), '\u{10ffff}');
        assert_eq!(cmap.iter().count(), 3);

        let output = CmapEntry::new('中', GlyphId::new(42)).encode_record();
        assert_eq!(&output[..4], &('中' as u32).to_le_bytes());
        assert_eq!(&output[4..6], &42_u16.to_le_bytes());
    }

    #[test]
    fn cmap_rejects_partial_invalid_and_unsorted_records() {
        assert_eq!(
            CmapIndex::open(&[0; 5]),
            Err(CmapIndexError::PartialRecord { byte_len: 5 })
        );
        let mut invalid = [0; CMAP_INDEX_RECORD_LEN];
        invalid[..4].copy_from_slice(&0xd800_u32.to_le_bytes());
        assert_eq!(
            CmapIndex::open(&invalid),
            Err(CmapIndexError::InvalidScalar {
                index: 0,
                value: 0xd800
            })
        );
        let mut duplicate = [0; 2 * CMAP_INDEX_RECORD_LEN];
        for record in duplicate.chunks_exact_mut(CMAP_INDEX_RECORD_LEN) {
            record[..4].copy_from_slice(&('A' as u32).to_le_bytes());
        }
        assert_eq!(
            CmapIndex::open(&duplicate),
            Err(CmapIndexError::NotSorted {
                index: 1,
                previous: 'A',
                current: 'A'
            })
        );
    }

    #[test]
    fn sparse_glyph_ids_map_between_ordinals_and_shaping_ids() {
        let mut bytes = [0; 6];
        for (record, glyph) in bytes.chunks_exact_mut(2).zip([0_u16, 9, u16::MAX]) {
            record.copy_from_slice(&glyph.to_le_bytes());
        }
        let ids = GlyphIds::open(&bytes).unwrap();
        assert_eq!(ids.get(1), Some(GlyphId::new(9)));
        assert_eq!(ids.ordinal(GlyphId::new(9)), Ok(1));
        assert_eq!(ids.ordinal(GlyphId::new(8)), Err(1));
        assert_eq!(ids.get(usize::MAX), None);
    }

    #[test]
    fn sparse_glyph_ids_reject_partial_duplicate_and_descending_records() {
        assert_eq!(
            GlyphIds::open(&[0]),
            Err(GlyphIdsError::PartialRecord { byte_len: 1 })
        );
        for values in [[2_u16, 2_u16], [2, 1]] {
            let mut bytes = [0; 4];
            bytes[..2].copy_from_slice(&values[0].to_le_bytes());
            bytes[2..].copy_from_slice(&values[1].to_le_bytes());
            assert!(matches!(
                GlyphIds::open(&bytes),
                Err(GlyphIdsError::NotSorted { index: 1, .. })
            ));
        }
    }
}