webfont-generator 0.11.0

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

use kurbo::{BezPath, CubicBez, PathEl, Point};

const QUAD_APPROXIMATION_ACCURACY: f64 = 0.25;
pub(super) const SIMPLIFY_TOLERANCE: f64 = 0.3;

pub(super) fn quadratic_path_from_svg_path_data(path_data: &str) -> Result<BezPath, Error> {
    let path = BezPath::from_svg(path_data).map_err(|error| {
        Error::new(
            ErrorKind::InvalidData,
            format!("Failed to parse generated SVG path data as a Bezier path: {error:?}"),
        )
    })?;
    quadratic_path(&path)
}

pub(super) fn quadratic_path(path: &BezPath) -> Result<BezPath, Error> {
    let mut elements: Vec<PathEl> = Vec::with_capacity(path.elements().len());
    let mut current: Option<Point> = None;
    let mut line_start: Option<Point> = None;

    for element in path.elements() {
        match *element {
            PathEl::MoveTo(point) => {
                elements.push(PathEl::MoveTo(point));
                current = Some(point);
                line_start = None;
            }
            PathEl::LineTo(point) => {
                let from = current.unwrap_or(point);
                current = Some(push_line(&mut elements, &mut line_start, from, point));
            }
            PathEl::QuadTo(control, point) => {
                let from = current.unwrap_or(control);
                if point_line_distance(control, from, point) <= SIMPLIFY_TOLERANCE {
                    current = Some(push_line(&mut elements, &mut line_start, from, point));
                } else {
                    elements.push(PathEl::QuadTo(control, point));
                    line_start = None;
                    current = Some(point);
                }
            }
            PathEl::CurveTo(control1, control2, point) => {
                let start = current.ok_or_else(|| {
                    Error::new(
                        ErrorKind::InvalidData,
                        "Encountered a cubic segment before a MoveTo while building a TTF glyph.",
                    )
                })?;
                let cubic = CubicBez::new(start, control1, control2, point);
                let mut from = start;
                for (_, _, quad) in cubic.to_quads(QUAD_APPROXIMATION_ACCURACY) {
                    if point_line_distance(quad.p1, from, quad.p2) <= SIMPLIFY_TOLERANCE {
                        from = push_line(&mut elements, &mut line_start, from, quad.p2);
                    } else {
                        elements.push(PathEl::QuadTo(quad.p1, quad.p2));
                        line_start = None;
                        from = quad.p2;
                    }
                }
                current = Some(from);
            }
            PathEl::ClosePath => {
                elements.push(PathEl::ClosePath);
                current = None;
                line_start = None;
            }
        }
    }
    Ok(BezPath::from_vec(elements))
}

pub(super) fn point_line_distance(p: Point, a: Point, b: Point) -> f64 {
    let dx = b.x - a.x;
    let dy = b.y - a.y;
    let len_sq = dx * dx + dy * dy;
    if len_sq <= f64::EPSILON {
        return (p - a).hypot();
    }
    ((p.x - a.x) * dy - (p.y - a.y) * dx).abs() / len_sq.sqrt()
}

fn push_line(
    elements: &mut Vec<PathEl>,
    line_start: &mut Option<Point>,
    from: Point,
    point: Point,
) -> Point {
    if let Some(start) = *line_start
        && point_line_distance(from, start, point) <= SIMPLIFY_TOLERANCE
        && let Some(PathEl::LineTo(end)) = elements.last_mut()
    {
        *end = point;
        return point;
    }
    elements.push(PathEl::LineTo(point));
    *line_start = Some(from);
    point
}