#![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");
}