fix(measure): include closing edges of every filled PDF subpath (#952)
* fix(measure): include closing edges of every filled subpath * fix(measure): include closing edges of every filled subpath * Run cargo fmt --------- Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
parent
a4742ba911
commit
43d16ef315
|
|
@ -74,6 +74,20 @@ fn nearest(p: Point, a: Point, b: Point) -> Point {
|
|||
let t = (dot(sub(p, a), v) / length).clamp(0.0, 1.0);
|
||||
[a[0] + v[0] * t, a[1] + v[1] * t]
|
||||
}
|
||||
/// Save a subpath's implicit closing edge for fill operations. An unpainted or
|
||||
/// stroke-only open path must not gain that edge.
|
||||
fn remember_close(closures: &mut Vec<[Point; 2]>, current: Option<Point>, start: Option<Point>) -> bool {
|
||||
if let (Some(a), Some(b)) = (current, start)
|
||||
&& distance(a, b) > 1e-9
|
||||
{
|
||||
if closures.len() >= MAX_SEGMENTS {
|
||||
return false;
|
||||
}
|
||||
closures.push([a, b]);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
/// Dense intersection searches are capped independently from path extraction.
|
||||
pub fn intersection_limited(&self, at: Point, tolerance: f64) -> bool {
|
||||
|
|
@ -233,6 +247,7 @@ impl Walker<'_> {
|
|||
let mut stack = Vec::new();
|
||||
let mut start = None;
|
||||
let mut current = None;
|
||||
let mut closures = Vec::new();
|
||||
let mut path = Geometry::default();
|
||||
let point = |m: Matrix, x: f64, y: f64| {
|
||||
let (x, y) = m.apply(x, y);
|
||||
|
|
@ -265,6 +280,10 @@ impl Walker<'_> {
|
|||
}
|
||||
"m" => {
|
||||
if let Some([x, y]) = op.nums::<2>() {
|
||||
if !remember_close(&mut closures, current, start) {
|
||||
self.geometry.truncated = true;
|
||||
break;
|
||||
}
|
||||
let p = point(matrix, x, y);
|
||||
current = Some(p);
|
||||
start = Some(p);
|
||||
|
|
@ -309,6 +328,10 @@ impl Walker<'_> {
|
|||
}
|
||||
"re" => {
|
||||
if let Some([x, y, w, h]) = op.nums::<4>() {
|
||||
if !remember_close(&mut closures, current, start) {
|
||||
self.geometry.truncated = true;
|
||||
break;
|
||||
}
|
||||
let a = point(matrix, x, y);
|
||||
let b = point(matrix, x + w, y);
|
||||
let c = point(matrix, x + w, y + h);
|
||||
|
|
@ -321,8 +344,24 @@ impl Walker<'_> {
|
|||
}
|
||||
}
|
||||
"S" | "s" | "f" | "F" | "f*" | "B" | "B*" | "b" | "b*" => {
|
||||
// Fills close open subpaths implicitly (ISO 32000-2 8.5.3.3); only a bare stroke leaves the path open.
|
||||
if String::from_utf8_lossy(&op.op).as_ref() != "S"
|
||||
let paint = String::from_utf8_lossy(&op.op);
|
||||
if matches!(paint.as_ref(), "f" | "F" | "f*" | "B" | "B*" | "b" | "b*") {
|
||||
if !remember_close(&mut closures, current, start) {
|
||||
self.geometry.truncated = true;
|
||||
break;
|
||||
}
|
||||
// Filling implicitly closes every open subpath, not just the last one.
|
||||
// Stroke-only S leaves its open subpaths unchanged.
|
||||
for [a, b] in closures.drain(..) {
|
||||
if self.geometry.segments.len().saturating_add(path.segments.len()) >= MAX_SEGMENTS
|
||||
|| self.geometry.targets().saturating_add(path.targets()) >= MAX_TARGETS
|
||||
{
|
||||
path.truncated = true;
|
||||
break;
|
||||
}
|
||||
path.edge(a, b);
|
||||
}
|
||||
} else if paint == "s"
|
||||
&& let (Some(a), Some(b)) = (current, start)
|
||||
{
|
||||
path.edge(a, b);
|
||||
|
|
@ -331,12 +370,14 @@ impl Walker<'_> {
|
|||
self.geometry.endpoints.append(&mut path.endpoints);
|
||||
self.geometry.midpoints.append(&mut path.midpoints);
|
||||
self.geometry.truncated |= path.truncated;
|
||||
closures.clear();
|
||||
current = None;
|
||||
start = None;
|
||||
}
|
||||
"n" => {
|
||||
self.geometry.truncated |= path.truncated;
|
||||
path = Geometry::default();
|
||||
closures.clear();
|
||||
current = None;
|
||||
start = None;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,6 +150,39 @@ fn snap_paths_endpoints_midpoints_and_intersections() {
|
|||
assert!(paths.snap([200.0, 200.0], 3.0, opts).unwrap().is_none());
|
||||
assert!(paths.snap([f64::NAN, 0.0], 3.0, opts).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn filled_open_subpath_offers_the_same_snap_edges_as_explicit_close() {
|
||||
let open = fixture("40 50 m 160 50 l 100 170 l f", "", &[]);
|
||||
let closed = fixture("40 50 m 160 50 l 100 170 l h f", "", &[]);
|
||||
let stroke = fixture("40 50 m 160 50 l 100 170 l S", "", &[]);
|
||||
let filled = snap::geometry(&open, 0).unwrap();
|
||||
let explicit = snap::geometry(&closed, 0).unwrap();
|
||||
let stroked = snap::geometry(&stroke, 0).unwrap();
|
||||
|
||||
assert_eq!(filled.segments, explicit.segments);
|
||||
assert_eq!(filled.segments.len(), 3);
|
||||
assert_eq!(stroked.segments.len(), 2);
|
||||
let paths_only = snap::SnapOptions { endpoints: false, midpoints: false, intersections: false, paths: true };
|
||||
let hit = filled.snap([55.0, 80.0], 5.0, paths_only).unwrap().unwrap();
|
||||
assert_eq!(hit.kind, snap::SnapKind::Path);
|
||||
assert_eq!(hit.point, [55.0, 80.0]);
|
||||
assert!(stroked.snap([55.0, 80.0], 5.0, paths_only).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fills_close_all_open_subpaths_but_strokes_do_not() {
|
||||
let triangles = "0 0 m 20 0 l 10 20 l 40 0 m 60 0 l 50 20 l";
|
||||
for paint in ["f", "F", "f*", "B", "B*", "b", "b*"] {
|
||||
let doc = fixture(&format!("{triangles} {paint}"), "", &[]);
|
||||
let geometry = snap::geometry(&doc, 0).unwrap();
|
||||
assert_eq!(geometry.segments.len(), 6, "{paint}");
|
||||
assert!(geometry.segments.contains(&[[10.0, 20.0], [0.0, 0.0]]), "{paint}");
|
||||
assert!(geometry.segments.contains(&[[50.0, 20.0], [40.0, 0.0]]), "{paint}");
|
||||
}
|
||||
let stroked = fixture(&format!("{triangles} S"), "", &[]);
|
||||
assert_eq!(snap::geometry(&stroked, 0).unwrap().segments.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_forms_transforms_curves_and_cycles_are_bounded() {
|
||||
let body = "0 0 m 10 0 l S 0 0 m 0 10 10 10 10 0 c S /Nested Do";
|
||||
|
|
|
|||
Loading…
Reference in New Issue