Merge branch 'print-color' into print-color-separations

This commit is contained in:
aiongg
2026-10-23 08:35:36 +00:00
4 changed files with 62 additions and 14 deletions
+8 -2
View File
@@ -1006,7 +1006,12 @@ impl Exporter<'_> {
for (si, gp) in designcraft_compose::path_glyphs(&cs, ft, &bp, pt) {
let st = &cs.styles[si as usize];
// A gradient spans the path's box.
let fill = designcraft_doc::Fill { swatch: st.fill.clone(), tint: st.fill_tint, ..designcraft_doc::Fill::none() };
let fill = designcraft_doc::Fill {
swatch: st.fill.clone(),
tint: st.fill_tint,
overprint: st.overprint_fill,
..designcraft_doc::Fill::none()
};
if let (Some(paint), Some(p)) = (self.fill_paint(&fill, bp.bounding_box()), to_path(&gp)) {
let op = self.overprints(&st.fill, st.fill_tint, st.overprint_fill);
let op = self.push_overprint(s, op, false);
@@ -1103,7 +1108,8 @@ impl Exporter<'_> {
let (swatch, tint) = (fill.swatch.as_str(), fill.tint);
if let Some(g) = designcraft_color::swatch::resolve_gradient(&self.doc.swatches, swatch) {
let kind = g.kind;
let stops = match self.ink_gradient_stops(g, swatch) {
let overprint = self.overprints(swatch, tint, fill.overprint);
let stops = match self.ink_gradient_stops(g, swatch, overprint) {
Some(stops) => stops,
None => self.device_gradient_stops(g, swatch),
};
+20 -6
View File
@@ -1,7 +1,10 @@
//! Gradients written in their inks. A gradient whose stops name a spot swatch shades in that
//! spot: one spot with paper or tints of itself is a Separation shading, a spot with process
//! inks or a second spot a DeviceN shading (process colorants in CMYK order, then the spots).
//! In print, a process gradient with a `[Paper]` stop shades in DeviceCMYK.
//! In print, a process gradient with a `[Paper]` stop shades in DeviceCMYK. An overprinting
//! gradient of fewer than four process inks is a DeviceN shading of just those inks: nonzero
//! overprint mode doesn't apply to shadings (ISO 32000-2 §8.6.7), so a DeviceCMYK shading's
//! zero components would knock out the plates beneath.
use craft_pdf::write::backend as krilla;
use designcraft_color::swatch::{ColorType, SwatchValue, apply_tint};
@@ -73,16 +76,18 @@ impl Exporter<'_> {
}
/// The gradient's stops in its inks, or `None` to write it in device colors (no spot, or
/// inks that can't share one shading space, which is warned about).
pub(crate) fn ink_gradient_stops(&mut self, g: &Gradient, swatch: &str) -> Option<Vec<Stop>> {
/// inks that can't share one shading space, which is warned about). `overprint`: the paint
/// overprints.
pub(crate) fn ink_gradient_stops(&mut self, g: &Gradient, swatch: &str, overprint: bool) -> Option<Vec<Stop>> {
let overprint = overprint && !self.rgb_only;
// Plain-color stops (all older documents) keep their device colors.
if g.stops.iter().all(|s| s.swatch.is_none()) {
if !overprint && g.stops.iter().all(|s| s.swatch.is_none()) {
return None;
}
let inks: Vec<Option<Ink>> = g.stops.iter().map(|s| self.stop_ink(s)).collect();
let has_spot = inks.iter().any(|i| matches!(i, Some(Ink::Spot { .. })));
let on_paper = g.stops.iter().any(|s| s.swatch.as_deref() == Some(designcraft_color::swatch::PAPER));
if !has_spot && !(on_paper && self.for_print() && !self.rgb_only) {
if !has_spot && !overprint && !(on_paper && self.for_print() && !self.rgb_only) {
return None;
}
let Some(inks) = inks.into_iter().collect::<Option<Vec<Ink>>>() else {
@@ -107,6 +112,14 @@ impl Exporter<'_> {
}
let process: Vec<usize> = (0..4).filter(|j| process.get(*j).copied().unwrap_or(false)).collect();
let space = match (spots.as_slice(), process.is_empty()) {
// All four inks are painted either way.
([], false) if overprint && process.len() < 4 => {
let colorants: Vec<Colorant> = process.iter().filter_map(|j| Process::ALL.get(*j).copied().map(Colorant::Process)).collect();
match DeviceNSpace::new(colorants) {
Some(s) => Some(Space::DeviceN(s)),
None => return self.unshaded(swatch, "its inks don't fit one DeviceN space", false),
}
}
([], _) => None,
([(name, alternate)], true) => {
Some(Space::Separation(SeparationSpace::new(SeparationColorant::Custom(name.clone()), regular(alternate, self.rgb_only))))
@@ -167,7 +180,8 @@ impl Exporter<'_> {
let color: krilla::color::Color = match &space {
Some(Space::Separation(sp)) => separation::Color::new(q(v.first().copied().unwrap_or(0.0)), sp.clone()).into(),
Some(Space::DeviceN(sp)) => device_n::Color::new(v.iter().map(|t| q(*t)).collect(), sp.clone())?.into(),
// Process inks only (a gradient to [Paper] in print).
// Process inks only (a gradient to [Paper] in print, or an overprinting one of
// all four inks).
None => {
let mut cmyk = [0.0f32; 4];
for (t, j) in v.iter().zip(&process) {
+6 -5
View File
@@ -32,10 +32,11 @@ use crate::export::{Exporter, solid_fill, tf, to_path};
impl Exporter<'_> {
/// A text run's paint: solid, or a gradient spanning the frame's box (`area`, text space),
/// for glyphs drawn under `origin` (text space ← run space).
fn text_paint(&mut self, swatch: &str, tint: f32, origin: Affine, area: Rect) -> Option<Paint> {
/// for glyphs drawn under `origin` (text space ← run space). `overprint`: the run's
/// overprint attribute for this paint.
fn text_paint(&mut self, swatch: &str, tint: f32, overprint: bool, origin: Affine, area: Rect) -> Option<Paint> {
if designcraft_color::swatch::resolve_gradient(&self.doc.swatches, swatch).is_some() && origin.determinant().abs() > 1e-12 {
let fill = designcraft_doc::Fill { swatch: swatch.into(), tint, ..designcraft_doc::Fill::none() };
let fill = designcraft_doc::Fill { swatch: swatch.into(), tint, overprint, ..designcraft_doc::Fill::none() };
return self.fill_paint_in(&fill, self.text_area.unwrap_or(area), origin.inverse());
}
self.swatch_color(swatch, tint).map(Into::into)
@@ -474,9 +475,9 @@ impl Exporter<'_> {
// Outside a frame (no text area), a gradient spans the run.
let end = glyphs.last().map_or(g0.x, |g| g.x + g.adv);
let run_area = Rect::new(g0.x, baseline - size, end.max(g0.x + 1.0), baseline + size * 0.25);
let fill = self.text_paint(&st.fill, st.fill_tint, origin, run_area);
let fill = self.text_paint(&st.fill, st.fill_tint, st.overprint_fill, origin, run_area);
let stroke = if st.stroke != designcraft_color::swatch::NONE && st.stroke_weight > 0.0 {
self.text_paint(&st.stroke, st.stroke_tint, origin, run_area)
self.text_paint(&st.stroke, st.stroke_tint, st.overprint_stroke, origin, run_area)
} else {
None
};
+28 -1
View File
@@ -19,6 +19,11 @@ fn spot(d: &mut Document, name: &str, color: Color) {
/// A document with spot inks Gold and Silver, a 40 % tint of Gold, and one box filled with a gradient on `stops`
/// (swatch name or plain color, location).
fn document(stops: &[(Result<&str, Color>, f32)], midpoint: f32) -> Document {
document_with(stops, midpoint, false)
}
/// [`document`], the box's fill overprinting when `overprint`.
fn document_with(stops: &[(Result<&str, Color>, f32)], midpoint: f32, overprint: bool) -> Document {
let mut d = Document::new(&NewDocument { width: 120.0, height: 120.0, facing_pages: false, ..Default::default() });
spot(&mut d, "Gold", Color::cmyk(0.0, 0.3, 0.9, 0.1));
spot(&mut d, "Silver", Color::cmyk(0.2, 0.15, 0.1, 0.3));
@@ -35,7 +40,7 @@ fn document(stops: &[(Result<&str, Color>, f32)], midpoint: f32) -> Document {
..Swatch::color("Fade", Color::BLACK)
});
let mut item = Item::new(ItemId(d.alloc()), d.default_layer(), Shape::Rectangle, shapes::rectangle(Rect::new(10.0, 10.0, 110.0, 110.0)));
item.fill = Fill::swatch("Fade");
item.fill = Fill { overprint, ..Fill::swatch("Fade") };
d.insert_item(SpreadRef::Doc(0), item, None).unwrap();
d
}
@@ -169,6 +174,28 @@ fn a_process_gradient_to_paper_prints_in_cmyk() {
assert!(close(&shadings[0].start, &[1.0, 0.0, 0.0, 0.0]) && close(&shadings[0].end, &[0.0, 0.0, 0.0, 0.0]));
}
/// Nonzero overprint mode doesn't apply to shadings, so an overprinting process gradient names
/// only the inks it uses and leaves the other plates alone.
#[test]
fn an_overprinting_process_gradient_is_a_device_n_shading_of_its_inks() {
for standard in [Standard::None, Standard::PdfX4] {
let (shadings, warnings) = export(&document_with(&[(Ok(CYAN), 0.0), (Ok("[Paper]"), 1.0)], 0.5, true), standard);
no_conversion(&warnings);
let s = &shadings[0];
assert_eq!((s.family.as_str(), s.colorants.as_slice()), ("DeviceN", &["Cyan".to_string()][..]), "{standard:?}");
assert!(close(&s.start, &[1.0]) && close(&s.end, &[0.0]), "{:?} → {:?}", s.start, s.end);
}
// Plain-color stops too.
let c_to_m = [(Err(Color::cmyk(1.0, 0.0, 0.0, 0.0)), 0.0), (Err(Color::cmyk(0.0, 1.0, 0.0, 0.0)), 1.0)];
let (shadings, _) = export(&document_with(&c_to_m, 0.5, true), Standard::PdfX4);
let s = &shadings[0];
assert_eq!((s.family.as_str(), s.colorants.as_slice()), ("DeviceN", &["Cyan".to_string(), "Magenta".to_string()][..]));
assert!(close(&s.start, &[1.0, 0.0]) && close(&s.end, &[0.0, 1.0]), "{:?} → {:?}", s.start, s.end);
// Without overprint it stays in DeviceCMYK.
let (shadings, _) = export(&document(&c_to_m, 0.5), Standard::PdfX4);
assert_eq!(shadings[0].family, "DeviceCMYK");
}
/// PDF/A forbids DeviceN's PostScript tint transform: the gradient goes to process color, with
/// a warning, and the export still succeeds.
#[test]