vectorcraft/crates/svg/tests/stroke_export.rs

84 lines
3.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Strokes in SVG export look as on the canvas: arrowheads, width profiles, dots and brushes are
//! written as the canvas's filled outlines or art, aligned strokes are clipped or masked, and open
//! paths stroke centred. Checked by rendering the exported SVG with resvg.
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{Appearance, AppearanceItem, Arrowhead, Dash, Document, LineCap, LineJoin, Node, StrokeAlign, StrokeLayer, WidthProfile};
use vectorcraft_geom::{PathData, Point, SubPath, shapes};
use vectorcraft_svg::{ExportOptions, export};
use vectorcraft_testkit::raster::{Image, assert_similar, render_artboard};
fn add(d: &mut Document, path: PathData, f: impl FnOnce(&mut StrokeLayer)) {
let mut st = StrokeLayer::new(Paint::solid(Color::rgb(0.1, 0.2, 0.6)), 4.0);
f(&mut st);
let n = Node::path(d.alloc_id(), path, Appearance { items: vec![AppearanceItem::Stroke(st)], ..Default::default() });
let l = d.layers[0].id;
d.insert(Some(l), usize::MAX, n).unwrap();
}
fn line(x0: f64, y: f64, x1: f64) -> PathData {
shapes::line(Point::new(x0, y), Point::new(x1, y))
}
/// The exported SVG rendered by resvg on white at 1 px/pt.
fn resvg_render(svg: &str, w: u32, h: u32) -> Image {
let tree = resvg::usvg::Tree::from_str(svg, &resvg::usvg::Options::default()).expect("parse");
let mut pm = resvg::tiny_skia::Pixmap::new(w, h).unwrap();
pm.fill(resvg::tiny_skia::Color::WHITE);
resvg::render(&tree, resvg::tiny_skia::Transform::identity(), &mut pm.as_mut());
Image { width: w, height: h, rgba: pm.data().to_vec() }
}
#[test]
fn exported_strokes_render_like_the_canvas() {
let mut d = Document::new(200.0, 200.0);
add(&mut d, line(20.0, 20.0, 150.0), |s| {
s.end_arrow = Some(Arrowhead::Triangle);
s.start_arrow = Some(Arrowhead::CircleOpen);
s.opacity = 0.5;
});
add(&mut d, line(20.0, 50.0, 180.0), |s| {
s.width = 14.0;
s.profile = Some(WidthProfile::lens());
});
add(&mut d, line(20.0, 80.0, 180.0), |s| {
s.cap = LineCap::Square;
s.dash = Some(Dash { pattern: vec![0.0, 12.0], offset: 0.0, align_corners: false });
});
add(&mut d, line(20.0, 110.0, 180.0), |s| s.brush = Some("Tapered Stroke".into()));
let tri = SubPath::polyline(&[Point::new(60.0, 185.0), Point::new(100.0, 185.0), Point::new(80.0, 135.0)], true);
add(&mut d, PathData::single(tri), |s| {
s.align = StrokeAlign::Outside;
s.join = LineJoin::Miter;
s.width = 6.0;
});
let svg = export(&d, &ExportOptions::default());
assert!(!svg.contains("stroke-dasharray"), "dots are outlines:\n{svg}");
let canvas = render_artboard(&d);
let written = resvg_render(&svg, 200, 200);
assert_similar(&canvas, &written, 24.0, 0.002);
// The comparison is meaningful: every stroke drew something.
for y in [20, 50, 80, 110, 160] {
assert!((0..200).any(|x| canvas.over_white(x, y).iter().any(|c| *c < 200)), "row {y} has ink");
}
}
#[test]
fn an_open_path_with_inside_alignment_exports_a_plain_stroke() {
let mut d = Document::new(200.0, 100.0);
add(&mut d, line(20.0, 50.0, 180.0), |s| s.align = StrokeAlign::Inside);
let svg = export(&d, &ExportOptions::default());
assert!(svg.contains("stroke-width=\"4\""), "{svg}");
assert!(!svg.contains("clip-path") && !svg.contains("mask"), "{svg}");
}
#[test]
fn a_lens_profile_exports_a_fill() {
let mut d = Document::new(200.0, 100.0);
add(&mut d, line(20.0, 50.0, 180.0), |s| s.profile = Some(WidthProfile::lens()));
let svg = export(&d, &ExportOptions::default());
assert!(svg.contains("fill=\"#1a3399\"") && !svg.contains("stroke="), "{svg}");
}