diff --git a/crates/plate-test/README.md b/crates/plate-test/README.md index 11e071a9..6a376f19 100644 --- a/crates/plate-test/README.md +++ b/crates/plate-test/README.md @@ -52,6 +52,7 @@ Paints: | Several inks (DeviceN) | `{"inks": {"Cyan": 60, "Black": 40}}` | | Registration (`/All`) | `"registration"` | | Linear gradient | `{"linear": {"from": [x, y], "to": [x, y], "stops": [{"at": 0, "paint": …}, …]}}` | +| Mesh gradient (one Coons patch with straight edges) | `{"mesh": {"rect": [x, y, w, h], "corners": [paint, paint, paint, paint]}}` (top left, top right, bottom right, bottom left) | Images are synthetic: `{"px": [w, h], "ramp": [paint, paint]}` (left to right), `"split": [paint, paint]` (left and right halves) or `"feather": pt` (a gray soft-mask image, diff --git a/crates/plate-test/scenes/S17-mesh-spot.json b/crates/plate-test/scenes/S17-mesh-spot.json new file mode 100644 index 00000000..dcd57a94 --- /dev/null +++ b/crates/plate-test/scenes/S17-mesh-spot.json @@ -0,0 +1,146 @@ +{ + "id": "S17", + "title": "Mesh gradient with a spot, process and DeviceN corner", + "source": "storytold/craft-libs#184 (mesh gradients), CodeRabbit review", + "page": [ + 240, + 160 + ], + "inks": [ + { + "name": "Gold", + "cmyk": [ + 0, + 20, + 60, + 20 + ] + } + ], + "objects": [ + { + "type": "rect", + "rect": [ + 20, + 20, + 200, + 120 + ], + "fill": { + "mesh": { + "rect": [ + 20, + 20, + 200, + 120 + ], + "corners": [ + { + "spot": "Gold", + "tint": 100 + }, + { + "cmyk": [ + 100, + 0, + 0, + 0 + ] + }, + { + "inks": { + "Magenta": 100, + "Gold": 50 + } + }, + { + "cmyk": [ + 0, + 0, + 0, + 0 + ] + } + ] + } + } + } + ], + "expect": { + "plates": [ + "Cyan", + "Magenta", + "Yellow", + "Black", + "Gold" + ], + "points": [ + { + "at": [ + 24, + 24 + ], + "inks": { + "Gold": 95 + }, + "tol": 10, + "note": "top left: Gold" + }, + { + "at": [ + 216, + 24 + ], + "inks": { + "Cyan": 95 + }, + "tol": 10, + "note": "top right: Cyan" + }, + { + "at": [ + 216, + 136 + ], + "inks": { + "Magenta": 95, + "Gold": 47 + }, + "tol": 10, + "note": "bottom right: Magenta and half Gold" + }, + { + "at": [ + 24, + 136 + ], + "inks": {}, + "tol": 10, + "note": "bottom left: paper" + }, + { + "at": [ + 120, + 80 + ], + "inks": { + "Gold": 37.5, + "Cyan": 25, + "Magenta": 25 + }, + "tol": 10, + "note": "center: bilinear mix of the corners" + } + ] + }, + "structure": { + "page_group": "none", + "smask": false, + "color_spaces_include": [ + "DeviceN(Cyan,Magenta,Gold, alt DeviceCMYK)" + ], + "device_rgb": false, + "output_intents": [] + }, + "note": "Corner colors in different ink spaces share one DeviceN space: process colorants first, then the spot." +} diff --git a/crates/plate-test/src/reference.rs b/crates/plate-test/src/reference.rs index 78e98fb7..c255e8cf 100644 --- a/crates/plate-test/src/reference.rs +++ b/crates/plate-test/src/reference.rs @@ -20,7 +20,7 @@ use krilla::image::{BitsPerComponent, CustomImage, Image, ImageColorspace}; use krilla::mask::{Mask as KrillaMask, MaskType, TransferFunction}; use krilla::num::NormalizedF32; use krilla::page::PageSettings; -use krilla::paint::{Fill, LinearGradient, Paint as KrillaPaint, SpreadMethod, Stop, Stroke as KrillaStroke}; +use krilla::paint::{Fill, LinearGradient, MeshGradient, MeshPatch, Paint as KrillaPaint, SpreadMethod, Stop, Stroke as KrillaStroke}; use krilla::pdf::{Pdf, PdfDocument}; use krilla::surface::Surface; use krilla::text::{Font, TextDirection}; @@ -318,12 +318,7 @@ impl Resolver<'_, '_> { fn paint(&self, paint: &Paint) -> Result { match paint { Paint::Linear { linear } => { - // krilla brings ink-based stops into one space; RGB and registration don't mix. - let space = linear.stops.first().map(|s| space_of(&s.paint)); - let mixed = linear.stops.iter().any(|s| Some(space_of(&s.paint)) != space); - if mixed && linear.stops.iter().any(|s| matches!(s.paint, Paint::Rgb { .. } | Paint::Named(_))) { - return Err(unsupported("gradient between RGB or registration and other color spaces")); - } + mixable(linear.stops.iter().map(|s| &s.paint))?; let mut stops = Vec::new(); for stop in &linear.stops { let offset = NormalizedF32::new(stop.at).ok_or_else(|| self.bad("gradient stop outside 0–1"))?; @@ -341,6 +336,23 @@ impl Resolver<'_, '_> { } .into()) } + Paint::Mesh { mesh } => { + mixable(mesh.corners.iter())?; + let [x, y, w, h] = mesh.rect; + let corners = [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]; + let mut points = [(0.0, 0.0); 12]; + for (i, &(ax, ay)) in corners.iter().enumerate() { + let (bx, by) = corners[(i + 1) % 4]; + for step in 0..3 { + let t = step as f32 / 3.0; + points[3 * i + step] = (ax + (bx - ax) * t, ay + (by - ay) * t); + } + } + let [a, b, c, d] = &mesh.corners; + let colors = [self.color(a)?, self.color(b)?, self.color(c)?, self.color(d)?]; + let patch = MeshPatch { points, colors, opacities: [NormalizedF32::ONE; 4] }; + Ok(MeshGradient { patches: vec![patch], transform: Transform::identity(), anti_alias: false }.into()) + } solid => Ok(self.color(solid)?.into()), } } @@ -359,7 +371,7 @@ impl Resolver<'_, '_> { separation::Color::new(byte(*tint), SeparationSpace::new(SeparationColorant::Custom(spot.clone()), alternate)).into() } Paint::Inks { inks } => self.device_n(inks)?.into(), - Paint::Linear { .. } => return Err(self.bad("gradients can't nest")), + Paint::Linear { .. } | Paint::Mesh { .. } => return Err(self.bad("gradients can't nest")), }) } @@ -457,7 +469,7 @@ impl Resolver<'_, '_> { Paint::Rgb { rgb } => (ImageColorspace::Rgb, rgb.to_vec()), Paint::Inks { .. } | Paint::Spot { .. } => return Err(self.bad("image paints must share a color space")), Paint::Named(_) => return Err(unsupported("registration image")), - Paint::Linear { .. } => return Err(self.bad("images take solid paints")), + Paint::Linear { .. } | Paint::Mesh { .. } => return Err(self.bad("images take solid paints")), }) } @@ -475,6 +487,16 @@ enum Space<'a> { Other, } +/// krilla brings ink-based gradient colors into one space; RGB and registration don't mix. +fn mixable<'a>(paints: impl Iterator + Clone) -> Result<(), Error> { + let space = paints.clone().next().map(space_of); + let mixed = paints.clone().any(|p| Some(space_of(p)) != space); + if mixed && paints.into_iter().any(|p| matches!(p, Paint::Rgb { .. } | Paint::Named(_))) { + return Err(unsupported("gradient between RGB or registration and other color spaces")); + } + Ok(()) +} + fn space_of(paint: &Paint) -> Space<'_> { match paint { Paint::Cmyk { .. } => Space::Cmyk, diff --git a/crates/plate-test/src/scene.rs b/crates/plate-test/src/scene.rs index f4e4968a..ad065764 100644 --- a/crates/plate-test/src/scene.rs +++ b/crates/plate-test/src/scene.rs @@ -169,6 +169,9 @@ pub enum Paint { Linear { linear: Gradient, }, + Mesh { + mesh: Box, + }, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] @@ -190,6 +193,15 @@ pub struct Gradient { pub stops: Vec, } +/// A mesh gradient of one Coons patch with straight edges over `rect` (page coordinates). +#[derive(Debug, Clone, PartialEq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Mesh { + pub rect: [f32; 4], + /// Solid paints at the top-left, top-right, bottom-right and bottom-left corners. + pub corners: [Paint; 4], +} + #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] pub struct GradientStop { @@ -520,12 +532,20 @@ impl Scene { out.push("gradients need at least two stops".into()); } for stop in &linear.stops { - if matches!(stop.paint, Paint::Linear { .. }) { + if matches!(stop.paint, Paint::Linear { .. } | Paint::Mesh { .. }) { out.push("gradient stops must be solid paints".into()); } self.paint_problems(&stop.paint, out); } } + Paint::Mesh { mesh } => { + for corner in &mesh.corners { + if matches!(corner, Paint::Linear { .. } | Paint::Mesh { .. }) { + out.push("mesh corners must be solid paints".into()); + } + self.paint_problems(corner, out); + } + } _ => {} } }