plate-test: mesh gradient paint and scene S17 (spot, process and DeviceN corners)

This commit is contained in:
aiongg committed 2026-10-20 03:41:18 +00:00
1 parent 60dc2b54ce
commit e5b73eb4cf
4 files changed
+199 -10

No files matched your search

+1
View File
@@ -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,
+146
View File
@@ -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."
}
+31 -9
View File
@@ -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<KrillaPaint, Error> {
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<Item = &'a Paint> + 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,
+21 -1
View File
@@ -169,6 +169,9 @@ pub enum Paint {
Linear {
linear: Gradient,
},
Mesh {
mesh: Box<Mesh>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
@@ -190,6 +193,15 @@ pub struct Gradient {
pub stops: Vec<GradientStop>,
}
/// 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);
}
}
_ => {}
}
}