diff --git a/crates/eps/src/import/graphics.rs b/crates/eps/src/import/graphics.rs index 00247fe7..0ffc89f2 100644 --- a/crates/eps/src/import/graphics.rs +++ b/crates/eps/src/import/graphics.rs @@ -39,8 +39,9 @@ const MAX_PATH: usize = 1 << 20; const MAX_DRAWN: usize = 1 << 24; /// Samples taken of a shading function that isn't a plain interpolation. const SHADING_SAMPLES: usize = 32; -/// Most bounds of a stitched linear function that become a gradient's stops: past it, the -/// function is sampled like any other (each stop evaluates the whole function). +/// Most bounds of a stitched linear function that become a gradient's stops (two where the pieces +/// don't meet): past it, the function is sampled like any other (each stop evaluates the whole +/// function). const MAX_STITCH_STOPS: usize = 1024; /// Most functions one evaluation of a shading's function visits: the function itself, each entry /// of an array of functions and, in each stitching function, the piece that applies to the input. @@ -1098,7 +1099,9 @@ impl Interp<'_> { // Where the colour changes: the ends, and the bounds of stitched linear functions (unless // there are too many). let mut visits = 0; - let ts: Vec = match linear_points(&f, 0, &mut visits).filter(|p| p.len() <= MAX_STITCH_STOPS) { + let points = linear_points(&f, 0, &mut visits).filter(|p| p.len() <= MAX_STITCH_STOPS); + let stitched = points.is_some(); + let ts: Vec = match points { Some(mut pts) => { pts.retain(|t| (d0.min(d1)..=d0.max(d1)).contains(t)); pts.iter().map(|t| if d1 != d0 { (t - d0) / (d1 - d0) } else { 0.0 }).chain([0.0, 1.0]).collect() @@ -1112,15 +1115,25 @@ impl Interp<'_> { let c = |i: usize| coords.get(i).copied().unwrap_or(0.0); let (r0, r1) = (c(2).max(0.0), c(5).max(1e-6)); let mut stops = Vec::with_capacity(ts.len()); + let stop = |paint: Paint, offset: f64| match paint { + Paint::Solid { color, swatch, tint } => GradientStop { swatch, tint, ..GradientStop::new(offset as f32, color) }, + _ => GradientStop::new(offset as f32, Color::BLACK), + }; for t in ts { let comps = self.eval(&f, &[d0 + (d1 - d0) * t], 0)?; let paint = self.paint_of(&space, &comps)?; let offset = if radial { (r0 + (r1 - r0) * t) / r1 } else { t }; - let stop = match paint { - Paint::Solid { color, swatch, tint } => GradientStop { swatch, tint, ..GradientStop::new(offset as f32, color) }, - _ => GradientStop::new(offset as f32, Color::BLACK), - }; - stops.push(stop); + // At a bound of a stitched function the pieces need not meet (a hard colour change): + // the colour just before the bound is a stop at the same offset, ahead of the one + // after it. + if stitched && t > 0.0 && t < 1.0 { + let comps = self.eval(&f, &[d0 + (d1 - d0) * (t - 1e-9)], 0)?; + let before = stop(self.paint_of(&space, &comps)?, offset); + if before.color != stop(paint.clone(), offset).color { + stops.push(before); + } + } + stops.push(stop(paint, offset)); } let (gk, geom) = if radial { let centre = Point::new(c(3), c(4)); diff --git a/crates/eps/src/import/tests.rs b/crates/eps/src/import/tests.rs index 238fcfca..63cf89b4 100644 --- a/crates/eps/src/import/tests.rs +++ b/crates/eps/src/import/tests.rs @@ -292,6 +292,19 @@ fn shadings_become_gradients() { assert_eq!(g.gradient.kind, GradientKind::Radial); let offsets: Vec = g.gradient.stops.iter().map(|s| s.offset).collect(); assert_eq!(offsets, vec![0.0, 0.25, 1.0]); + // Pieces that don't meet at a bound make a hard stop: the colour before the bound and the + // one after it, at the same offset (the PDF reader does the same). + let hard = "<< /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 200 0] /Function << /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [<< /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >> << /FunctionType 2 /Domain [0 1] /C0 [1 0 0] /C1 [0 0 1] /N 1 >>] >> >>"; + let r = read(&format!("0 0 200 50 rectclip {hard} shfill")); + let o = objects(&r.document); + let Some(Paint::Gradient(g)) = fill(&o[0]) else { panic!("{:?}", o[0]) }; + let stops: Vec<(f32, Color)> = g.gradient.stops.iter().map(|s| (s.offset, s.color)).collect(); + let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0)); + assert_eq!(stops.len(), 4, "{stops:?}"); + assert_eq!((stops[0], stops[3]), ((0.0, red), (1.0, blue)), "{stops:?}"); + assert_eq!((stops[1].0, stops[2]), (0.5, (0.5, red)), "{stops:?}"); + let near = |a: [f32; 3], b: [f32; 3]| a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6); + assert!(near(stops[1].1.to_rgb(), blue.to_rgb()), "{stops:?}"); } /// Tiling patterns and spot inks are found again through a hash of what tells them apart, so a diff --git a/crates/eps/src/import/tests_generators.rs b/crates/eps/src/import/tests_generators.rs index cf5f4e08..c7584df9 100644 --- a/crates/eps/src/import/tests_generators.rs +++ b/crates/eps/src/import/tests_generators.rs @@ -920,10 +920,12 @@ fn gradients_stitched_from_linear_pieces_keep_or_sample_their_stops() { /Functions [ {pieces} {{ F }} repeat ] /Bounds [ 1 1 {bounds} {{ {pieces} div }} for ] >> >> shfill" ) }; - // A few pieces: the bounds are kept exactly as stops (three bounds -> five stops with the ends). - assert_eq!(stitch_stops(&stitched(3)), vec![0.0, 0.25, 0.5, 0.75, 1.0]); - // At the cap (1024 bounds, 1025 pieces) the 1024 bounds and the two ends are all kept. - assert_eq!(stitch_stops(&stitched(1024)).len(), 1026); + // A few pieces: the bounds are kept exactly as stops. Each piece runs from 0 to 1, so at every + // bound the colour jumps: two stops there (three bounds -> eight stops with the ends). + assert_eq!(stitch_stops(&stitched(3)), vec![0.0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1.0]); + // At the cap (1024 bounds, 1025 pieces) the 1024 bounds (two stops each) and the two ends are + // all kept. + assert_eq!(stitch_stops(&stitched(1024)).len(), 2 * 1024 + 2); // One past it (1025 bounds, 1026 pieces) the function is sampled instead. assert_eq!(stitch_stops(&stitched(1025)).len(), 33); }