webfont-generator 0.8.0

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

use write_fonts::tables::cmap::Cmap;
use write_fonts::tables::head::Head;
use write_fonts::tables::hhea::Hhea;
use write_fonts::tables::hmtx::{Hmtx, LongMetric};
use write_fonts::tables::maxp::Maxp;
use write_fonts::tables::name::{Name, NameRecord};
use write_fonts::tables::os2::Os2;
use write_fonts::tables::post::Post;
use write_fonts::types::{FWord, Fixed, GlyphId, LongDateTime, NameId, Tag, UfWord};

use crate::pipeline::TtfGlyphCache;
use crate::sfnt::SerializedFontTables;

use super::cache::{
    dump_cached_ttf_table, dump_ttf_table, hash_option_str, hash_str, table_cache_key,
};
use super::ligatures::LigaturePlaceholderGlyph;
use super::types::{CompiledGlyph, GlyphMetrics, TtfOptions};
use super::{clamp_to_i16, clamp_to_u16, current_unix_timestamp};

const DEFAULT_VENDOR_ID: Tag = Tag::new(b"ATLW");
const DEFAULT_TTF_DESCRIPTION: &str = "Generated by svg2ttf from Fontello project.";
const DEFAULT_TTF_MANUFACTURER_URL: &str = "http://fontello.com";

#[allow(clippy::too_many_arguments)]
pub(super) fn assemble_font(
    options: &TtfOptions,
    compiled_glyphs: &[CompiledGlyph],
    cmap_aliases: &[(u32, usize)],
    ligature_placeholders: &[LigaturePlaceholderGlyph],
    glyf: write_fonts::tables::glyf::Glyf,
    loca: write_fonts::tables::loca::Loca,
    loca_format: write_fonts::tables::loca::LocaFormat,
    metrics: &GlyphMetrics,
    ascent: f64,
    descent: f64,
    font_height: f64,
    mut table_cache: Option<&mut TtfGlyphCache>,
) -> Result<SerializedFontTables, Error> {
    let units_per_em = clamp_to_u16(font_height.round(), 16, 16_384);
    let h_metrics = std::iter::once(LongMetric::new(0, 0))
        .chain(
            compiled_glyphs
                .iter()
                .map(|g| LongMetric::new(g.advance_width, g.left_side_bearing)),
        )
        .chain(ligature_placeholders.iter().map(|_| LongMetric::new(0, 0)))
        .collect::<Vec<_>>();
    let head_timestamp = derive_head_timestamp(options.ts);
    let head = Head::new(
        Fixed::from_i32(1),
        0,
        Default::default(),
        units_per_em,
        head_timestamp,
        head_timestamp,
        metrics.bbox.0,
        metrics.bbox.1,
        metrics.bbox.2,
        metrics.bbox.3,
        Default::default(),
        8,
        i16::from(loca_format == write_fonts::tables::loca::LocaFormat::Long),
    );
    let hhea = Hhea::new(
        FWord::new(clamp_to_i16(ascent.round())),
        FWord::new(clamp_to_i16(-descent.round())),
        FWord::new(0),
        UfWord::new(metrics.advance_width_max),
        FWord::new(metrics.min_left_side_bearing),
        FWord::new(clamp_to_i16(f64::from(metrics.min_right_side_bearing))),
        FWord::new(clamp_to_i16(f64::from(metrics.x_max_extent))),
        1,
        0,
        0,
        h_metrics.len() as u16,
    );
    let hmtx = Hmtx::new(h_metrics, Vec::new());
    let maxp = Maxp {
        num_glyphs: (compiled_glyphs.len() + ligature_placeholders.len() + 1) as u16,
        max_points: Some(metrics.max_points),
        max_contours: Some(metrics.max_contours),
        max_composite_points: Some(0),
        max_composite_contours: Some(0),
        max_zones: Some(2),
        max_twilight_points: Some(0),
        max_storage: Some(0),
        max_function_defs: Some(0),
        max_instruction_defs: Some(0),
        max_stack_elements: Some(0),
        max_size_of_instructions: Some(0),
        max_component_elements: Some(0),
        max_component_depth: Some(0),
    };
    let os2 = build_os2(
        options,
        metrics,
        ascent,
        descent,
        compiled_glyphs
            .iter()
            .map(|glyph| glyph.codepoint)
            .chain(ligature_placeholders.iter().map(|glyph| glyph.codepoint)),
    );
    let cmap = Cmap::from_mappings(
        compiled_glyphs
            .iter()
            .enumerate()
            .filter_map(|(i, g)| {
                char::from_u32(g.codepoint).map(|c| (c, GlyphId::new((i + 1) as u32)))
            })
            .chain(cmap_aliases.iter().filter_map(|(cp, idx)| {
                char::from_u32(*cp).map(|c| (c, GlyphId::new((*idx + 1) as u32)))
            }))
            .chain(
                ligature_placeholders
                    .iter()
                    .enumerate()
                    .filter_map(|(i, g)| {
                        char::from_u32(g.codepoint)
                            .map(|c| (c, GlyphId::new((compiled_glyphs.len() + i + 1) as u32)))
                    }),
            ),
    )
    .map_err(|error| {
        Error::new(
            ErrorKind::InvalidData,
            format!("Failed to build cmap table: {error}"),
        )
    })?;
    let font_subfamily = derive_subfamily_name(options);
    let name = build_name_table(
        options.font_name,
        &font_subfamily,
        None,
        options.copyright,
        options.description,
        options.manufacturer_url,
        derive_version_string(options.version).as_deref(),
    );
    let post = Post::new_v2(
        std::iter::once(".notdef")
            .chain(compiled_glyphs.iter().map(|g| g.name.as_str()))
            .chain(ligature_placeholders.iter().map(|g| g.name.as_str())),
    );
    let gsub = super::ligatures::build_ligature_gsub(compiled_glyphs, ligature_placeholders);
    let mut used_table_keys = HashSet::new();
    let mut tables = Vec::with_capacity(11);
    tables.push(dump_ttf_table(&head, "head")?);
    tables.push(dump_ttf_table(&hhea, "hhea")?);
    tables.push(dump_ttf_table(&maxp, "maxp")?);
    tables.push(dump_ttf_table(&os2, "OS/2")?);
    tables.push(dump_cached_ttf_table(
        &mut table_cache,
        &mut used_table_keys,
        || hmtx_cache_key(compiled_glyphs, ligature_placeholders),
        &hmtx,
        "hmtx",
    )?);
    tables.push(dump_cached_ttf_table(
        &mut table_cache,
        &mut used_table_keys,
        || cmap_cache_key(compiled_glyphs, cmap_aliases, ligature_placeholders),
        &cmap,
        "cmap",
    )?);
    tables.push(dump_cached_ttf_table(
        &mut table_cache,
        &mut used_table_keys,
        || glyf_loca_cache_key(b"loca", compiled_glyphs, ligature_placeholders),
        &loca,
        "loca",
    )?);
    tables.push(dump_cached_ttf_table(
        &mut table_cache,
        &mut used_table_keys,
        || glyf_loca_cache_key(b"glyf", compiled_glyphs, ligature_placeholders),
        &glyf,
        "glyf",
    )?);
    tables.push(dump_cached_ttf_table(
        &mut table_cache,
        &mut used_table_keys,
        || name_cache_key(options, &font_subfamily),
        &name,
        "name",
    )?);
    tables.push(dump_cached_ttf_table(
        &mut table_cache,
        &mut used_table_keys,
        || post_cache_key(compiled_glyphs, ligature_placeholders),
        &post,
        "post",
    )?);
    if let Some(gsub) = &gsub {
        tables.push(dump_cached_ttf_table(
            &mut table_cache,
            &mut used_table_keys,
            || gsub_cache_key(compiled_glyphs, ligature_placeholders),
            gsub,
            "GSUB",
        )?);
    }
    if let Some(cache) = table_cache {
        cache.tables.retain(|key, _| used_table_keys.contains(key));
    }
    SerializedFontTables::new(tables)
}

fn hmtx_cache_key(
    compiled_glyphs: &[CompiledGlyph],
    ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> u64 {
    table_cache_key(b"hmtx", |hasher| {
        for glyph in compiled_glyphs {
            hasher.write_u16(glyph.advance_width);
            hasher.write_i16(glyph.left_side_bearing);
        }
        hasher.write_usize(ligature_placeholders.len());
    })
}
fn cmap_cache_key(
    compiled_glyphs: &[CompiledGlyph],
    cmap_aliases: &[(u32, usize)],
    ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> u64 {
    table_cache_key(b"cmap", |hasher| {
        for glyph in compiled_glyphs {
            hasher.write_u32(glyph.codepoint);
        }
        for (codepoint, index) in cmap_aliases {
            hasher.write_u32(*codepoint);
            hasher.write_usize(*index);
        }
        for glyph in ligature_placeholders {
            hasher.write_u32(glyph.codepoint);
        }
    })
}
fn glyf_loca_cache_key(
    tag: &[u8; 4],
    compiled_glyphs: &[CompiledGlyph],
    ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> u64 {
    table_cache_key(tag, |hasher| {
        for glyph in compiled_glyphs {
            hasher.write_u64(
                glyph
                    .outline_key
                    .expect("glyf/loca cache keys are only built for cached glyphs"),
            );
        }
        hasher.write_usize(ligature_placeholders.len());
    })
}
fn name_cache_key(options: &TtfOptions, font_subfamily: &str) -> u64 {
    table_cache_key(b"name", |hasher| {
        hash_str(hasher, options.font_name);
        hash_str(hasher, font_subfamily);
        hash_option_str(hasher, options.copyright);
        hash_option_str(hasher, options.description);
        hash_option_str(hasher, options.manufacturer_url);
        hash_option_str(hasher, derive_version_string(options.version).as_deref());
    })
}
fn post_cache_key(
    compiled_glyphs: &[CompiledGlyph],
    ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> u64 {
    table_cache_key(b"post", |hasher| {
        for glyph in compiled_glyphs {
            hash_str(hasher, &glyph.name);
        }
        for glyph in ligature_placeholders {
            hash_str(hasher, &glyph.name);
        }
    })
}
fn gsub_cache_key(
    compiled_glyphs: &[CompiledGlyph],
    ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> u64 {
    table_cache_key(b"GSUB", |hasher| {
        for glyph in compiled_glyphs {
            hash_str(hasher, &glyph.name);
        }
        for glyph in ligature_placeholders {
            hasher.write_u32(glyph.codepoint);
        }
    })
}

pub(super) fn build_name_table(
    font_family: &str,
    font_subfamily: &str,
    postscript_name: Option<&str>,
    copyright: Option<&str>,
    description: Option<&str>,
    url: Option<&str>,
    version: Option<&str>,
) -> Name {
    let full_name = if font_subfamily == "Regular" {
        font_family.to_string()
    } else {
        format!("{font_family} {font_subfamily}")
    };
    let default_postscript_name = full_name.replace(' ', "-");
    let mut name_record = vec![
        make_windows_name_record(1, font_family),
        make_windows_name_record(2, font_subfamily),
        make_windows_name_record(4, &full_name),
        make_windows_name_record(5, version.unwrap_or("Version 1.0")),
        make_windows_name_record(6, postscript_name.unwrap_or(&default_postscript_name)),
    ];
    if let Some(copyright) = copyright {
        name_record.push(make_windows_name_record(0, copyright));
    }
    name_record.push(make_windows_name_record(
        10,
        description.unwrap_or(DEFAULT_TTF_DESCRIPTION),
    ));
    name_record.push(make_windows_name_record(
        11,
        url.unwrap_or(DEFAULT_TTF_MANUFACTURER_URL),
    ));
    name_record.sort();
    Name {
        name_record,
        ..Default::default()
    }
}
pub(super) fn make_windows_name_record(name_id: u16, value: &str) -> NameRecord {
    NameRecord::new(3, 1, 0x0409, NameId::new(name_id), value.to_string().into())
}

pub(super) fn build_os2(
    options: &TtfOptions,
    metrics: &GlyphMetrics,
    ascent: f64,
    descent: f64,
    codepoints: impl Iterator<Item = u32>,
) -> Os2 {
    let character_indices = codepoints
        .map(|codepoint| codepoint.min(u32::from(u16::MAX)) as u16)
        .collect::<Vec<_>>();
    Os2 {
        x_avg_char_width: metrics.x_avg_char_width,
        us_weight_class: derive_weight_class(options.font_weight),
        ach_vend_id: DEFAULT_VENDOR_ID,
        us_first_char_index: character_indices.iter().copied().min().unwrap_or(0),
        us_last_char_index: character_indices.iter().copied().max().unwrap_or(0),
        s_typo_ascender: clamp_to_i16(ascent.round()),
        s_typo_descender: clamp_to_i16(-descent.round()),
        s_typo_line_gap: 0,
        us_win_ascent: clamp_to_u16(ascent.round(), 0, u16::MAX),
        us_win_descent: clamp_to_u16(descent.round(), 0, u16::MAX),
        ..Default::default()
    }
}
fn derive_subfamily_name(options: &TtfOptions) -> String {
    match options.font_style {
        Some(style) if style.eq_ignore_ascii_case("italic") => "Italic".to_string(),
        _ => "Regular".to_string(),
    }
}
fn derive_weight_class(font_weight: Option<&str>) -> u16 {
    match font_weight {
        Some(value) if value.eq_ignore_ascii_case("bold") => 700,
        Some(value) => value
            .parse::<u16>()
            .ok()
            .filter(|weight| (1..=1000).contains(weight))
            .unwrap_or(400),
        None => 400,
    }
}
pub(super) fn derive_version_string(version: Option<&str>) -> Option<String> {
    let version = version?.trim();
    if version.is_empty() {
        return None;
    }
    if version.starts_with("Version ") {
        return Some(version.to_string());
    }
    Some(format!("Version {}", version.trim_end_matches('.')))
}
fn derive_head_timestamp(ts: Option<i64>) -> LongDateTime {
    const MAC_EPOCH_OFFSET_SECONDS: i64 = 2_082_844_800;
    let unix_seconds = ts.unwrap_or_else(current_unix_timestamp);
    LongDateTime::new(unix_seconds.saturating_add(MAC_EPOCH_OFFSET_SECONDS))
}