klyff_msdf 0.1.3

MSDF generation library with optional GPU acceleration.
Documentation
#![allow(dead_code)]

pub mod compare;
#[cfg(feature = "wgpu")]
pub mod gpu;

use compare::{LumaImageBuffer, compare_image};
use image::GenericImage;
use klyff_msdf::{AtlasRegionSize, MsdfGenerator, OutlineProvider, SkrifaOutlineProvider, median};
use skrifa::MetadataProvider;

pub fn compare_font_render(
    name: &str,
    font_data: &[u8],
    character: char,
    ref_img_path: &std::path::Path,
    output: &std::path::Path,
) {
    let font_ref = skrifa::FontRef::new(font_data).expect("Can read font");
    let glyph_id = font_ref
        .charmap()
        .map(character)
        .unwrap_or_else(|| panic!("{character} is not valid character in font"));
    let skrifa_provider = SkrifaOutlineProvider::new(font_ref.clone(), glyph_id);

    const PPEM: f32 = 100.;
    const PADDING_X: usize = 10;
    const PADDING_Y: usize = 10;

    let mut generator = MsdfGenerator::new();

    let em_size = skrifa_provider.length_per_em();
    let mut outline = match generator.read_glyph(&skrifa_provider) {
        Ok(outline) => outline,
        Err(klyff_msdf::ReadGlyphOutlineError::EmptyGlyph)
        | Err(klyff_msdf::ReadGlyphOutlineError::ShouldRasterize) => {
            return;
        }
        Err(klyff_msdf::ReadGlyphOutlineError::NotFound) => {
            panic!("Glyph not found for {glyph_id}");
        }
        Err(klyff_msdf::ReadGlyphOutlineError::Other(e)) => {
            panic!("Error encountered with glyph {glyph_id}: {e}");
        }
    };
    let size = AtlasRegionSize {
        inner_width: (outline.width_em() * PPEM).round() as usize,
        inner_height: (outline.height_em() * PPEM).round() as usize,
        padding_x: PADDING_X,
        padding_y: PADDING_Y,
    };
    let width = size.total_width();
    let height = size.total_height();
    let inner_width = size.inner_width;
    let inner_height = size.inner_height;

    let msdf_bytes = outline.generate_msdf(size, klyff_msdf::MsdfPixelFormat::PackedRgb8);
    let msdf_source_image =
        image::RgbImage::from_raw(width as u32, height as u32, msdf_bytes.to_vec())
            .expect("Valid image data");
    assert_ne!(msdf_source_image.width(), 0);
    assert_ne!(msdf_source_image.height(), 0);

    let mtsdf_bytes = outline.generate_mtsdf(size);
    let mtsdf_source_image =
        image::RgbaImage::from_raw(width as u32, height as u32, mtsdf_bytes.to_vec())
            .expect("Valid image data");
    assert_ne!(mtsdf_source_image.width(), 0);
    assert_ne!(mtsdf_source_image.height(), 0);

    let msdf_content = {
        let mut data = vec![0; inner_width * inner_height];
        for (j, y) in (size.padding_y..size.padding_y + inner_height).enumerate() {
            for (i, x) in (size.padding_x..size.padding_x + inner_width).enumerate() {
                let [r, g, b] = image::imageops::sample_bilinear(
                    &msdf_source_image,
                    (x as f32 + 0.5) / (width as f32),
                    (y as f32 + 0.5) / (height as f32),
                )
                .expect("Valid pixel")
                .0;
                let m = median(r, g, b);
                data[j * inner_width + i] = dist_to_color(m, em_size);
            }
        }

        data
    };

    let (mtsdf_pseudo_content, mtsdf_true_content) = {
        let mut pseudo = vec![0; inner_width * inner_height];
        let mut true_dist = vec![0; inner_width * inner_height];
        for (j, y) in (size.padding_y..size.padding_y + inner_height).enumerate() {
            for (i, x) in (size.padding_x..size.padding_x + inner_width).enumerate() {
                let [r, g, b, a] = image::imageops::sample_bilinear(
                    &mtsdf_source_image,
                    (x as f32 + 0.5) / (width as f32),
                    (y as f32 + 0.5) / (height as f32),
                )
                .expect("Valid pixel")
                .0;
                let m = median(r, g, b);

                pseudo[j * inner_width + i] = dist_to_color(m, em_size);
                true_dist[j * inner_width + i] = dist_to_color(a, em_size);
            }
        }

        (pseudo, true_dist)
    };

    let w = inner_width as u32;
    let h = inner_height as u32;
    let msdf_cmp_image = LumaImageBuffer {
        width: inner_width,
        height: inner_height,
        buffer: &msdf_content,
    };
    let mtsdf_pseudo_cmp_image = LumaImageBuffer {
        width: inner_width,
        height: inner_height,
        buffer: &mtsdf_pseudo_content,
    };
    let mtsdf_true_cmp_image = LumaImageBuffer {
        width: inner_width,
        height: inner_height,
        buffer: &mtsdf_true_content,
    };

    let Some(ref_image) = std::fs::File::open(ref_img_path)
        .map(std::io::BufReader::new)
        .ok()
        .and_then(|r| image::load(r, image::ImageFormat::Png).ok())
    else {
        println!("No existing reference image found. Generating a new one.");
        to_dyn_image(msdf_content, w, h)
            .save(ref_img_path)
            .expect("Failed to save reference image");

        return;
    };
    let ref_image = LumaImageBuffer {
        width: inner_width,
        height: inner_height,
        buffer: &ref_image
            .resize(w, h, image::imageops::FilterType::Triangle)
            .to_luma8(),
    };

    let (msdf_valid, msdf_deviation) = compare_image(&msdf_cmp_image, &ref_image);
    let (mtsdf_pseudo_valid, mtsdf_pseudo_deviation) =
        compare_image(&mtsdf_pseudo_cmp_image, &ref_image);
    let (mtsdf_true_valid, mtsdf_true_deviation) = compare_image(&mtsdf_true_cmp_image, &ref_image);

    if !msdf_valid || !mtsdf_pseudo_valid || !mtsdf_true_valid {
        dump_image(
            output,
            name,
            glyph_id.to_u32(),
            &[
                image::DynamicImage::ImageRgb8(msdf_source_image),
                image::DynamicImage::ImageRgba8(mtsdf_source_image),
                to_dyn_image(msdf_content, w, h),
                to_dyn_image(mtsdf_pseudo_content, w, h),
                to_dyn_image(mtsdf_true_content, w, h),
            ],
        );

        panic!(
            "Regression with rendering glyph {glyph_id}. Check dumped images at {}.
Deviation ratio: MSDF = {msdf_deviation}, MTSDF Pseudo = {mtsdf_pseudo_deviation}, MTSDF True = {mtsdf_true_deviation}",
            output.display()
        )
    }
}

pub fn dist_to_color(m: u8, em_size: f32) -> u8 {
    let dist = ((m as f32) / 255. - 0.5) * em_size;
    let w = (dist / (em_size * 0.006)).clamp(-1., 1.);
    ((w + 1.) / 2. * 255.) as u8
}

#[allow(dead_code)]
pub fn to_dyn_image(data: Vec<u8>, width: u32, height: u32) -> image::DynamicImage {
    image::DynamicImage::ImageLuma8(
        image::GrayImage::from_raw(width, height, data).expect("Valid image data"),
    )
}

#[allow(dead_code)]
pub fn dump_image(output: &std::path::Path, name: &str, id: u32, images: &[image::DynamicImage]) {
    let width = images.iter().map(|x| x.width()).sum::<u32>();
    let height = images.iter().map(|x| x.height()).max().expect("Not empty");

    let mut canvas = image::RgbaImage::new(width, height);
    let mut x_offset = 0;
    for img in images {
        canvas
            .copy_from(&img.to_rgba8(), x_offset, 0)
            .expect("Size should be correct");
        x_offset += img.width()
    }

    let output_path = output.join(format!("{}_{}.png", name, id));
    std::fs::create_dir_all(output_path.parent().expect("Valid path")).expect("Valid path");
    canvas.save(&output_path).expect("Failed to save image");
}