webfont-generator 0.8.0

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

use write_fonts::read::tables::compute_checksum;
use write_fonts::read::tables::head::Head;
use write_fonts::read::tables::maxp::Maxp;
use write_fonts::read::{FontData, FontRead};

use crate::byte_helpers::BigEndian;
use crate::sfnt::{SerializedFontTables, SerializedTable};

use super::Woff2TransformCache;
use super::glyf::{
    GLYF_ENCODER_VERSION, NormalizedGlyfLoca, invalid_data, transform_cache_key,
    transform_glyf_loca,
};
use super::serialize::write_directory_entry;

pub(super) struct PreparedWoff2 {
    pub(super) directory: Vec<u8>,
    pub(super) stream: Vec<u8>,
    pub(super) table_count: u16,
    pub(super) total_sfnt_size: u32,
}

pub(super) fn prepare(
    tables: &SerializedFontTables,
    mut cache: Option<&mut Woff2TransformCache>,
) -> Result<PreparedWoff2, Error> {
    let mut ordered = Vec::with_capacity(tables.tables().len());
    let mut glyf = None;
    let mut loca = None;
    let mut head = None;
    let mut maxp = None;
    for table in tables.tables() {
        let required = match &table.tag {
            b"glyf" => Some(&mut glyf),
            b"loca" => Some(&mut loca),
            b"head" => Some(&mut head),
            b"maxp" => Some(&mut maxp),
            b"DSIG" => continue,
            _ => None,
        };
        if let Some(required) = required
            && required.replace(table).is_some()
        {
            return Err(invalid_data("duplicate required WOFF2 table"));
        }
        ordered.push(table);
    }
    if ordered.is_empty() {
        return Err(Error::new(ErrorKind::InvalidInput, "WOFF2 requires tables"));
    }
    let (Some(glyf), Some(loca), Some(head), Some(maxp)) = (glyf, loca, head, maxp) else {
        return Err(invalid_data(
            "transformed WOFF2 requires head, maxp, glyf, and loca",
        ));
    };
    ordered.sort_unstable_by_key(|table| table.tag);
    move_loca_after_glyf(&mut ordered);
    let head_table =
        Head::read(FontData::new(&head.bytes)).map_err(|_| invalid_data("invalid head table"))?;
    let maxp_table =
        Maxp::read(FontData::new(&maxp.bytes)).map_err(|_| invalid_data("invalid maxp table"))?;
    let index_format = head_table.index_to_loc_format();
    if !matches!(index_format, 0 | 1) {
        return Err(invalid_data("head indexToLocFormat must be 0 or 1"));
    }
    let num_glyphs = maxp_table.num_glyphs();
    let expected_loca_len = (usize::from(num_glyphs) + 1) * if index_format == 0 { 2 } else { 4 };
    if loca.bytes.len() != expected_loca_len {
        return Err(invalid_data("maxp numGlyphs does not match loca length"));
    }

    let key = transform_cache_key(
        *b"glyf",
        GLYF_ENCODER_VERSION,
        &[
            b"woff2-glyf-loca",
            &glyf.bytes,
            &loca.bytes,
            &index_format.to_be_bytes(),
            &num_glyphs.to_be_bytes(),
        ],
    );
    let payload = if let Some(hit) = cache.as_deref().and_then(|cache| cache.transformed(&key)) {
        hit
    } else {
        let payload = transform_glyf_loca(glyf, loca, index_format, num_glyphs)?;
        if let Some(cache) = cache.as_deref_mut() {
            cache.insert(key, payload.clone());
        }
        payload
    };
    if let Some(cache) = cache {
        cache.retain(&HashSet::from([key]));
    }

    let normalized = NormalizedGlyfLoca {
        glyf_len: payload.normalized_glyf_len,
        glyf_checksum: payload.normalized_glyf_checksum,
        loca_format: payload.normalized_loca_format,
        loca_len: payload.normalized_loca_len,
        loca_checksum: payload.normalized_loca_checksum,
    };
    let normalized_head = normalized_head(&ordered, &normalized)?;
    let table_count =
        u16::try_from(ordered.len()).map_err(|_| invalid_data("WOFF2 table count exceeds u16"))?;
    let stream_capacity = ordered.iter().try_fold(0_usize, |length, table| {
        let table_length = match &table.tag {
            b"glyf" => payload.transformed.len(),
            b"loca" => 0,
            b"head" => normalized_head.len(),
            _ => table.bytes.len(),
        };
        length
            .checked_add(table_length)
            .ok_or_else(|| invalid_data("WOFF2 stream size overflow"))
    })?;
    let mut directory = Vec::with_capacity(usize::from(table_count) * 11);
    let mut stream = Vec::with_capacity(stream_capacity);
    for table in &ordered {
        write_directory_entry(
            &mut directory,
            table,
            payload.transformed.len(),
            normalized.glyf_len,
            normalized.loca_len,
        )?;
        match &table.tag {
            b"glyf" => stream.extend_from_slice(&payload.transformed),
            b"loca" => {}
            b"head" => stream.extend_from_slice(&normalized_head),
            _ => stream.extend_from_slice(&table.bytes),
        }
    }

    Ok(PreparedWoff2 {
        directory,
        stream,
        table_count,
        total_sfnt_size: total_sfnt_size(&ordered, &normalized)?,
    })
}

fn total_sfnt_size(
    tables: &[&SerializedTable],
    normalized: &NormalizedGlyfLoca,
) -> Result<u32, Error> {
    12_usize
        .checked_add(16 * tables.len())
        .and_then(|size| {
            tables.iter().try_fold(size, |size, table| {
                let length = match &table.tag {
                    b"glyf" => normalized.glyf_len,
                    b"loca" => normalized.loca_len,
                    _ => table.bytes.len(),
                };
                size.checked_add((length + 3) & !3)
            })
        })
        .and_then(|size| u32::try_from(size).ok())
        .ok_or_else(|| invalid_data("SFNT size exceeds u32"))
}

fn normalized_head(
    tables: &[&SerializedTable],
    normalized: &NormalizedGlyfLoca,
) -> Result<Vec<u8>, Error> {
    let head = tables
        .iter()
        .find(|table| table.tag == *b"head")
        .ok_or_else(|| invalid_data("WOFF2 requires a head table"))?;
    if head.bytes.len() < 52 {
        return Err(invalid_data("head table is too short"));
    }

    let mut bytes = head.bytes.clone();
    bytes[8..12].fill(0);
    let flags = u16::from_be_bytes(bytes[16..18].try_into().unwrap()) | (1 << 11);
    let mut writer = BigEndian::new(&mut bytes);
    writer.write_u16_at(16, flags);
    writer.write_i16_at(50, normalized.loca_format);
    let head_checksum = compute_checksum(&bytes);

    let table_count =
        u32::try_from(tables.len()).map_err(|_| invalid_data("WOFF2 table count exceeds u32"))?;
    let max_power = if table_count == 0 {
        0
    } else {
        1 << table_count.ilog2()
    };
    let search_range = max_power * 16;
    let entry_selector = if table_count == 0 {
        0
    } else {
        table_count.ilog2()
    };
    let range_shift = table_count * 16 - search_range;
    let mut checksum = 0x0001_0000_u32
        .wrapping_add((table_count << 16).wrapping_add(search_range))
        .wrapping_add((entry_selector << 16).wrapping_add(range_shift));
    let mut offset = 12_usize
        .checked_add(
            16_usize
                .checked_mul(tables.len())
                .ok_or_else(|| invalid_data("SFNT size overflow"))?,
        )
        .ok_or_else(|| invalid_data("SFNT size overflow"))?;
    for table in tables {
        let (table_checksum, table_len) = match &table.tag {
            b"head" => (head_checksum, table.bytes.len()),
            b"glyf" => (normalized.glyf_checksum, normalized.glyf_len),
            b"loca" => (normalized.loca_checksum, normalized.loca_len),
            _ => (table.checksum, table.bytes.len()),
        };
        checksum = checksum
            .wrapping_add(u32::from_be_bytes(table.tag))
            .wrapping_add(table_checksum)
            .wrapping_add(
                u32::try_from(offset).map_err(|_| invalid_data("SFNT offset exceeds u32"))?,
            )
            .wrapping_add(
                u32::try_from(table_len).map_err(|_| invalid_data("table size exceeds u32"))?,
            )
            .wrapping_add(table_checksum);
        offset = offset
            .checked_add((table_len + 3) & !3)
            .ok_or_else(|| invalid_data("SFNT size overflow"))?;
    }
    BigEndian::new(&mut bytes).write_u32_at(8, 0xb1b0_afba_u32.wrapping_sub(checksum));
    Ok(bytes)
}

fn move_loca_after_glyf(tables: &mut Vec<&SerializedTable>) {
    let loca_index = tables
        .iter()
        .position(|table| table.tag == *b"loca")
        .expect("required loca table must be present");
    let loca = tables.remove(loca_index);
    let glyf_index = tables
        .iter()
        .position(|table| table.tag == *b"glyf")
        .expect("required glyf table must be present");
    tables.insert(glyf_index + 1, loca);
}