webfont-generator 0.5.0

Generate webfonts (SVG, TTF, EOT, WOFF, WOFF2) from SVG icons
Documentation
use std::io::{Error, ErrorKind};
use std::sync::{Arc, OnceLock};

use write_fonts::read::tables::compute_checksum;

const CHECKSUM_ADJUSTMENT: u32 = 0xb1b0_afba;
const HEAD_CHECKSUM_ADJUSTMENT_OFFSET: usize = 8;
const HEAD_TAG: [u8; 4] = *b"head";
const SFNT_HEADER_SIZE: usize = 12;
const SFNT_TABLE_ENTRY_SIZE: usize = 16;
const TT_SFNT_VERSION: [u8; 4] = [0x00, 0x01, 0x00, 0x00];

#[derive(Clone)]
pub(crate) struct SerializedFontTables {
    tables: Vec<SerializedTable>,
    ttf: OnceLock<Arc<Vec<u8>>>,
}

#[derive(Clone)]
pub(crate) struct SerializedTable {
    pub tag: [u8; 4],
    pub checksum: u32,
    pub bytes: Vec<u8>,
}

impl SerializedFontTables {
    pub fn new(tables: Vec<([u8; 4], Vec<u8>)>) -> Result<Self, Error> {
        if tables.len() > u16::MAX as usize {
            return Err(Error::new(ErrorKind::InvalidInput, "Too many SFNT tables."));
        }
        let mut tables = tables;
        tables.sort_unstable_by_key(|(tag, _)| table_order_key(tag));
        let mut tables = tables
            .into_iter()
            .map(|(tag, mut bytes)| {
                if tag == HEAD_TAG && bytes.len() >= HEAD_CHECKSUM_ADJUSTMENT_OFFSET + 4 {
                    bytes[HEAD_CHECKSUM_ADJUSTMENT_OFFSET..HEAD_CHECKSUM_ADJUSTMENT_OFFSET + 4]
                        .copy_from_slice(&[0, 0, 0, 0]);
                }
                SerializedTable {
                    tag,
                    checksum: compute_checksum(&bytes),
                    bytes,
                }
            })
            .collect::<Vec<_>>();
        apply_checksum_adjustment(&mut tables);
        let ttf = OnceLock::new();
        Ok(Self { tables, ttf })
    }

    pub fn ttf(&self) -> &[u8] {
        self.ttf
            .get_or_init(|| Arc::new(build_sfnt(&self.tables)))
            .as_slice()
    }

    pub fn ttf_arc(&self) -> Arc<Vec<u8>> {
        Arc::clone(self.ttf.get_or_init(|| Arc::new(build_sfnt(&self.tables))))
    }

    pub fn tables(&self) -> &[SerializedTable] {
        &self.tables
    }

    #[cfg(feature = "bench")]
    pub(crate) fn uncached_ttf(&self) -> Vec<u8> {
        build_sfnt(&self.tables)
    }

    #[cfg(feature = "bench")]
    pub(crate) fn clone_raw_tables(&self) -> Vec<([u8; 4], Vec<u8>)> {
        self.tables
            .iter()
            .map(|table| (table.tag, table.bytes.clone()))
            .collect()
    }
}

fn table_order_key(tag: &[u8; 4]) -> (u8, usize, [u8; 4]) {
    // Recommended TTF table order (OpenType spec). DSIG sorts last, unknown
    // tables sort after the recommended set but before DSIG.
    let recommended = match tag {
        b"head" => 0,
        b"hhea" => 1,
        b"maxp" => 2,
        b"OS/2" => 3,
        b"hmtx" => 4,
        b"LTSH" => 5,
        b"VDMX" => 6,
        b"hdmx" => 7,
        b"cmap" => 8,
        b"fpgm" => 9,
        b"prep" => 10,
        b"cvt " => 11,
        b"loca" => 12,
        b"glyf" => 13,
        b"kern" => 14,
        b"name" => 15,
        b"post" => 16,
        b"gasp" => 17,
        b"PCLT" => 18,
        b"DSIG" => return (2, 0, *tag),
        _ => return (1, 0, *tag),
    };
    (0, recommended, *tag)
}

fn apply_checksum_adjustment(tables: &mut [SerializedTable]) {
    let checksum_adjustment = checksum_adjustment(tables);
    if let Some(head) = tables.iter_mut().find(|table| table.tag == HEAD_TAG)
        && head.bytes.len() >= HEAD_CHECKSUM_ADJUSTMENT_OFFSET + 4
    {
        head.bytes[HEAD_CHECKSUM_ADJUSTMENT_OFFSET..HEAD_CHECKSUM_ADJUSTMENT_OFFSET + 4]
            .copy_from_slice(&checksum_adjustment.to_be_bytes());
    }
}

fn checksum_adjustment(tables: &[SerializedTable]) -> u32 {
    let directory_checksum = compute_checksum(&sfnt_directory(tables));
    let table_checksum = tables
        .iter()
        .map(|table| table.checksum)
        .fold(0_u32, u32::wrapping_add);
    CHECKSUM_ADJUSTMENT.wrapping_sub(table_checksum.wrapping_add(directory_checksum))
}

fn build_sfnt(tables: &[SerializedTable]) -> Vec<u8> {
    let mut bytes = sfnt_directory(tables);
    bytes.reserve(
        tables
            .iter()
            .map(|table| align4_len(table.bytes.len()))
            .sum(),
    );
    for table in tables {
        bytes.extend_from_slice(&table.bytes);
        align4(&mut bytes);
    }
    bytes
}

fn sfnt_directory(tables: &[SerializedTable]) -> Vec<u8> {
    let mut offset = SFNT_HEADER_SIZE + tables.len() * SFNT_TABLE_ENTRY_SIZE;
    let mut records = Vec::with_capacity(tables.len());
    for table in tables {
        records.push((table.tag, table.checksum, offset, table.bytes.len()));
        offset += align4_len(table.bytes.len());
    }
    records.sort_unstable_by_key(|record| record.0);

    let mut directory =
        Vec::with_capacity(SFNT_HEADER_SIZE + records.len() * SFNT_TABLE_ENTRY_SIZE);
    directory.extend_from_slice(&TT_SFNT_VERSION);
    write_u16_be(&mut directory, tables.len() as u16);
    let (search_range, entry_selector, range_shift) =
        search_range(tables.len(), SFNT_TABLE_ENTRY_SIZE);
    write_u16_be(&mut directory, search_range);
    write_u16_be(&mut directory, entry_selector);
    write_u16_be(&mut directory, range_shift);
    for (tag, checksum, offset, length) in records {
        directory.extend_from_slice(&tag);
        write_u32_be(&mut directory, checksum);
        write_u32_be(&mut directory, offset as u32);
        write_u32_be(&mut directory, length as u32);
    }
    directory
}

fn search_range(item_count: usize, item_size: usize) -> (u16, u16, u16) {
    if item_count == 0 || item_size == 0 {
        return (0, 0, 0);
    }

    let entry_selector = usize::BITS as usize - 1 - item_count.leading_zeros() as usize;
    let search_range = (1_usize << entry_selector) * item_size;
    let range_shift = item_count * item_size - search_range;
    (
        search_range as u16,
        entry_selector as u16,
        range_shift as u16,
    )
}

fn align4(bytes: &mut Vec<u8>) {
    bytes.resize(align4_len(bytes.len()), 0);
}

fn align4_len(len: usize) -> usize {
    (len + 3) & !3
}

fn write_u16_be(output: &mut Vec<u8>, value: u16) {
    output.extend_from_slice(&value.to_be_bytes());
}

fn write_u32_be(output: &mut Vec<u8>, value: u32) {
    output.extend_from_slice(&value.to_be_bytes());
}