fdsm-skrifa 0.2.0

Import glyphs from Skrifa for fdsm
Documentation
use std::{fs, iter};

use fdsm::{generate::generate_msdf, render::render_msdf, shape::Shape};
use image::{EncodableLayout, GrayImage, ImageBuffer, Pixel, PixelWithColorType, RgbImage};
use na::{Affine2, Similarity2, Vector2};

use skrifa::{FontRef, MetadataProvider, prelude::Size, setting::VariationSetting};

use crate::load_shape_from_face;

// Adapted from the tests in msdfgen-rs:
// <https://github.com/katyo/msdfgen-rs/blob/master/src/lib.rs>

type Previewer<P> = fn(&ImageBuffer<P, Vec<<P as Pixel>::Subpixel>>, &mut GrayImage, f64);

fn save_bitmap_and_preview<P>(
    prefix: &str,
    name: &str,
    suffix: &str,
    image: &ImageBuffer<P, Vec<P::Subpixel>>,
    px_range: f64,
    previewer: Option<Previewer<P>>,
) where
    [P::Subpixel]: EncodableLayout,
    P: Pixel + PixelWithColorType,
{
    fs::create_dir_all("output").unwrap();

    image
        .save(format!("output/{prefix}-{name}-{suffix}.png"))
        .unwrap();

    if let Some(previewer) = previewer {
        let mut preview = GrayImage::new(image.width() * 10, image.height() * 10);

        previewer(image, &mut preview, px_range);

        preview
            .save(format!("output/{prefix}-{name}-{suffix}-preview.png"))
            .unwrap();
    }
}

fn test_font_glyph(prefix: &str, face: &FontRef<'_>, ch: char, expected_error: f64) {
    use image::{Rgb32FImage, RgbaImage, buffer::ConvertBuffer};

    use fdsm::{
        bezier::scanline::FillRule,
        correct_error::{ErrorCorrectionConfig, correct_error_msdf},
        generate::{generate_mtsdf, generate_sdf},
        render::{correct_sign_msdf, correct_sign_mtsdf, render_sdf},
        transform::Transform,
    };

    let glyph_id = face.charmap().map(ch).unwrap();
    let name = face
        .glyph_names()
        .get(glyph_id)
        .map(|name| name.as_str().into())
        .unwrap_or_else(|| ch.to_string());
    let axes = face.axes().location(iter::empty::<VariationSetting>());
    let bbox = face
        .glyph_metrics(Size::unscaled(), &axes)
        .bounds(glyph_id)
        .unwrap();
    let (mut shape, _) = load_shape_from_face(face, glyph_id, &axes).expect("no shape?");

    const RANGE: f64 = 4.0;
    const SHRINKAGE: f64 = 16.0;
    let transformation = na::convert::<_, Affine2<f64>>(Similarity2::new(
        Vector2::new(
            RANGE - bbox.x_min as f64 / SHRINKAGE,
            RANGE - bbox.y_min as f64 / SHRINKAGE,
        ),
        0.0,
        1.0 / SHRINKAGE,
    ));
    let width = ((bbox.x_max as f64 - bbox.x_min as f64) / SHRINKAGE + 2.0 * RANGE).ceil() as u32;
    let height = ((bbox.y_max as f64 - bbox.y_min as f64) / SHRINKAGE + 2.0 * RANGE).ceil() as u32;
    #[cfg(feature = "visualize")]
    let orig_shape = shape.clone();
    shape.transform(&transformation);

    let colored_shape = Shape::edge_coloring_simple(shape.clone(), 0.03, 69441337420);
    dbg!(&colored_shape);

    let mut sdf = GrayImage::new(width, height);
    let prepared_shape = shape.prepare();
    generate_sdf(&prepared_shape, RANGE, &mut sdf);
    save_bitmap_and_preview(prefix, &name, "sdf", &sdf, RANGE, Some(render_sdf));

    let mut msdf = Rgb32FImage::new(width, height);
    let prepared_colored_shape = colored_shape.prepare();
    generate_msdf(&prepared_colored_shape, RANGE, &mut msdf);
    correct_error_msdf(
        &mut msdf,
        &colored_shape,
        &prepared_colored_shape,
        RANGE,
        &ErrorCorrectionConfig::default(),
    );
    correct_sign_msdf(&mut msdf, &prepared_colored_shape, FillRule::Nonzero);
    let msdf = msdf.convert();
    save_bitmap_and_preview(prefix, &name, "msdf", &msdf, RANGE, Some(render_msdf));

    let mut mtsdf = RgbaImage::new(width, height);
    let prepared_colored_shape = colored_shape.prepare();
    generate_mtsdf(&prepared_colored_shape, RANGE, &mut mtsdf);
    correct_sign_mtsdf(&mut mtsdf, &prepared_colored_shape, FillRule::Nonzero);
    save_bitmap_and_preview(prefix, &name, "mtsdf", &mtsdf, RANGE, None);

    #[cfg(feature = "visualize")]
    {
        use fdsm::visualize::generate_vis;
        use na::Scale2;
        let colored_shape = Shape::edge_coloring_simple(orig_shape, 0.03, 69441337420);
        let mut vis = RgbImage::new(width * 10, height * 10);
        let transformation =
            na::convert::<_, Affine2<f64>>(Scale2::new(10.0, 10.0)) * transformation;
        generate_vis(&colored_shape, &transformation, &mut vis);
        save_bitmap_and_preview(prefix, &name, "voronoi", &vis, RANGE, None);
    }
}

#[test]

fn test_glyphs_noto() {
    let font = FontRef::from_index(notosans::REGULAR_TTF, 0).unwrap();
    for c in 'A'..='Z' {
        test_font_glyph("notosans", &font, c, 0.05);
    }
}

#[test]

fn test_glyphs_inter() {
    let font = FontRef::from_index(assets::INTER, 0).unwrap();
    for c in 'A'..='Z' {
        test_font_glyph("inter", &font, c, 0.05);
    }
}

#[test]

fn test_glyphs_noto_serif_sinhala() {
    let font = FontRef::from_index(assets::NOTO_SERIF_SINHALA, 0).unwrap();
    for c in ['a', 'b', '+'] {
        test_font_glyph("notoserif-sinhala", &font, c, 0.05);
    }
}

mod assets {
    pub static INTER: &[u8] = include_bytes!("../../assets/Inter-Regular.otf");
    // Ah, Noto Serif Sinhala. Why this specific font? Because someone reported an issue with Caxton using this font, and I noticed that it had some rendering artifacts. And here I am, using it to test MSDF error correction. I’m not even testing this with Sinhala glyphs at the moment. :concern:
    pub static NOTO_SERIF_SINHALA: &[u8] =
        include_bytes!("../../assets/noto_serif_sinhala_regular.ttf");
}