use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use artano::{load_font, Annotation, Canvas, Font};
use image::{DynamicImage, Rgba, RgbaImage};
struct Case {
name: &'static str,
width: u32,
height: u32,
annotations: Vec<Annotation>,
}
struct DiffResult {
max_channel_diff: u8,
mean_channel_diff: f64,
pixels_with_diff: usize,
pixels_with_diff_gt_5: usize,
pixels_with_diff_gt_32: usize,
total_pixels: usize,
time_a: Duration,
time_b: Duration,
}
fn main() {
let font = load_font("Impact").expect("Impact font must be installed");
let cases = vec![
Case {
name: "hd_short_top",
width: 1920,
height: 1080,
annotations: vec![Annotation::top("HELLO WORLD")],
},
Case {
name: "hd_long_split",
width: 1920,
height: 1080,
annotations: vec![Annotation::top(
"THIS IS A VERY LONG CAPTION THAT SHOULD DEFINITELY BE SPLIT ACROSS TWO LINES",
)],
},
Case {
name: "hd_two_annotations",
width: 1920,
height: 1080,
annotations: vec![
Annotation::top("TOP CAPTION"),
Annotation::bottom("BOTTOM CAPTION"),
],
},
Case {
name: "4k_short_top",
width: 3840,
height: 2160,
annotations: vec![Annotation::top("HELLO WORLD")],
},
];
let out_dir = PathBuf::from("target/diff");
std::fs::create_dir_all(&out_dir).expect("create diff dir");
println!("\n=== artano diff (method_a vs method_b) ===\n");
println!(
"{:<22} {:>9} {:>10} {:>9} {:>9} {:>8} {:>8} {:>11} {:>11}",
"case", "max_diff", "mean_diff", "px_diff", "of_total", "px>5", "px>32", "time_a", "time_b"
);
println!("{}", "-".repeat(110));
for case in &cases {
let image_bytes = generate_test_image(case.width, case.height);
let (img_a, time_a) = render_a(&image_bytes, &case.annotations, &font);
let (img_b, time_b) = render_b(&image_bytes, &case.annotations, &font);
let result = compute_diff(&img_a, &img_b, time_a, time_b);
if let Err(e) =
write_diff_image(&img_a, &img_b, &out_dir.join(format!("{}.png", case.name)))
{
eprintln!("failed to write diff image for {}: {}", case.name, e);
}
let pct = if result.total_pixels > 0 {
100.0 * result.pixels_with_diff as f64 / result.total_pixels as f64
} else {
0.0
};
println!(
"{:<22} {:>9} {:>10.4} {:>9} {:>8.3}% {:>8} {:>8} {:>11.2?} {:>11.2?}",
case.name,
result.max_channel_diff,
result.mean_channel_diff,
result.pixels_with_diff,
pct,
result.pixels_with_diff_gt_5,
result.pixels_with_diff_gt_32,
result.time_a,
result.time_b,
);
}
println!("\nDiff visualizations written to: {}", out_dir.display());
println!("(method_a and method_b currently use the same code path; diff should be 0.)\n");
}
fn render_a(
image_bytes: &[u8],
annotations: &[Annotation],
font: &Font,
) -> (DynamicImage, Duration) {
let start = Instant::now();
let mut canvas = Canvas::read_from_buffer(image_bytes).expect("decode");
for ann in annotations {
canvas.add_annotation(ann, font, 1.0);
}
canvas.render();
let time = start.elapsed();
(canvas_to_image(&canvas), time)
}
fn render_b(
image_bytes: &[u8],
annotations: &[Annotation],
font: &Font,
) -> (DynamicImage, Duration) {
render_a(image_bytes, annotations, font)
}
fn canvas_to_image(canvas: &Canvas) -> DynamicImage {
let mut buf = std::io::Cursor::new(Vec::new());
canvas.save_png(&mut buf).expect("encode png");
image::load_from_memory(&buf.into_inner()).expect("decode png")
}
fn compute_diff(
img_a: &DynamicImage,
img_b: &DynamicImage,
time_a: Duration,
time_b: Duration,
) -> DiffResult {
let a = img_a.to_rgba8();
let b = img_b.to_rgba8();
let (w, h) = a.dimensions();
let total = (w * h) as usize;
let mut max_diff: u8 = 0;
let mut sum_diff: u64 = 0;
let mut with_diff: usize = 0;
let mut with_diff_gt_5: usize = 0;
let mut with_diff_gt_32: usize = 0;
for (pa, pb) in a.pixels().zip(b.pixels()) {
let mut any = false;
let mut any_5 = false;
let mut any_32 = false;
for i in 0..4 {
let d = pa[i].abs_diff(pb[i]);
max_diff = max_diff.max(d);
sum_diff += d as u64;
if d > 0 {
any = true;
}
if d > 5 {
any_5 = true;
}
if d > 32 {
any_32 = true;
}
}
if any {
with_diff += 1;
}
if any_5 {
with_diff_gt_5 += 1;
}
if any_32 {
with_diff_gt_32 += 1;
}
}
DiffResult {
max_channel_diff: max_diff,
mean_channel_diff: sum_diff as f64 / (total * 4) as f64,
pixels_with_diff: with_diff,
pixels_with_diff_gt_5: with_diff_gt_5,
pixels_with_diff_gt_32: with_diff_gt_32,
total_pixels: total,
time_a,
time_b,
}
}
fn write_diff_image(
img_a: &DynamicImage,
img_b: &DynamicImage,
path: &Path,
) -> image::ImageResult<()> {
let a = img_a.to_rgba8();
let b = img_b.to_rgba8();
let (w, h) = a.dimensions();
let mut out = RgbaImage::new(w, h);
for (x, y, pa) in a.enumerate_pixels() {
let pb = b.get_pixel(x, y);
out.put_pixel(
x,
y,
Rgba([
pa[0].abs_diff(pb[0]).saturating_mul(16),
pa[1].abs_diff(pb[1]).saturating_mul(16),
pa[2].abs_diff(pb[2]).saturating_mul(16),
pa[3].abs_diff(pb[3]).saturating_mul(16),
]),
);
}
DynamicImage::ImageRgba8(out).save(path)
}
fn generate_test_image(width: u32, height: u32) -> Vec<u8> {
let img = image::ImageBuffer::from_fn(width, height, |x, y| {
let r = ((x * 255) / width.max(1)) as u8;
let g = ((y * 255) / height.max(1)) as u8;
let b = (((x.wrapping_add(y)) * 128) / (width + height).max(1)) as u8;
Rgba([r, g, b, 255])
});
let dynamic = image::DynamicImage::ImageRgba8(img);
let mut buf = std::io::Cursor::new(Vec::new());
dynamic
.write_to(&mut buf, image::ImageFormat::Png)
.expect("encode png");
buf.into_inner()
}