From 28f8f84fd5a64b71e1195a920968ddbbf8e7096b Mon Sep 17 00:00:00 2001 From: Don Omondi <282319807+mockydevs@users.noreply.github.com> Date: Sat, 7 Nov 2026 05:01:25 -0500 Subject: [PATCH] EPS import: strips under a larger clip keep the clip A run of gradient strips under a clip larger than the strips became the clip path filled with the gradient, painting where the strips did not. The clip path now takes the gradient only when every point of the clip lies inside the run; otherwise the gradient stays clipped to the strips' area. --- crates/eps/src/import/graphics/strips.rs | 40 ++++++++++++++++++------ crates/eps/src/import/tests.rs | 17 ++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/crates/eps/src/import/graphics/strips.rs b/crates/eps/src/import/graphics/strips.rs index 84146c338..9f0b39a24 100644 --- a/crates/eps/src/import/graphics/strips.rs +++ b/crates/eps/src/import/graphics/strips.rs @@ -16,7 +16,7 @@ use super::{ Affine, Appearance, AppearanceItem, BezPath, Color, Drawn, FillLayer, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, HashSet, - MAX_STITCH_STOPS, Node, NodeId, NodeKind, Paint, PathData, Point, Vec2, + MAX_STITCH_STOPS, Node, NodeId, NodeKind, Paint, PathData, PathEl, Point, Vec2, }; /// Fewest strips (that paint something) a gradient is read from. @@ -116,6 +116,13 @@ impl Axes { self.o + self.w * s + self.v * t } + /// Does `p` lie within the run's length, and across it from `lo` to `hi`? + fn holds(&self, p: Point, lo: f64, hi: f64) -> bool { + let (ts, ss) = (EPS / self.v.hypot(), EPS / self.w.hypot()); + let l = self.inv * p; + l.x >= lo - ss && l.x <= hi + ss && l.y >= -ts && l.y <= 1.0 + ts + } + /// The strip's extent across the run (`s` from, to), when it spans the run's length exactly. fn band(&self, st: &Strip) -> Option<(f64, f64)> { let (ts, ss) = (EPS / self.v.hypot(), EPS / self.w.hypot()); @@ -131,12 +138,14 @@ impl Axes { } } -/// A run of strips found at one place in what was drawn: where it ends (exclusive) and the -/// object it becomes. +/// A run of strips found at one place in what was drawn: where it ends (exclusive), the object +/// it becomes, and whether that object covers the clip around it (so the clip path filled with +/// the gradient paints no more than the strips did). struct Run { start: usize, end: usize, node: Node, + covers_clip: bool, } /// Is `s` (from, to) too thin to paint anything? @@ -226,9 +235,19 @@ fn run_at(drawn: &Drawn, strips: &[Option], i: usize) -> Option { bp.line_to(axes.at(lo, 1.0)); bp.close_path(); let item = AppearanceItem::Fill(FillLayer { overprint: a.overprint, ..FillLayer::new(Paint::Gradient(Box::new(g))) }); - let id = drawn.get(i).map(|(_, n)| n.id)?; - let node = Node::path(id, PathData::from_bezpath(&bp), Appearance { items: vec![item], ..Appearance::default() }); - Some(Run { start: i, end, node }) + let (clips, first_node) = drawn.get(i)?; + // Every point of a clip's path, control points included, inside the run: its curves are too + // (each lies in the hull of its points), and the run's area is convex. + let covers_clip = clips.last().and_then(|c| c.region.as_ref()).is_some_and(|(clip, _)| { + clip.elements().iter().all(|el| match *el { + PathEl::MoveTo(p) | PathEl::LineTo(p) => axes.holds(p, lo, hi), + PathEl::QuadTo(a, p) => axes.holds(a, lo, hi) && axes.holds(p, lo, hi), + PathEl::CurveTo(a, b, p) => axes.holds(a, lo, hi) && axes.holds(b, lo, hi) && axes.holds(p, lo, hi), + PathEl::ClosePath => true, + }) + }); + let node = Node::path(first_node.id, PathData::from_bezpath(&bp), Appearance { items: vec![item], ..Appearance::default() }); + Some(Run { start: i, end, node, covers_clip }) } /// The stops `stops` (offset, strip) needs: those the gradient between the stops kept on either @@ -262,8 +281,9 @@ fn on_line(stops: &[(f32, &Strip)], a: usize, b: usize) -> bool { }) } -/// Runs of strips in `drawn` become one gradient-filled object each (marked in `shadings`, so a -/// clipping group of nothing else is its clip path filled with the gradient). +/// Runs of strips in `drawn` become one gradient-filled object each; one that covers its clip is +/// marked in `shadings`, so a clipping group of nothing else is its clip path filled with the +/// gradient (a clip larger than the strips keeps clipping the gradient to them). pub(crate) fn merge_strips(drawn: &mut Drawn, shadings: &mut HashSet) { let strips: Vec> = drawn.iter().map(|(_, n)| strip_of(n)).collect(); if strips.iter().filter(|s| s.is_some()).count() < MIN_STRIPS { @@ -292,7 +312,9 @@ pub(crate) fn merge_strips(drawn: &mut Drawn, shadings: &mut HashSet) { } if let Some(r) = runs.next_if(|r| r.start == k) { skip_to = r.end; - shadings.insert(r.node.id); + if r.covers_clip { + shadings.insert(r.node.id); + } drawn.push((item.0, r.node)); continue; } diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 03bce703d..4bc8a65be 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -1486,6 +1486,23 @@ fn abutting_strips_of_a_gradient_become_one_gradient() { assert!((light.0 - 11.0).abs() < 0.01 && light.1.abs() < 1e-6, "{light:?}"); } +#[test] +fn strips_under_a_larger_clip_keep_the_clip() { + // The same ramp under a clip larger than the strips: the gradient fills only where the + // strips were, still clipped, not the whole clip. + let r = read( + "gsave 5 5 90 90 rectclip + 0 1 39 { /i exch def i 39 div setgray 88 i 2 mul sub 10 2 80 rectfill } for grestore", + ); + let o = objects(&r.document); + assert_eq!(o.len(), 1, "{o:#?}"); + let NodeKind::Group { children, clip: true } = &o[0].kind else { panic!("not a clipping group: {:#?}", o[0]) }; + let [_, fill] = children.as_slice() else { panic!("{children:#?}") }; + assert!(near(bounds(fill), Rect::new(10.0, 10.0, 90.0, 90.0)), "{:?}", bounds(fill)); + let (stops, _, _) = linear_stops(fill); + assert_eq!(stops.len(), 2, "{stops:?}"); +} + #[test] fn turned_strips_become_a_gradient_across_them() { // Strips drawn in a turned space: the gradient runs square to them.