use crate::path_boolean::{self, FillRule, PathBooleanOperation}; use crate::path_data::{path_from_path_data, path_to_path_data}; use core::panic; use glob::glob; use image::{DynamicImage, GenericImageView, RgbaImage}; use resvg::render; use resvg::tiny_skia::Transform; use resvg::usvg::{Options, Tree}; use std::fs; use std::path::PathBuf; use svg::parser::Event; const TOLERANCE: u8 = 84; fn get_fill_rule(fill_rule: &str) -> FillRule { match fill_rule { "evenodd" => FillRule::EvenOdd, _ => FillRule::NonZero, } } #[test] fn visual_tests() { let ops = [ ("union", PathBooleanOperation::Union), ("difference", PathBooleanOperation::Difference), ("intersection", PathBooleanOperation::Intersection), ("exclusion", PathBooleanOperation::Exclusion), ("division", PathBooleanOperation::Division), ("fracture", PathBooleanOperation::Fracture), ]; let folders: Vec<(String, PathBuf, &str, PathBooleanOperation)> = glob("visual-tests/*/") .expect("Failed to read glob pattern") .flat_map(|entry| { let dir = entry.expect("Failed to get directory entry"); ops.iter() .map(move |(op_name, op)| (dir.file_name().unwrap().to_string_lossy().into_owned(), dir.clone(), *op_name, *op)) }) .collect(); let mut failure = false; for (name, dir, op_name, op) in folders { let test_name = format!("{} {}", name, op_name); println!("Running test: {}", test_name); fs::create_dir_all(dir.join("test-results")).expect("Failed to create test-results directory"); let original_path = dir.join("original.svg"); let mut content = String::new(); let svg_tree = svg::open(&original_path, &mut content).expect("Failed to parse SVG"); let mut paths = Vec::new(); let mut first_path_attributes = String::new(); let mut width = String::new(); let mut height = String::new(); let mut view_box = String::new(); let mut transform = String::new(); for event in svg_tree { match event { Event::Tag("svg", svg::node::element::tag::Type::Start, attributes) => { width = attributes.get("width").map(|s| s.to_string()).unwrap_or_default(); height = attributes.get("height").map(|s| s.to_string()).unwrap_or_default(); view_box = attributes.get("viewBox").map(|s| s.to_string()).unwrap_or_default(); } Event::Tag("g", svg::node::element::tag::Type::Start, attributes) => { if let Some(transform_attr) = attributes.get("transform") { transform = transform_attr.to_string(); } } Event::Tag("path", svg::node::element::tag::Type::Empty, attributes) => { let data = attributes.get("d").map(|s| s.to_string()).expect("Path data not found"); let fill_rule = attributes.get("fill-rule").map(|v| v.to_string()).unwrap_or_else(|| "nonzero".to_string()); paths.push((data, fill_rule)); // Store attributes of the first path if first_path_attributes.is_empty() { for (key, value) in attributes.iter() { if key != "d" && key != "id" { first_path_attributes.push_str(&format!("{}=\"{}\" ", key, value)); } } } } _ => {} } } if (width.is_empty() || height.is_empty()) && !view_box.is_empty() { let vb: Vec<&str> = view_box.split_whitespace().collect(); if vb.len() == 4 { width = vb[2].to_string(); height = vb[3].to_string(); } } if width.is_empty() || height.is_empty() { panic!("Failed to extract width and height from SVG"); } let a_node = paths[0].clone(); let b_node = paths[1].clone(); let a = path_from_path_data(&a_node.0).unwrap(); let b = path_from_path_data(&b_node.0).unwrap(); let a_fill_rule = get_fill_rule(&a_node.1); let b_fill_rule = get_fill_rule(&b_node.1); let result = path_boolean::path_boolean(&a, a_fill_rule, &b, b_fill_rule, op).unwrap(); // Create the result SVG with correct dimensions and transform let mut result_svg = format!("", width, height, view_box); if !transform.is_empty() { result_svg.push_str(&format!("", transform)); } for path in &result { result_svg.push_str(&format!("", path_to_path_data(path, 1e-4), first_path_attributes)); } if !transform.is_empty() { result_svg.push_str(""); } result_svg.push_str(""); // Save the result SVG let destination_path = dir.join("test-results").join(format!("{}-ours.svg", op_name)); fs::write(&destination_path, &result_svg).expect("Failed to write result SVG"); // Render and compare images let ground_truth_path = dir.join(format!("{}.svg", op_name)); let ground_truth_svg = fs::read_to_string(&ground_truth_path).expect("Failed to read ground truth SVG"); let ours_image = render_svg(&result_svg); let ground_truth_image = render_svg(&ground_truth_svg); let ours_png_path = dir.join("test-results").join(format!("{}-ours.png", op_name)); ours_image.save(&ours_png_path).expect("Failed to save our PNG"); let ground_truth_png_path = dir.join("test-results").join(format!("{}.png", op_name)); ground_truth_image.save(&ground_truth_png_path).expect("Failed to save ground truth PNG"); failure |= compare_images(&ours_image, &ground_truth_image, TOLERANCE); // Check the number of paths let result_path_count = result.len(); let ground_truth_path_count = ground_truth_svg.matches(" DynamicImage { let opts = Options::default(); let tree = Tree::from_str(svg_code, &opts).unwrap(); let pixmap_size = tree.size(); let (width, height) = (pixmap_size.width() as u32, pixmap_size.height() as u32); let mut pixmap = resvg::tiny_skia::Pixmap::new(width, height).unwrap(); let mut pixmap_mut = pixmap.as_mut(); render(&tree, Transform::default(), &mut pixmap_mut); DynamicImage::ImageRgba8(RgbaImage::from_raw(width, height, pixmap.data().to_vec()).unwrap()) } fn compare_images(img1: &DynamicImage, img2: &DynamicImage, tolerance: u8) -> bool { assert_eq!(img1.dimensions(), img2.dimensions(), "Image dimensions do not match"); for (x, y, pixel1) in img1.pixels() { let pixel2 = img2.get_pixel(x, y); for i in 0..4 { let difference = (pixel1[i] as i32 - pixel2[i] as i32).unsigned_abs() as u8; if difference > tolerance { println!("Difference {} larger than tolerance {} at [{}, {}], channel {}.", difference, tolerance, x, y, i); return true; } assert!( difference <= tolerance, "Difference {} larger than tolerance {} at [{}, {}], channel {}.", difference, tolerance, x, y, i ); } } false }