EPS import: a stitched shading keeps its hard colour changes (#1653)

A stitching function's bound became one stop, evaluated at the bound:
the right piece's colour. Where the pieces don't meet, the left piece's
end colour was lost. The colour just before the bound is now a stop at
the same offset too, ahead of the one after it, when the two differ;
continuous stitches keep one stop per bound as before.

Fixes #1653
This commit is contained in:
Gert Pellin 2026-10-13 08:44:20 +02:00 committed by bflatastic
parent 85335e9e14
commit 3412d3a073
3 changed files with 40 additions and 12 deletions

View File

@ -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<f64> = 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<f64> = 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));

View File

@ -292,6 +292,19 @@ fn shadings_become_gradients() {
assert_eq!(g.gradient.kind, GradientKind::Radial);
let offsets: Vec<f32> = 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

View File

@ -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);
}