diff --git a/crates/pdf/src/import_shading.rs b/crates/pdf/src/import_shading.rs index ccd3c1c04..a4954a57a 100644 --- a/crates/pdf/src/import_shading.rs +++ b/crates/pdf/src/import_shading.rs @@ -44,11 +44,16 @@ pub(crate) fn shading_gradient(sp: &ShadingPattern, colors: &mut Colors<'_>, ink let t = i as f32 / N as f32; pts.push((t, sample(t)?.to_rgb())); } + // Every sample dropped since the last stop is checked against the line to the next one, not + // just its neighbours: a gentle curve (an exponential's N) looks straight between neighbours. let mut keep = vec![0usize]; for w in 1..N { - let (Some(a), Some(p), Some(b)) = (keep.last().and_then(|&k| pts.get(k)), pts.get(w), pts.get(w + 1)) else { continue }; - let u = (p.0 - a.0) / (b.0 - a.0).max(1e-6); - let off = (0..3).map(|k| (a.1[k] + (b.1[k] - a.1[k]) * u - p.1[k]).abs()).fold(0.0f32, f32::max); + let (Some(&k), Some(b)) = (keep.last(), pts.get(w + 1)) else { continue }; + let Some(a) = pts.get(k) else { continue }; + let off = pts.get(k + 1..=w).unwrap_or_default().iter().fold(0.0f32, |worst, p| { + let u = (p.0 - a.0) / (b.0 - a.0).max(1e-6); + (0..3).map(|c| (a.1[c] + (b.1[c] - a.1[c]) * u - p.1[c]).abs()).fold(worst, f32::max) + }); if off > 1.5 / 255.0 { keep.push(w); } diff --git a/crates/pdf/src/tests_import_fidelity.rs b/crates/pdf/src/tests_import_fidelity.rs index 933cb1d45..9e50dce00 100644 --- a/crates/pdf/src/tests_import_fidelity.rs +++ b/crates/pdf/src/tests_import_fidelity.rs @@ -422,6 +422,24 @@ fn a_shading_that_does_not_extend_paints_only_between_its_ends() { assert!(b.x0 <= 0.0 && (b.x1 - 80.0).abs() < 0.01, "{b:?}"); } +/// GUARDS: an exponential function with N off 1 (how a gradient midpoint is written) keeps its +/// curve: sampled stops must follow it, not collapse to the two ends because neighbouring samples +/// look straight. +#[test] +fn a_shading_with_an_exponent_keeps_its_curve() { + let shading = "/Shading << /Sh0 << /ShadingType 2 /ColorSpace /DeviceRGB /Coords [0 0 100 0] /Extend [true true] /Function << /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [1 1 1] /N 1.4234 >> >> >>"; + let d = open(&one_page("/Sh0 sh", shading, &[])); + let g = all(&d) + .into_iter() + .find_map(|n| n.appearance.items.iter().find_map(|i| if let Paint::Gradient(g) = i.paint() { Some(g.clone()) } else { None })) + .expect("a gradient"); + assert!(g.gradient.stops.len() > 2, "{:?}", g.gradient.stops); + // Halfway along, the curve is at 0.5^1.4234 ≈ 0.373, well off the straight 0.5. + let (c, _) = g.gradient.sample(0.5); + let v = c.to_rgb()[0]; + assert!((v - 0.5f32.powf(1.4234)).abs() < 0.01, "{v}"); +} + /// A diamond (10–90 × 10–90) clipping `paint`, which fills its bounds: how a shape filled with a /// gradient is written (`W n`, then `sh`). fn clipped_diamond(paint: &str) -> Document {