Skip to content
This repository was archived by the owner on Jan 25, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ skrifa.workspace = true
svg = { version = "0.18", default-features = false }
tiny-skia = "0.11.4"
usvg = { version = "0.44.0", default-features = false }
resvg = { version = "0.44.0", default-features = false }
serde_yaml = "0.9.34"

[profile.dev.package."*"]
opt-level = 2
Expand Down
137 changes: 128 additions & 9 deletions tests/regression.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,58 @@
use kompari::image;
use kurbo::BezPath;
use libtest_mimic::{Arguments, Failed, Trial};
use linesweeper::binary_op;
use linesweeper::topology::Contours;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tiny_skia::{Pixmap, Transform};

#[derive(Serialize, Deserialize, Debug)]
enum FillRule {
EvenOdd,
NonZero,
}

#[derive(Serialize, Deserialize, Debug)]
enum BinaryOp {
Union,
Intersection,
Difference,
Xor,
}

#[derive(Serialize, Deserialize, Debug)]
enum Assertion {
NoPanic,
Snapshot { width: u16, height: u16 },
}

#[derive(Serialize, Deserialize, Debug)]
struct RegressionCaseDeclaration {
svg_path_1: String,
svg_path_2: String,
fill_rule: FillRule,
op: BinaryOp,
assert: Option<Assertion>,
}

impl RegressionCaseDeclaration {
fn linesweeper_fill_rule(&self) -> linesweeper::FillRule {
match self.fill_rule {
FillRule::EvenOdd => linesweeper::FillRule::EvenOdd,
FillRule::NonZero => linesweeper::FillRule::NonZero,
}
}

fn linesweeper_binary_op(&self) -> linesweeper::BinaryOp {
match self.op {
BinaryOp::Union => linesweeper::BinaryOp::Union,
BinaryOp::Intersection => linesweeper::BinaryOp::Intersection,
BinaryOp::Difference => linesweeper::BinaryOp::Difference,
BinaryOp::Xor => linesweeper::BinaryOp::Xor,
}
}
}

fn main() {
let args = Arguments::from_args();
Expand All @@ -12,8 +63,9 @@ fn main() {

fn regression_tests() -> Vec<Trial> {
let ws = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let paths = glob::glob(&format!("{ws}/tests/regression/**/*.txt")).unwrap();
paths
let file_paths = glob::glob(&format!("{ws}/tests/regression/**/*.yml")).unwrap();

file_paths
.into_iter()
.map(|p| {
let p = p.unwrap();
Expand All @@ -31,16 +83,83 @@ fn input_path_base(input_path: &Path) -> &Path {

fn generate_regression_test(path: PathBuf) -> Result<(), Failed> {
let input = std::fs::read_to_string(&path).unwrap();
let lines: Vec<_> = input.lines().collect();
assert_eq!(lines.len(), 2);
let p0 = BezPath::from_svg(lines[0]).unwrap();
let p1 = BezPath::from_svg(lines[1]).unwrap();
binary_op(
let case: RegressionCaseDeclaration = serde_yaml::from_str(&input).unwrap();
let p0 = BezPath::from_svg(case.svg_path_1.as_str()).unwrap();
let p1 = BezPath::from_svg(case.svg_path_2.as_str()).unwrap();
let contours = binary_op(
&p0,
&p1,
linesweeper::FillRule::EvenOdd,
linesweeper::BinaryOp::Xor,
case.linesweeper_fill_rule(),
case.linesweeper_binary_op(),
)
.unwrap();

if let Assertion::Snapshot { width, height } = case.assert.unwrap_or(Assertion::NoPanic) {
assert_regression_snapshot(&path, &contours, width, height)?;
}

Ok(())
}

fn assert_regression_snapshot(
path: &Path,
contours: &Contours,
width: u16,
height: u16,
) -> Result<(), Failed> {
let mut bezpath = BezPath::new();

for contour in contours.contours() {
bezpath.extend(contour.path.elements().iter().cloned());
}

let svg_path_data = bezpath.to_svg();

let actual_pixmap = to_pixmap_from_svg_path(&svg_path_data, width, height)?;

let case_name = path.file_stem().unwrap().to_str().unwrap();
let snapshot_rel_name = format!("regression/{case_name}");
let snapshot_rel_path = Path::new(&snapshot_rel_name);

let snapshot_path = linesweeper_util::saved_snapshot_path_for(snapshot_rel_path);

if snapshot_path.exists() {
let png_data = actual_pixmap.encode_png().unwrap();
let actual_image = image::load_from_memory(&png_data).unwrap().into_rgba8();
let actual_snapshot = kompari::Image::from_raw(
actual_image.width(),
actual_image.height(),
actual_image.into_raw(),
)
.unwrap();
let expected_snapshot = kompari::load_image(&snapshot_path)?;

match kompari::compare_images(&expected_snapshot, &actual_snapshot) {
kompari::ImageDifference::None => Ok(()),
_ => Err("image comparison failed".into()),
}
} else {
std::fs::create_dir_all(snapshot_path.parent().unwrap()).unwrap();
actual_pixmap.save_png(&snapshot_path).unwrap();
Ok(())
}
}

fn to_pixmap_from_svg_path(svg_path: &str, width: u16, height: u16) -> Result<Pixmap, Failed> {
let opt = usvg::Options::default();
let svg_str = format!(
r#"
<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}">
<path d="{svg_path}"/>
</svg>
"#
);
let usvg_tree = usvg::Tree::from_str(&svg_str, &opt).unwrap();
let width = usvg_tree.size().width().floor() as u32;
let height = usvg_tree.size().height().floor() as u32;
let mut pixmap = Pixmap::new(width, height).unwrap();

resvg::render(&usvg_tree, Transform::default(), &mut pixmap.as_mut());

Ok(pixmap)
}
7 changes: 7 additions & 0 deletions tests/regression/font_awesome_0.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
svg_path_1: M128 160L128 128L64 128L64 512L128 512L128 352L224 352L224 512L288 512L288 128L224 128L224 288L128 288L128 160zM416 352L472 352C498.5 352 520 373.5 520 400C520 426.5 498.5 448 472 448L352 448L352 512L472 512C533.9 512 584 461.9 584 400C584 368.7 571.1 340.3 550.4 320C571.1 299.7 584 271.3 584 240C584 178.1 533.9 128 472 128L352 128L352 192L472 192C498.5 192 520 213.5 520 240C520 266.5 498.5 288 472 288L384 288L384 352L416 352z
svg_path_2: M610.1 576.1L576.2 610L30.2 64.1L64.2 30.2L610.1 576.1z
fill_rule: NonZero
op: Difference
assert: !Snapshot
width: 640
height: 640
7 changes: 7 additions & 0 deletions tests/regression/font_awesome_1.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
svg_path_1: M128 160L128 128L64 128L64 512L128 512L128 352L224 352L224 512L288 512L288 128L224 128L224 288L128 288L128 160zM416 160L416 128L352 128L352 352L512 352L512 512L576 512L576 128L512 128L512 288L416 288L416 160z
svg_path_2: M610.1 576.1L576.2 610L30.2 64.1L64.2 30.2L610.1 576.1z
fill_rule: NonZero
op: Difference
assert: !Snapshot
width: 640
height: 640
7 changes: 7 additions & 0 deletions tests/regression/font_awesome_2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
svg_path_1: M128 160L128 128L64 128L64 512L128 512L128 352L224 352L224 512L288 512L288 128L224 128L224 288L128 288L128 160zM384 128L352 128L352 336L464 336C494.9 336 520 361.1 520 392C520 422.9 494.9 448 464 448L352 448L352 512L464 512C530.3 512 584 458.3 584 392C584 325.7 530.3 272 464 272L416 272L416 192L568 192L568 128L384 128z
svg_path_2: M610.1 576.1L576.2 610L30.2 64.1L64.2 30.2L610.1 576.1z
fill_rule: NonZero
op: Difference
assert: !Snapshot
width: 640
height: 640
2 changes: 0 additions & 2 deletions tests/regression/graphite_0.txt

This file was deleted.

4 changes: 4 additions & 0 deletions tests/regression/graphite_0.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
svg_path_1: M27.30779709,-257.85907878 C27.30779665,-257.85907835 27.307796200000002,-257.85907792 27.30779576,-257.8590775 C27.292172620000002,-257.8439677 -109.98458240000001,-125.07781586 -233.88593633,-5.24757973 C-233.88593684,-5.24757923 -233.88593736,-5.24757873 -233.88593787,-5.24757824 C-233.88593736,-5.2475777400000005 -233.88593684,-5.24757724 -233.88593632,-5.24757675 C-174.52579263,51.894883570000005 -115.13821436,109.22164426 -77.72402204000001,145.36173051 C-77.72402157,145.36173091 -77.72402116,145.3617313 -77.7240208,145.3617317 C-77.7240208,145.3617317 -77.72402077,145.3617317 -77.72402077,145.3617317 C-77.72401908,145.3617313 -77.72401740000001,145.36173091 -77.72401572,145.36173051 C-1.60745976,127.42335802000001 72.89114167,108.36966443 101.12619937000001,101.0791851 C101.12620127,101.07918461 101.12620318,101.07918412000001 101.12620509,101.07918362000001 C101.1262059,101.07918313 101.12620674,101.07918264 101.1262076,101.07918215000001 C105.56968257,98.47134695 110.06808693,95.83127419 114.61338829,93.16367801 C114.61338927,93.16367744 114.61339025000001,93.16367686 114.61339123,93.16367628 C114.61339123,93.16367628 114.61339123,93.16367628 114.61339123,93.16367628 C114.61339142,93.16367571 114.61339161000001,93.16367513 114.6133918,93.16367456 C123.52785808,66.12754849 166.90764887,-69.59356743000001 182.19132273,-195.37502561 C182.19132278,-195.37502607 182.19132284,-195.37502653 182.19132290000002,-195.37502699 C182.19132175000001,-195.37502746 182.19132061,-195.37502792 182.19131946000002,-195.37502838 C99.03601942,-228.9220441 27.32536748,-257.85199043 27.307800320000002,-257.8590775 C27.30779923,-257.85907792 27.30779816,-257.85907835 27.30779709,-257.85907878 L27.30779709,-257.85907878 Z M-408.62463824,-172.46325538 C-408.62463827,-172.46325472 -408.62463831,-172.46325406 -408.62463834,-172.4632534 C-414.02520486000003,-63.40231834 -377.28160484,34.20037327 -342.02221595000003,99.33558422 C-342.02221565,99.33558477 -342.02221536,99.33558531 -342.02221506,99.33558586000001 C-342.02221506,99.33558586000001 -342.02221506,99.33558586000001 -342.02221506,99.33558586000001 C-342.0222145,99.33558531 -342.02221393,99.33558477 -342.02221337000003,99.33558422 C-310.40978596,68.76186957 -272.80781456,32.395413760000004 -233.88593941,-5.24757675 C-233.8859389,-5.24757724 -233.88593838,-5.2475777400000005 -233.88593787,-5.24757824 C-233.88593787,-5.24757824 -233.88593787,-5.24757824 -233.88593787,-5.24757824 C-233.88593839,-5.24757873 -233.88593891,-5.24757923 -233.88593942,-5.24757973 C-304.53464918000003,-73.25686748 -375.14449713,-141.00509201 -408.62463617000003,-172.4632534 C-408.62463687,-172.46325406 -408.62463756,-172.46325472 -408.62463824,-172.46325538 L-408.62463824,-172.46325538 Z
svg_path_2: M27.3077958,-257.85907753 C27.2923816,-257.84416980000003 -109.98448809,-125.07790706 -233.88593634,-5.24757971 C-233.88593634,-5.24757971 -233.88593825,-5.24757971 -233.88593825,-5.24757971 C-233.88593941,-5.24757971 -233.88593942,-5.24757972 -233.88593942,-5.24757972 C-304.53459173,-73.25681217 -375.1443823,-141.00498183 -408.62463995,-172.46317667 C-408.62464066,-172.46317733 -408.62464135,-172.46317798 -408.62464203,-172.46317863000002 C-408.62464208,-172.46317798 -408.62464211,-172.46317733 -408.62464214,-172.46317667 C-414.02518763,-63.40227258 -377.28159657000003,34.20038853 -342.02221596,99.3355842 C-342.02221566000003,99.33558476 -342.02221536,99.33558531 -342.02221506,99.33558586000001 C-342.02221449,99.33558531 -342.02221392,99.33558476 -342.02221335,99.3355842 C-310.40978594,68.76186955 -272.80781455,32.39541375 -233.88593941,-5.24757675 C-233.88593941,-5.24757675 -233.88593941,-5.24757675 -233.88593941,-5.24757675 C-233.88593889,-5.24757724 -233.88593838,-5.2475777400000005 -233.88593787,-5.24757823 C-233.88593735,-5.2475777400000005 -233.88593684,-5.24757724 -233.88593632,-5.24757675 C-233.88593632,-5.24757675 -233.88593632,-5.24757675 -233.88593632,-5.24757675 C-174.52579259,51.89488361 -115.13821427,109.22164435 -77.72402196,145.36173056 C-77.72402157,145.36173094 -77.72402117,145.36173132 -77.72402078,145.3617317 C-77.72401917,145.36173132 -77.72401755,145.36173094 -77.72401594,145.36173056 C-1.60745982,127.42335803 72.89114182,108.36966439 101.12619946,101.07918507000001 C101.12620136,101.07918458 101.12620325,101.07918409 101.12620514,101.07918360000001 C101.12620595,101.07918311 101.12620679,101.07918263 101.12620764,101.07918214 C105.56968257,98.47134695 110.0680869,95.83127422 114.61338822,93.16367806 C114.61338923,93.16367747 114.61339023000001,93.16367689 114.61339123,93.1636763 C114.61339142,93.16367571 114.61339161000001,93.16367512000001 114.61339181,93.16367453000001 C123.52785811,66.12754843 166.90764887,-69.59356747 182.19132273,-195.37502563 C182.19132279000002,-195.37502608 182.19132285,-195.37502654 182.19132289,-195.37502699 C182.19132177,-195.37502745 182.19132064000001,-195.3750279 182.1913195,-195.37502836000002 C99.03588674,-228.92209763 27.32513858,-257.85208278 27.30780024,-257.85907753 C27.30780024,-257.85907753 27.30779791,-257.85907753 27.30779791,-257.85907753 C27.30779791,-257.85907753 27.3077958,-257.85907753 27.3077958,-257.85907753 L27.3077958,-257.85907753 Z
fill_rule: EvenOdd
op: Xor
Loading