webfont-generator 0.8.0

Generate webfonts (SVG, TTF, EOT, WOFF, WOFF2) from SVG icons
Documentation
use std::cmp::{max, min};
use std::collections::{HashMap, HashSet};
use std::io::Error;
use std::sync::Arc;

use write_fonts::tables::glyf::{GlyfLocaBuilder, Glyph, SimpleGlyph};

use crate::pipeline::TtfGlyphCache;
use crate::svg::types::ProcessedGlyph;

use super::cache::compiled_glyph_cache_key;
use super::ligatures::LigaturePlaceholderGlyph;
use super::outlines::{quadratic_path, quadratic_path_from_svg_path_data};
use super::types::{
    CachedCompiledGlyph, CmapAliases, CompiledGlyph, CompiledGlyphOutline, GlyphMetrics,
};
use super::{clamp_to_i16, clamp_to_u16};

pub(super) fn compile_and_dedup_glyphs(
    glyphs: &[ProcessedGlyph],
) -> Result<(Vec<CompiledGlyph>, CmapAliases), Error> {
    let mut compiled: Vec<CompiledGlyph> = Vec::with_capacity(glyphs.len());
    let mut aliases: Vec<(u32, usize)> = Vec::new();
    let mut seen: HashMap<(u64, u16), Vec<usize>> = HashMap::new();
    for (i, glyph) in glyphs.iter().enumerate() {
        let advance_width = clamp_to_u16(glyph.width.round(), 0, u16::MAX);
        let key = (glyph_path_bucket(glyph), advance_width);
        let duplicate_of = seen.get(&key).and_then(|indices| {
            indices
                .iter()
                .find(|&&idx| glyph_paths_equal(&glyphs[compiled[idx].source_index], glyph))
                .copied()
        });
        if let Some(first_idx) = duplicate_of {
            aliases.push((glyph.codepoint, first_idx));
        } else {
            let idx = compiled.len();
            seen.entry(key).or_default().push(idx);
            compiled.push(compile_glyph(i, glyph)?);
        }
    }
    Ok((compiled, aliases))
}

pub(super) fn compile_and_dedup_glyphs_cached(
    glyphs: &[ProcessedGlyph],
    cache: &mut TtfGlyphCache,
) -> Result<(Vec<CompiledGlyph>, CmapAliases), Error> {
    let mut compiled: Vec<CompiledGlyph> = Vec::with_capacity(glyphs.len());
    let mut aliases: Vec<(u32, usize)> = Vec::new();
    let mut seen: HashMap<(u64, u16), Vec<usize>> = HashMap::new();
    let mut used_keys = HashSet::with_capacity(glyphs.len());
    for (i, glyph) in glyphs.iter().enumerate() {
        let advance_width = clamp_to_u16(glyph.width.round(), 0, u16::MAX);
        let key = (glyph_path_bucket(glyph), advance_width);
        let duplicate_of = seen.get(&key).and_then(|indices| {
            indices
                .iter()
                .find(|&&idx| glyph_paths_equal(&glyphs[compiled[idx].source_index], glyph))
                .copied()
        });
        if let Some(first_idx) = duplicate_of {
            aliases.push((glyph.codepoint, first_idx));
        } else {
            let cache_key = compiled_glyph_cache_key(glyph, advance_width);
            let cached = match cache.entries.get(&cache_key) {
                Some(cached) => Arc::clone(cached),
                None => {
                    #[cfg(test)]
                    {
                        cache.compile_count += 1;
                    }
                    let simple_glyph = compile_simple_glyph(glyph)?;
                    let bbox = simple_glyph.bbox;
                    let cached = Arc::new(CachedCompiledGlyph {
                        advance_width,
                        bbox,
                        simple_glyph,
                    });
                    cache.entries.insert(cache_key, Arc::clone(&cached));
                    cached
                }
            };
            used_keys.insert(cache_key);
            let idx = compiled.len();
            seen.entry(key).or_default().push(idx);
            compiled.push(CompiledGlyph {
                advance_width: cached.advance_width,
                bbox: cached.bbox,
                codepoint: glyph.codepoint,
                left_side_bearing: cached.bbox.x_min,
                name: glyph.name.clone(),
                outline_key: Some(cache_key),
                outline: CompiledGlyphOutline::Shared(cached),
                source_index: i,
            });
        }
    }
    cache.entries.retain(|key, _| used_keys.contains(key));
    Ok((compiled, aliases))
}

pub(super) fn build_glyf_table(
    compiled_glyphs: &[CompiledGlyph],
    ligature_placeholders: &[LigaturePlaceholderGlyph],
) -> Result<
    (
        write_fonts::tables::glyf::Glyf,
        write_fonts::tables::loca::Loca,
        write_fonts::tables::loca::LocaFormat,
    ),
    Error,
> {
    let mut builder = GlyfLocaBuilder::new();
    builder
        .add_glyph(&Glyph::Empty)
        .map_err(|error| Error::other(format!("Failed to add .notdef glyph: {error}")))?;
    for glyph in compiled_glyphs {
        builder.add_glyph(glyph.simple_glyph()).map_err(|error| {
            Error::other(format!("Failed to compile glyph '{}': {error}", glyph.name))
        })?;
    }
    for placeholder in ligature_placeholders {
        builder.add_glyph(&Glyph::Empty).map_err(|error| {
            Error::other(format!(
                "Failed to add ligature placeholder '{}': {error}",
                placeholder.name
            ))
        })?;
    }
    Ok(builder.build())
}

pub(super) fn compute_glyph_metrics(glyphs: &[CompiledGlyph]) -> GlyphMetrics {
    let bbox = glyphs.iter().fold(
        (0_i16, 0_i16, 0_i16, 0_i16),
        |(x_min, y_min, x_max, y_max), g| {
            (
                min(x_min, g.bbox.x_min),
                min(y_min, g.bbox.y_min),
                max(x_max, g.bbox.x_max),
                max(y_max, g.bbox.y_max),
            )
        },
    );
    GlyphMetrics {
        advance_width_max: glyphs.iter().map(|g| g.advance_width).max().unwrap_or(0),
        bbox,
        max_contours: glyphs
            .iter()
            .map(|g| g.simple_glyph().contours.len() as u16)
            .max()
            .unwrap_or(0),
        max_points: glyphs
            .iter()
            .map(|g| {
                g.simple_glyph()
                    .contours
                    .iter()
                    .map(|c| c.len())
                    .sum::<usize>() as u16
            })
            .max()
            .unwrap_or(0),
        min_left_side_bearing: glyphs
            .iter()
            .map(|g| g.left_side_bearing)
            .min()
            .unwrap_or(0),
        min_right_side_bearing: glyphs
            .iter()
            .map(|g| {
                i32::from(g.advance_width)
                    - (i32::from(g.left_side_bearing) + i32::from(g.bbox.x_max)
                        - i32::from(g.bbox.x_min))
            })
            .min()
            .unwrap_or(0),
        x_avg_char_width: average_advance_width(glyphs),
        x_max_extent: glyphs
            .iter()
            .map(|g| {
                i32::from(g.left_side_bearing) + (i32::from(g.bbox.x_max) - i32::from(g.bbox.x_min))
            })
            .max()
            .unwrap_or(0),
    }
}

fn compile_glyph(source_index: usize, glyph: &ProcessedGlyph) -> Result<CompiledGlyph, Error> {
    let advance_width = clamp_to_u16(glyph.width.round(), 0, u16::MAX);
    let simple_glyph = compile_simple_glyph(glyph)?;
    let bbox = simple_glyph.bbox;
    Ok(CompiledGlyph {
        advance_width,
        bbox,
        codepoint: glyph.codepoint,
        left_side_bearing: bbox.x_min,
        name: glyph.name.clone(),
        outline: CompiledGlyphOutline::Inline(simple_glyph),
        outline_key: None,
        source_index,
    })
}

pub(super) fn compile_simple_glyph(glyph: &ProcessedGlyph) -> Result<SimpleGlyph, Error> {
    let path = match &glyph.ttf_path {
        Some(path) => quadratic_path(path)?,
        None => quadratic_path_from_svg_path_data(&glyph.path_data)?,
    };
    SimpleGlyph::from_bezpath(&path).map_err(|error| {
        Error::other(format!(
            "Failed to convert glyph '{}' into a TrueType outline: {error:?}",
            glyph.name
        ))
    })
}

fn glyph_path_bucket(glyph: &ProcessedGlyph) -> u64 {
    glyph.ttf_path_hash.unwrap_or(glyph.path_data.len() as u64)
}

fn glyph_paths_equal(left: &ProcessedGlyph, right: &ProcessedGlyph) -> bool {
    match (&left.ttf_path, &right.ttf_path) {
        (Some(left), Some(right)) => left.elements() == right.elements(),
        _ => left.path_data == right.path_data,
    }
}

fn average_advance_width(compiled_glyphs: &[CompiledGlyph]) -> i16 {
    let non_zero_widths = compiled_glyphs
        .iter()
        .map(|glyph| glyph.advance_width)
        .filter(|width| *width > 0)
        .collect::<Vec<_>>();
    if non_zero_widths.is_empty() {
        return 0;
    }
    let total: u32 = non_zero_widths.iter().map(|width| u32::from(*width)).sum();
    clamp_to_i16((total / non_zero_widths.len() as u32) as f64)
}