File size: 6,797 Bytes
2409829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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!("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"{}\" viewBox=\"{}\">", width, height, view_box);
		if !transform.is_empty() {
			result_svg.push_str(&format!("<g transform=\"{}\">", transform));
		}
		for path in &result {
			result_svg.push_str(&format!("<path d=\"{}\" {}/>", path_to_path_data(path, 1e-4), first_path_attributes));
		}
		if !transform.is_empty() {
			result_svg.push_str("</g>");
		}
		result_svg.push_str("</svg>");

		// 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("<path").count();
		if result_path_count != ground_truth_path_count {
			failure = true;
			eprintln!("Number of paths doesn't match for test: {}", test_name);
		}
	}
	if failure {
		panic!("Some tests have failed");
	}
}

fn render_svg(svg_code: &str) -> 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
}