mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 16:38:07 +00:00
Merge pull request #6317 from dotCooCoo/fix/pdf-cmyk-gradient-image
PDF export: dithered and Perceptual CMYK gradients stay CMYK
This commit is contained in:
@@ -456,6 +456,41 @@ impl Gradient {
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
/// A gradient whose stops are all CMYK as a ramp of C, M, Y, K and opacity: the stops' own
|
||||
/// inks, an off-center midpoint halfway between its two stops' inks, and a Perceptual
|
||||
/// gradient's curve as [`Self::ink_mixes`] mixes the inks. `None` when a stop is in another
|
||||
/// model, there are no stops, or [`Self::ink_mixes`] gives none for the curve.
|
||||
fn ink_ramp(&self) -> Option<Ramp<5>> {
|
||||
let inks: Vec<[f32; 4]> = self
|
||||
.stops
|
||||
.iter()
|
||||
.map(|s| match s.color {
|
||||
Color::Cmyk { c, m, y, k } => Some([c, m, y, k]),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<_>>()?;
|
||||
if inks.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let entry = |i: usize, w: f32, o: f32| {
|
||||
let (a, b) = (inks[i], inks.get(i + 1).copied().unwrap_or(inks[i]));
|
||||
let [c, m, y, k]: [f32; 4] = std::array::from_fn(|n| a[n] + (b[n] - a[n]) * w);
|
||||
[c, m, y, k, o]
|
||||
};
|
||||
if !self.method.is_classic() {
|
||||
return Some(Ramp { stops: self.ink_mixes()?.into_iter().map(|(t, i, w, o)| (t, entry(i, w, o))).collect() });
|
||||
}
|
||||
let mut stops = Vec::with_capacity(inks.len());
|
||||
for (i, s) in self.stops.iter().enumerate() {
|
||||
stops.push((s.offset, entry(i, 0.0, s.opacity)));
|
||||
if let Some(n) = self.stops.get(i + 1)
|
||||
&& (s.midpoint - 0.5).abs() > 1e-3
|
||||
{
|
||||
stops.push((s.offset + (n.offset - s.offset) * s.midpoint, entry(i, 0.5, (s.opacity + n.opacity) / 2.0)));
|
||||
}
|
||||
}
|
||||
Some(Ramp { stops })
|
||||
}
|
||||
/// The same ramp the other way round: the stops in reverse order at mirrored offsets, each
|
||||
/// segment's midpoint mirrored with it.
|
||||
pub fn reverse(&mut self) {
|
||||
@@ -895,19 +930,45 @@ impl GradientPaint {
|
||||
fill: bool,
|
||||
rgb: &dyn Fn(&Color) -> [f32; 3],
|
||||
) -> impl Iterator<Item = [u8; 4]> + use<> {
|
||||
self.grid(Ramp::new(&self.gradient, rgb), bounds, area, cols, rows, cells, fill, false)
|
||||
}
|
||||
/// [`Self::pixels`] of a linear or radial gradient whose stops are all CMYK, as C, M, Y, K
|
||||
/// and opacity levels mixed from the stops' inks; `None` when a stop is in another model,
|
||||
/// there are no stops, or a Perceptual curve has too many stops for [`Gradient::ink_mixes`].
|
||||
/// Dither's noise adds no ink where the ramp has none.
|
||||
pub fn ink_pixels(&self, bounds: Rect, area: Rect, cols: u16, rows: u16, fill: bool) -> Option<impl Iterator<Item = [u8; 5]> + use<>> {
|
||||
let ramp = self.gradient.ink_ramp()?;
|
||||
Some(self.grid(ramp, bounds, area, cols, rows, [0..cols, 0..rows], fill, true))
|
||||
}
|
||||
/// `ramp` sampled at each cell's center in columns `cells[0]` and rows `cells[1]` of the grid,
|
||||
/// its last channel (opacity) rounded and each other channel with dither's noise, except
|
||||
/// (`inks`) where that channel is empty.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn grid<const N: usize>(
|
||||
&self,
|
||||
ramp: Ramp<N>,
|
||||
bounds: Rect,
|
||||
area: Rect,
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
cells: [std::ops::Range<u16>; 2],
|
||||
fill: bool,
|
||||
inks: bool,
|
||||
) -> impl Iterator<Item = [u8; N]> + use<N> {
|
||||
let (kind, geom, dither) = (self.gradient.kind, self.resolve(bounds), fill && self.gradient.dither);
|
||||
let ramp = Ramp::new(&self.gradient, rgb);
|
||||
let (cw, ch) = (area.width() / f64::from(cols.max(1)), area.height() / f64::from(rows.max(1)));
|
||||
let (x0, y0) = (area.x0, area.y0);
|
||||
let [across, down] = cells.map(|r| u32::from(r.start)..u32::from(r.end));
|
||||
down.flat_map(move |j| across.clone().map(move |i| (i, j))).map(move |(i, j)| {
|
||||
let p = Point::new(x0 + (f64::from(i) + 0.5) * cw, y0 + (f64::from(j) + 0.5) * ch);
|
||||
let c = ramp.at(geom.param_at(kind, p) as f32);
|
||||
let level = |k: usize| {
|
||||
let noise = if dither { dither_noise(i, j, k as u32) } else { 0.0 };
|
||||
std::array::from_fn(|k| {
|
||||
if k + 1 == N {
|
||||
return (c[k].clamp(0.0, 1.0) * 255.0).round() as u8;
|
||||
}
|
||||
let noise = if dither && !(inks && c[k] <= 0.0) { dither_noise(i, j, k as u32) } else { 0.0 };
|
||||
(c[k].clamp(0.0, 1.0) * 255.0 + noise).round().clamp(0.0, 255.0) as u8
|
||||
};
|
||||
[level(0), level(1), level(2), (c[3].clamp(0.0, 1.0) * 255.0).round() as u8]
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -916,9 +977,9 @@ impl GradientPaint {
|
||||
/// (display RGB, or an ink's coverage): its stops, midpoints and a Perceptual gradient's
|
||||
/// in-between colours as stops (as [`Gradient::expanded_stops`]), each turned into that space
|
||||
/// once, with colour and opacity mixed linearly between them, each on its own (as the renderer's
|
||||
/// vector gradients mix them).
|
||||
pub struct Ramp {
|
||||
stops: Vec<(f32, [f32; 4])>,
|
||||
/// vector gradients mix them). `N` counts the channels, with opacity last.
|
||||
pub struct Ramp<const N: usize = 4> {
|
||||
stops: Vec<(f32, [f32; N])>,
|
||||
}
|
||||
|
||||
impl Ramp {
|
||||
@@ -932,10 +993,13 @@ impl Ramp {
|
||||
.collect();
|
||||
Self { stops }
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Ramp<N> {
|
||||
/// Unpremultiplied colour and opacity at `t`, padded past the ends (transparent black
|
||||
/// without stops).
|
||||
pub fn at(&self, t: f32) -> [f32; 4] {
|
||||
let Some(&(t0, first)) = self.stops.first() else { return [0.0; 4] };
|
||||
pub fn at(&self, t: f32) -> [f32; N] {
|
||||
let Some(&(t0, first)) = self.stops.first() else { return [0.0; N] };
|
||||
// Not after the first stop (or not a number): its colour.
|
||||
if t.partial_cmp(&t0) != Some(std::cmp::Ordering::Greater) {
|
||||
return first;
|
||||
@@ -947,7 +1011,7 @@ impl Ramp {
|
||||
return std::array::from_fn(|k| ca[k] + (cb[k] - ca[k]) * u);
|
||||
}
|
||||
}
|
||||
self.stops.last().map_or([0.0; 4], |s| s.1)
|
||||
self.stops.last().map_or([0.0; N], |s| s.1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -604,3 +604,96 @@ fn ink_mixes_use_the_mix_position_when_the_lightness_turns_back() {
|
||||
}
|
||||
assert!(mixes.windows(2).filter(|p| p[1].1 == 0).all(|p| p[0].2 <= p[1].2), "the weights rise: {mixes:?}");
|
||||
}
|
||||
|
||||
fn cmyk_stop(offset: f32, c: f32, m: f32, y: f32, k: f32) -> GradientStop {
|
||||
GradientStop::new(offset, Color::cmyk(c, m, y, k))
|
||||
}
|
||||
|
||||
/// The `n` × 1 grid of `g` as a left-to-right linear gradient's ink pixels.
|
||||
fn ink_row(g: Gradient, n: u16, fill: bool) -> Vec<[u8; 5]> {
|
||||
let b = Rect::new(0.0, 0.0, f64::from(n), 1.0);
|
||||
GradientPaint::new(g).ink_pixels(b, b, n, 1, fill).expect("CMYK stops").collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_pixels_mix_the_stops_inks_in_cmyk() {
|
||||
let mut g = Gradient::new(GradientKind::Linear, vec![cmyk_stop(0.0, 1.0, 0.0, 0.0, 0.0), cmyk_stop(1.0, 0.0, 0.0, 0.0, 1.0)]);
|
||||
g.stops[1].opacity = 0.5;
|
||||
let px = ink_row(g.clone(), 10, true);
|
||||
let at = |x: f32| (x * 255.0).round() as u8;
|
||||
for (i, p) in px.iter().enumerate() {
|
||||
let t = (i as f32 + 0.5) / 10.0;
|
||||
assert_eq!(*p, [at(1.0 - t), 0, 0, at(t), at(1.0 - t * 0.5)], "pixel {i}");
|
||||
}
|
||||
// An off-center midpoint is halfway between the two stops' inks, not a display RGB mix.
|
||||
g.stops[0].midpoint = 0.25;
|
||||
let px = ink_row(g.clone(), 4, false);
|
||||
assert_eq!(&px[0][..4], &[at(0.75), 0, 0, at(0.25)], "halfway to the midpoint");
|
||||
assert_eq!(&px[1][..4], &[at(0.5 * 5.0 / 6.0), 0, 0, at(0.5 + 0.5 / 6.0)], "a sixth of the way past it");
|
||||
// A hard stop keeps both sides.
|
||||
let hard = Gradient::new(
|
||||
GradientKind::Linear,
|
||||
vec![
|
||||
cmyk_stop(0.0, 1.0, 0.0, 0.0, 0.0),
|
||||
cmyk_stop(0.5, 1.0, 0.0, 0.0, 0.0),
|
||||
cmyk_stop(0.5, 0.0, 1.0, 0.0, 0.0),
|
||||
cmyk_stop(1.0, 0.0, 1.0, 0.0, 0.0),
|
||||
],
|
||||
);
|
||||
let px = ink_row(hard, 4, false);
|
||||
assert_eq!((&px[1][..4], &px[2][..4]), (&[255, 0, 0, 0][..], &[0, 255, 0, 0][..]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_pixels_dither_only_the_inks_the_ramp_has() {
|
||||
let mut g = Gradient::new(GradientKind::Linear, vec![cmyk_stop(0.0, 0.2, 0.0, 0.0, 0.4), cmyk_stop(1.0, 0.8, 0.0, 0.0, 0.4)]);
|
||||
g.dither = true;
|
||||
let noisy = ink_row(g.clone(), 64, true);
|
||||
let plain = ink_row(g.clone(), 64, false);
|
||||
assert_ne!(noisy, plain, "a fill has noise, a stroke none");
|
||||
for (n, p) in noisy.iter().zip(&plain) {
|
||||
assert_eq!((n[1], n[2]), (0, 0), "no ink on the empty plates: {n:?}");
|
||||
assert!(n[0].abs_diff(p[0]) <= 1 && n[3].abs_diff(p[3]) <= 1, "{n:?} {p:?}");
|
||||
assert_eq!(n[4], 255, "opacity has no noise");
|
||||
}
|
||||
assert!(noisy.iter().zip(&plain).any(|(n, p)| n[3] != p[3]), "K gets its own noise");
|
||||
assert!(noisy.iter().zip(&plain).any(|(n, p)| (n[0] != p[0]) != (n[3] != p[3])), "the noise differs from channel to channel");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_pixels_follow_the_perceptual_curve_as_ink_mixes() {
|
||||
let mut g = Gradient::new(GradientKind::Linear, vec![cmyk_stop(0.0, 0.0, 0.0, 0.0, 0.0), cmyk_stop(1.0, 1.0, 0.0, 0.0, 0.0)]);
|
||||
g.method = GradientMethod::Perceptual;
|
||||
let mixes = g.ink_mixes().unwrap();
|
||||
let ramp = g.ink_ramp().unwrap();
|
||||
assert_eq!(ramp.stops.len(), mixes.len());
|
||||
for ((t, ink), (o, i, w, _)) in ramp.stops.iter().zip(&mixes) {
|
||||
assert_eq!((*t, ink[0]), (*o, if *i == 0 { *w } else { 1.0 }), "cyan at {t}");
|
||||
}
|
||||
let px = ink_row(g, 2, false);
|
||||
assert!(px[0][0] > 0 && px[0][0] < px[1][0], "{px:?}");
|
||||
assert!(px.iter().all(|p| p[1] == 0 && p[2] == 0 && p[3] == 0), "{px:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_pixels_need_every_stop_in_cmyk() {
|
||||
let b = Rect::new(0.0, 0.0, 2.0, 1.0);
|
||||
let with = |stop: GradientStop| GradientPaint::new(Gradient::new(GradientKind::Linear, vec![cmyk_stop(0.0, 1.0, 0.0, 0.0, 0.0), stop]));
|
||||
assert!(with(cmyk_stop(1.0, 0.0, 0.0, 0.0, 1.0)).ink_pixels(b, b, 2, 1, true).is_some());
|
||||
for other in [Color::rgb(1.0, 0.0, 0.0), Color::gray(0.5), Color::lab(50.0, 10.0, 10.0)] {
|
||||
assert!(with(GradientStop::new(1.0, other)).ink_pixels(b, b, 2, 1, true).is_none(), "{other:?}");
|
||||
}
|
||||
assert!(GradientPaint::new(Gradient::new(GradientKind::Linear, vec![])).ink_pixels(b, b, 2, 1, true).is_none(), "no stops");
|
||||
}
|
||||
|
||||
/// A Perceptual curve over the limit of [`Gradient::ink_mixes`] has no inks; a Classic gradient of as many stops does.
|
||||
#[test]
|
||||
fn ink_pixels_stop_at_the_ink_mixes_limit() {
|
||||
let stops: Vec<_> = (0..=MAX_INK_TINTS).map(|i| cmyk_stop(i as f32 / MAX_INK_TINTS as f32, (i % 2) as f32, 0.0, 0.0, 0.0)).collect();
|
||||
let classic = Gradient::new(GradientKind::Linear, stops);
|
||||
let perceptual = Gradient { method: GradientMethod::Perceptual, ..classic.clone() };
|
||||
assert!(!perceptual.fits_ink_tints());
|
||||
let b = Rect::new(0.0, 0.0, 2.0, 1.0);
|
||||
assert!(GradientPaint::new(perceptual).ink_pixels(b, b, 2, 1, false).is_none());
|
||||
assert!(GradientPaint::new(classic).ink_pixels(b, b, 2, 1, false).is_some());
|
||||
}
|
||||
|
||||
@@ -435,20 +435,27 @@ impl Exporter<'_> {
|
||||
/// Warn when gradient `g`, drawn as an image, has stops linked to a spot swatch: the image is
|
||||
/// in process colors, so the spot inks have no plate of their own.
|
||||
fn warn_spot_image(&mut self, g: &vectorcraft_color::GradientPaint) {
|
||||
let doc = self.doc;
|
||||
let from_stops = g.gradient.kind != GradientKind::Freeform || !g.has_freeform_points();
|
||||
if from_stops && g.gradient.stops.iter().any(|s| s.swatch.as_deref().is_some_and(|n| doc.swatch(n).is_some_and(|w| w.spot))) {
|
||||
if from_stops && self.has_spot_stop(&g.gradient) {
|
||||
self.warn(
|
||||
"spot colors in gradients exported as images (freeform or dithered gradients, and Perceptual ones with RGB or Lab stops or too many stops) are written in process colors",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Is a stop of `g` linked to a spot swatch?
|
||||
fn has_spot_stop(&self, g: &vectorcraft_color::Gradient) -> bool {
|
||||
let doc = self.doc;
|
||||
g.stops.iter().any(|s| s.swatch.as_deref().is_some_and(|n| doc.swatch(n).is_some_and(|w| w.spot)))
|
||||
}
|
||||
|
||||
/// Draw gradient `g`, placed on `bounds`, as an image over `area`, sampled at the document's
|
||||
/// raster effects resolution where it is drawn (`area` as the current transform sizes it, so
|
||||
/// scaled type gets its pixels too): a freeform gradient's colour field (at most
|
||||
/// [`MAX_FIELD_PX`] a side), or a linear or radial gradient (at most [`MAX_GRADIENT_PX`] a side),
|
||||
/// with dither's noise when it paints a fill (`fill`).
|
||||
/// with dither's noise when it paints a fill (`fill`). A linear or radial gradient whose stops
|
||||
/// are all CMYK and not linked to a spot swatch is a CMYK image, unless its Perceptual curve
|
||||
/// has too many stops; any other is an RGB image.
|
||||
pub(super) fn field_image(&mut self, s: &mut Surface, g: &vectorcraft_color::GradientPaint, bounds: Rect, area: Rect, fill: bool) {
|
||||
use vectorcraft_color::cms::Model;
|
||||
use vectorcraft_color::freeform::{painted_box, spread_scale};
|
||||
@@ -469,24 +476,36 @@ impl Exporter<'_> {
|
||||
let (cols, rows) = (along(w), along(h));
|
||||
let field = freeform.then(|| g.freeform_on(b).model_field(spread_scale(b)));
|
||||
let cmyk_doc = self.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk;
|
||||
let img = match &field {
|
||||
Some(field) if field.model() == Some(Model::Cmyk) || (cmyk_doc && field.model() == Some(Model::Gray)) => {
|
||||
if !freeform {
|
||||
self.warn("linear and radial gradients drawn as pixels are exported as images");
|
||||
}
|
||||
let ramp_inks = if freeform || self.has_spot_stop(&g.gradient) { None } else { g.ink_pixels(b, area, cols, rows, fill) };
|
||||
let ink_image = |inks, alpha| {
|
||||
let convert = if self.convert { self.out.cmyk_pixels() } else { None };
|
||||
crate::images::from_inks(inks, alpha, cols.into(), rows.into(), self.interpolate, convert)
|
||||
};
|
||||
let img = match (&field, ramp_inks) {
|
||||
(Some(field), _) if field.model() == Some(Model::Cmyk) || (cmyk_doc && field.model() == Some(Model::Gray)) => {
|
||||
let (mut inks, mut alpha) = (Vec::new(), Vec::new());
|
||||
for (color, a) in field.grid(area, cols, rows) {
|
||||
let [c, m, y, k] = color.to_cmyk();
|
||||
inks.extend([q(c), q(m), q(y), q(k)]);
|
||||
alpha.push(q(a));
|
||||
}
|
||||
let convert = if self.convert { self.out.cmyk_pixels() } else { None };
|
||||
crate::images::from_inks(inks, alpha, cols.into(), rows.into(), self.interpolate, convert)
|
||||
ink_image(inks, alpha)
|
||||
}
|
||||
(_, Some(pixels)) => {
|
||||
let (mut inks, mut alpha) = (Vec::new(), Vec::new());
|
||||
for [c, m, y, k, a] in pixels {
|
||||
inks.extend([c, m, y, k]);
|
||||
alpha.push(a);
|
||||
}
|
||||
ink_image(inks, alpha)
|
||||
}
|
||||
_ => {
|
||||
let rgba: Vec<u8> = match &field {
|
||||
Some(field) => field.display_grid(area, cols, rows).flat_map(|(c, a)| [q(c[0]), q(c[1]), q(c[2]), q(a)]).collect(),
|
||||
None => {
|
||||
self.warn("linear and radial gradients drawn as pixels are exported as images");
|
||||
g.pixels(b, area, cols, rows, fill, &|c| c.to_rgb()).flatten().collect()
|
||||
}
|
||||
None => g.pixels(b, area, cols, rows, fill, &|c| c.to_rgb()).flatten().collect(),
|
||||
};
|
||||
let pixels = if self.convert { self.out.pixels() } else { None };
|
||||
crate::images::from_rgba_in(rgba, cols.into(), rows.into(), self.interpolate, pixels)
|
||||
|
||||
@@ -8,7 +8,7 @@ use craft_pdf::syntax::Pdf;
|
||||
use craft_pdf::syntax::object::{Array, Name, Object, Stream};
|
||||
use serde_json::json;
|
||||
use vectorcraft_color::cms::{self, DEVICE_CMYK, GENERIC_CMYK, SRGB};
|
||||
use vectorcraft_color::{Color, Freeform, FreeformPoint, Gradient, GradientKind, GradientPaint, Paint};
|
||||
use vectorcraft_color::{Color, Freeform, FreeformPoint, Gradient, GradientKind, GradientMethod, GradientPaint, GradientStop, Paint, Swatch};
|
||||
use vectorcraft_doc::cmyk::Inks;
|
||||
use vectorcraft_doc::{Appearance, ColorMode, Document, ImageBlob, ImageObject, Node, NodeKind};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, shapes};
|
||||
@@ -514,6 +514,103 @@ fn freeform_gradients_of_cmyk_points_are_written_as_cmyk_images() {
|
||||
assert_ne!(w[0].space, "DeviceCMYK", "converted to RGB");
|
||||
}
|
||||
|
||||
/// A 100 × 100 pt document of `mode` whose square is filled with `g`, at one pixel a point.
|
||||
fn gradient_doc(mode: ColorMode, g: Gradient) -> Document {
|
||||
let mut d = Document::new_with_mode(100.0, 100.0, mode);
|
||||
d.raster_effects_ppi = 72.0;
|
||||
let square = Node::path(
|
||||
d.alloc_id(),
|
||||
shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)),
|
||||
Appearance::basic(Paint::Gradient(Box::new(GradientPaint::new(g))), Paint::None, 0.0),
|
||||
);
|
||||
let l = d.layers[0].id;
|
||||
d.insert(Some(l), 0, square).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
/// A gradient of `stops`, dithered or Perceptual.
|
||||
fn drawn_as_pixels(kind: GradientKind, stops: Vec<GradientStop>, dither: bool) -> Gradient {
|
||||
let method = if dither { GradientMethod::Classic } else { GradientMethod::Perceptual };
|
||||
Gradient { dither, method, ..Gradient::new(kind, stops) }
|
||||
}
|
||||
|
||||
fn cyan_to_black() -> Vec<GradientStop> {
|
||||
vec![GradientStop::new(0.0, Color::cmyk(1.0, 0.0, 0.0, 0.0)), GradientStop::new(1.0, Color::cmyk(0.0, 0.0, 0.0, 1.0))]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dithered_and_perceptual_gradients_of_cmyk_stops_are_written_as_cmyk_images() {
|
||||
for mode in [ColorMode::Cmyk, ColorMode::Rgb] {
|
||||
for kind in [GradientKind::Linear, GradientKind::Radial] {
|
||||
for dither in [true, false] {
|
||||
let d = gradient_doc(mode, drawn_as_pixels(kind, cyan_to_black(), dither));
|
||||
let out = export_with_report(&d, &opts(json!({}))).unwrap();
|
||||
let w = written(&out.bytes);
|
||||
assert_eq!(w.len(), 1, "{mode:?} {kind:?} dither {dither}: opaque, no soft mask: {w:?}");
|
||||
assert_eq!(w[0].space, "DeviceCMYK", "{mode:?} {kind:?} dither {dither}");
|
||||
let back = import(&out.bytes).unwrap();
|
||||
let inks = all_images(&back)[0].cmyk().unwrap();
|
||||
assert_eq!((inks.width, inks.height), (100, 100));
|
||||
assert!(inks.data.chunks(4).all(|p| p[1] == 0 && p[2] == 0), "{mode:?} {kind:?} dither {dither}: only cyan and black");
|
||||
if kind == GradientKind::Linear {
|
||||
let row = &inks.data[50 * 400..51 * 400];
|
||||
assert!(close(&row[..4], &[254, 0, 0, 1], 3), "{mode:?} dither {dither}: {:?}", &row[..4]);
|
||||
assert!(close(&row[396..], &[1, 0, 0, 254], 3), "{mode:?} dither {dither}: {:?}", &row[396..]);
|
||||
if dither {
|
||||
assert!(close(&row[50 * 4..50 * 4 + 4], &[127, 0, 0, 128], 3), "{mode:?}: {:?}", &row[50 * 4..50 * 4 + 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Opacity goes in a soft mask, and the inks stay.
|
||||
let mut stops = cyan_to_black();
|
||||
stops[1].opacity = 0.5;
|
||||
let w = written(
|
||||
&export_with_report(&gradient_doc(ColorMode::Cmyk, drawn_as_pixels(GradientKind::Linear, stops, true)), &opts(json!({}))).unwrap().bytes,
|
||||
);
|
||||
assert_eq!(w.len(), 2, "the image and its soft mask: {w:?}");
|
||||
assert!(w.iter().any(|i| i.space == "DeviceCMYK") && w.iter().any(|i| i.space == "DeviceGray"), "{w:?}");
|
||||
// The Output settings convert the inks like any CMYK image's.
|
||||
let d = gradient_doc(ColorMode::Cmyk, drawn_as_pixels(GradientKind::Linear, cyan_to_black(), true));
|
||||
let w = written(&export_with_report(&d, &opts(json!({"output": {"conversion": "destination", "destination": SRGB}}))).unwrap().bytes);
|
||||
assert_eq!(w.len(), 1, "{w:?}");
|
||||
assert_ne!(w[0].space, "DeviceCMYK", "converted to RGB");
|
||||
}
|
||||
|
||||
/// A stop in another model, or linked to a spot swatch, keeps the RGB image.
|
||||
#[test]
|
||||
fn gradients_with_a_stop_that_is_not_process_cmyk_stay_rgb_images() {
|
||||
let cyan = || GradientStop::new(0.0, Color::cmyk(1.0, 0.0, 0.0, 0.0));
|
||||
let others = [Color::rgb(1.0, 0.0, 0.0), Color::gray(0.5), Color::lab(50.0, 10.0, 10.0)];
|
||||
for mode in [ColorMode::Cmyk, ColorMode::Rgb] {
|
||||
for other in others {
|
||||
for dither in [true, false] {
|
||||
let d = gradient_doc(mode, drawn_as_pixels(GradientKind::Linear, vec![cyan(), GradientStop::new(1.0, other)], dither));
|
||||
let w = written(&export_with_report(&d, &opts(json!({}))).unwrap().bytes);
|
||||
assert_eq!(w.len(), 1, "{mode:?} {other:?}: {w:?}");
|
||||
assert_eq!(w[0].space, "DeviceRGB", "{mode:?} {other:?} dither {dither}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// A stop of a spot swatch whose color is CMYK: a dithered fill is an RGB image with a warning.
|
||||
let gold = Color::cmyk(0.0, 0.2, 0.8, 0.1);
|
||||
let mut stop = GradientStop::new(1.0, gold);
|
||||
stop.swatch = Some("Gold".into());
|
||||
let mut d = gradient_doc(ColorMode::Cmyk, drawn_as_pixels(GradientKind::Linear, vec![cyan(), stop.clone()], true));
|
||||
d.swatches.push(Swatch { name: "Gold".into(), paint: Paint::solid(gold), global: true, spot: true });
|
||||
let out = export_with_report(&d, &opts(json!({}))).unwrap();
|
||||
let w = written(&out.bytes);
|
||||
assert_eq!(w.len(), 1, "{w:?}");
|
||||
assert_eq!(w[0].space, "DeviceRGB");
|
||||
assert!(out.warnings.iter().any(|m| m.contains("written in process colors")), "{:?}", out.warnings);
|
||||
// A stop of a global process swatch is written as its CMYK color.
|
||||
let mut d = gradient_doc(ColorMode::Cmyk, drawn_as_pixels(GradientKind::Linear, vec![cyan(), stop], true));
|
||||
d.swatches.push(Swatch { name: "Gold".into(), paint: Paint::solid(gold), global: true, spot: false });
|
||||
let w = written(&export_with_report(&d, &opts(json!({}))).unwrap().bytes);
|
||||
assert_eq!(w[0].space, "DeviceCMYK", "{w:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freeform_gradients_of_lab_points_are_written_as_their_lab_mix() {
|
||||
let d = freeform_doc(ColorMode::Rgb, Color::lab(90.0, -60.0, 40.0), Color::lab(30.0, 60.0, -40.0), 1.0);
|
||||
|
||||
Reference in New Issue
Block a user