Gradient: Perceptual PDF help counts stops as written, CMYK documents included

The method help of paint.editGradient and docs/mcp.md said a Perceptual
gradient with spot stops becomes an image in PDF unless every other stop
is paper white, CMYK or gray. PDF export checks each stop's color as it is
written: a CMYK document, or an export that converts colors to a CMYK
destination profile, writes RGB and Lab stops in CMYK, so the gradient
becomes a DeviceN shading. Both texts now say so, and a test covers the
CMYK document and CMYK destination cases.
This commit is contained in:
dotCooCoo
2026-11-05 01:31:24 +07:00
committed by bflatastic
parent 66664020b0
commit 584b9706cc
3 changed files with 35 additions and 6 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Gradient",
["Window", "Gradient"],
None,
"{stroke?: bool (default: the targeted item's kind, else the active proxy), kind?: linear|radial|freeform (freeform places points on each object, coloured along the stops), mode?: points|lines (freeform: how the Gradient tool adds points), method?: classic|perceptual (how the colours between the stops mix: in display RGB, or perceptually (OKLab) so the steps look even; SVG exports draw a Perceptual gradient as an image, and PDF exports do too unless it is linear or radial, has spot color stops, every other stop is paper white, CMYK or gray, it is not a dithered fill and its curve does not have too many stops: then PDF writes tints of one spot ink as a Separation shading in that ink, and spot inks mixed with other spot, CMYK or gray inks as a DeviceN shading; freeform gradients mix their own way), stops?: [{offset 0..1, color? (needed without swatch; #rrggbbaa and #rgba are refused), opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: colour swatch name (a global or spot colour or tint swatch links the stop, so swatch edits recolour it and a spot stop prints on its plate; a process colour just gives its colour), tint?: 0..100 (% of the linked swatch; default 100, or a tint swatch's own)}] (at least 2; a freeform gradient's points are recoloured along them), keepModel?: bool (as paint.setFill; a stop color the gradient already has keeps its model), angle?: deg, aspect?: %, reverse?: bool, dither?: bool (fills only: a linear or radial gradient fill is drawn with fine noise that hides banding, and as pixels in SVG and PDF exports), item?: fill/stroke item index|null (omitted: the Appearance panel's active item when it is of the edited kind), ids?|id?, strokeMode?: within|along|across (strokes only: the gradient lies on the page and shows through the stroke, runs from the start of each subpath to its end, or runs from the stroke's left edge to its right all along it; with nothing selected, for the next object drawn; type characters' own strokes always paint within), target?: \"object\"|\"contents\"} edit the gradient in place (keeps its placement); solid/none paints become the default gradient",
"{stroke?: bool (default: the targeted item's kind, else the active proxy), kind?: linear|radial|freeform (freeform places points on each object, coloured along the stops), mode?: points|lines (freeform: how the Gradient tool adds points), method?: classic|perceptual (how the colours between the stops mix: in display RGB, or perceptually (OKLab) so the steps look even; SVG exports draw a Perceptual gradient as an image, and PDF exports do too unless it is linear or radial, has spot color stops, every other stop is paper white or written in CMYK or gray (a CMYK document, or an export that converts colors to a CMYK destination profile, writes RGB and Lab stops in CMYK), it is not a dithered fill and its curve does not have too many stops: then PDF writes tints of one spot ink as a Separation shading in that ink, and spot inks mixed with other spot, CMYK or gray inks as a DeviceN shading; freeform gradients mix their own way), stops?: [{offset 0..1, color? (needed without swatch; #rrggbbaa and #rgba are refused), opacity? 0..1 (or 0..100), midpoint? 0.13..0.87, swatch?: colour swatch name (a global or spot colour or tint swatch links the stop, so swatch edits recolour it and a spot stop prints on its plate; a process colour just gives its colour), tint?: 0..100 (% of the linked swatch; default 100, or a tint swatch's own)}] (at least 2; a freeform gradient's points are recoloured along them), keepModel?: bool (as paint.setFill; a stop color the gradient already has keeps its model), angle?: deg, aspect?: %, reverse?: bool, dither?: bool (fills only: a linear or radial gradient fill is drawn with fine noise that hides banding, and as pixels in SVG and PDF exports), item?: fill/stroke item index|null (omitted: the Appearance panel's active item when it is of the edited kind), ids?|id?, strokeMode?: within|along|across (strokes only: the gradient lies on the page and shows through the stroke, runs from the start of each subpath to its end, or runs from the stroke's left edge to its right all along it; with nothing selected, for the next object drawn; type characters' own strokes always paint within), target?: \"object\"|\"contents\"} edit the gradient in place (keeps its placement); solid/none paints become the default gradient",
has_doc,
edit_gradient
),
+27
View File
@@ -290,6 +290,33 @@ fn perceptual_spot_gradients_mixed_with_other_inks_export_as_device_n_shadings()
assert!(!pdf.contains("/Subtype/Image") && pdf.contains("/ShadingType 3") && pdf.contains("/DeviceN[/Cyan/Ink]/DeviceCMYK"), "radial");
}
#[test]
fn perceptual_spot_gradients_with_rgb_stops_in_a_cmyk_document_export_as_device_n_shadings() {
let (mut s, id) = setup();
run(&mut s, "paint.editGradient", json!({"stops": [{"offset": 0, "swatch": "Ink"}, {"offset": 1, "color": "#00ff00"}], "method": "perceptual"}));
assert!(gradient(&s, id).gradient.stops.iter().any(|st| matches!(st.color, Color::Rgb { .. })), "an RGB stop");
let (pdf, _) = pdf_of(&s);
assert!(pdf.contains("/Subtype/Image"), "an image in an RGB document");
// A CMYK document, or a conversion to a CMYK destination, writes the RGB stop in CMYK: a
// DeviceN shading, not an image.
let mut cmyk = doc(&s).clone();
cmyk.color_mode = vectorcraft_doc::ColorMode::Cmyk;
let plain = vectorcraft_pdf::PdfOptions { created: Some(0), ..vectorcraft_pdf::PdfOptions::uncompressed() };
let mut converted = plain.clone();
converted.settings.output = vectorcraft_pdf::OutputSettings {
conversion: vectorcraft_pdf::ColorConversion::Destination,
destination: vectorcraft_pdf::DOCUMENT_CMYK.into(),
..Default::default()
};
for (what, d, opts) in [("CMYK document", &cmyk, &plain), ("CMYK destination", doc(&s), &converted)] {
let r = vectorcraft_pdf::export_with_report(d, opts).unwrap();
let pdf = String::from_utf8_lossy(&r.bytes);
assert!(!pdf.contains("/Subtype/Image"), "{what}: no image");
assert!(pdf.contains("/ShadingType 2") && pdf.contains("/Ink]/DeviceCMYK"), "{what}: a DeviceN shading with Ink");
assert!(!r.warnings.iter().any(|w| w.contains("spot")), "{what}: {:?}", r.warnings);
}
}
#[test]
fn spot_gradients_drawn_as_images_warn() {
let (mut s, _) = setup();
+7 -5
View File
@@ -925,9 +925,10 @@ Method (the Gradient panel's menu): `classic` (the default) mixes the colours be
`perceptual` mixes a linear or radial gradient's colours in OKLab so the steps look even. SVG exports write a
Perceptual gradient as an image at the Document Raster Effects resolution (a dithered Perceptual fill with its noise).
PDF exports write it the same way, except for a linear or radial gradient with spot color stops whose other stops
are all paper white, CMYK or gray, that is not a dithered fill and whose curve does not have too many stops. PDF
writes tints of one spot ink as a Separation shading in that ink, and spot inks mixed with other spot, CMYK or gray
inks as a DeviceN shading.
are all paper white or written in CMYK or gray, that is not a dithered fill and whose curve does not have too many
stops. A CMYK document, or an export that converts colors to a CMYK destination profile, writes RGB and Lab stops in
CMYK. PDF writes tints of one spot ink as a Separation shading in that ink, and spot inks mixed with other spot, CMYK
or gray inks as a DeviceN shading.
`paint.setFill`/`paint.setStroke` take it in `gradient` too:
```json
@@ -2786,8 +2787,9 @@ give it `swatch` (and `tint` %, default 100 or a tint swatch's own) instead of `
{stops}` or a `gradient` paint's stops. Editing or deleting the swatch then recolours or unlinks the stop, in art and
in gradient swatches. A spot stop separates on its plate. A linear or radial gradient whose stops are all tints of one spot ink (or
paper white, 0 %) exports to PDF as a Separation shading, and one that mixes spot inks with other spot, CMYK or gray
inks exports as a DeviceN shading. A gradient that mixes a spot ink with RGB or Lab colors is written in process
colors, with a warning. Freeform gradients and dithered fills export to PDF as images in process colors, and Method
inks exports as a DeviceN shading. A gradient that mixes a spot ink with colors written in RGB or Lab is written in
process colors, with a warning; a CMYK document, or an export that converts colors to a CMYK destination profile,
writes RGB and Lab stops in CMYK. Freeform gradients and dithered fills export to PDF as images in process colors, and Method
above covers Perceptual gradients.
```json