PDF export: a Perceptual gradient that mixes spot and process inks keeps its inks

A Perceptual linear or radial gradient that mixes spot colors with each
other or with CMYK or gray stops was written as an image in process
colors. It is now written as a DeviceN shading of its inks, with the
stops on its Perceptual curve: at each one, the inks of the two stops on
either side are mixed at the weight where the two colors mixed in
display RGB have the OKLab lightness of the color on the curve.

- Gradient::ink_mixes gives the curve's stops as weights between stops,
  under the same 4,096-stop limit as ink_tints.
- Perceptual gradients that mix a spot color with RGB or Lab stops,
  dithered fills and freeform gradients stay images, and the warning
  now names those cases.
This commit is contained in:
dotCooCoo
2026-11-01 11:09:09 +07:00
committed by bflatastic
parent 7e5e099f8e
commit 38bb213917
7 changed files with 180 additions and 29 deletions
+30 -2
View File
@@ -236,11 +236,39 @@ fn perceptual_spot_gradients_export_as_separation_shadings() {
assert!(pdf.contains("/C0[1]") && pdf.contains("/C1[0.4"), "white ink at 100% to 40%: {}", &pdf[at..(at + 200).min(pdf.len())]);
}
#[test]
fn perceptual_spot_gradients_mixed_with_other_inks_export_as_device_n_shadings() {
let (mut s, _) = setup();
run(&mut s, "swatch.new", json!({"name": "Leaf", "color": {"c": 1, "m": 0, "y": 1, "k": 0}, "spot": true}));
let ink_to_cyan = json!({"stops": [{"offset": 0, "swatch": "Ink"}, {"offset": 1, "color": {"c": 1, "m": 0, "y": 0, "k": 0}}]});
let ink_to_leaf = json!({"stops": [{"offset": 0, "swatch": "Ink", "midpoint": 30}, {"offset": 1, "swatch": "Leaf", "tint": 50}]});
for (what, edit, space) in [
("Ink → cyan", with(ink_to_cyan.clone(), json!({"method": "perceptual"})), "/DeviceN[/Cyan/Ink]/DeviceCMYK"),
("Ink → Leaf 50 %", with(ink_to_leaf, json!({"method": "perceptual"})), "/DeviceN[/Ink/Leaf]/DeviceCMYK"),
] {
run(&mut s, "paint.editGradient", edit);
let (pdf, warnings) = pdf_of(&s);
assert!(!pdf.contains("/Subtype/Image"), "{what}: no image");
assert!(pdf.contains("/ShadingType 2") && pdf.contains(space), "{what}: a DeviceN shading");
// The curve's stops: a stitching function of more than the two ends.
assert!(pdf.contains("/FunctionType 3"), "{what}: inks on the curve");
assert!(!warnings.iter().any(|w| w.contains("spot")), "{what}: {warnings:?}");
}
// The curve starts at the first stop's inks: no cyan, Ink at 100 %.
run(&mut s, "paint.editGradient", with(ink_to_cyan.clone(), json!({"method": "perceptual"})));
let (pdf, _) = pdf_of(&s);
assert!(pdf.contains("/C0[0 1]"), "the first stop's inks");
// A radial one too.
run(&mut s, "paint.editGradient", with(ink_to_cyan, json!({"method": "perceptual", "kind": "radial"})));
let (pdf, _) = pdf_of(&s);
assert!(!pdf.contains("/Subtype/Image") && pdf.contains("/ShadingType 3") && pdf.contains("/DeviceN[/Cyan/Ink]/DeviceCMYK"), "radial");
}
#[test]
fn spot_gradients_drawn_as_images_warn() {
let (mut s, _) = setup();
let spot_warning = |w: &[String]| w.iter().any(|w| w.contains("spot colors in gradients exported as images"));
let ink_to_cyan = json!({"stops": [{"offset": 0, "swatch": "Ink"}, {"offset": 1, "color": {"c": 1, "m": 0, "y": 0, "k": 0}}]});
let ink_to_green = json!({"stops": [{"offset": 0, "swatch": "Ink"}, {"offset": 1, "color": "#00ff00"}]});
// More stops than the export finds tints for: an image, as before.
let n = 4097;
let stops: Vec<Value> = (0..n)
@@ -250,7 +278,7 @@ fn spot_gradients_drawn_as_images_warn() {
})
.collect();
for (what, edit) in [
("Perceptual Ink → cyan", with(ink_to_cyan, json!({"method": "perceptual"}))),
("Perceptual Ink → RGB green", with(ink_to_green, json!({"method": "perceptual"}))),
("dithered Classic Ink → paper", with(ink_to_white(100.0), json!({"dither": true, "method": "classic"}))),
("dithered Perceptual Ink → paper", with(ink_to_white(100.0), json!({"dither": true, "method": "perceptual"}))),
("Perceptual Ink and paper, 4,097 stops", json!({"stops": stops, "dither": false, "method": "perceptual"})),