Files
vectorcraft/crates/pdf/src/print/plates.rs
T
japbcoelhoandClaude Opus 5.5 92a801a04f PDF: colours are written in the profiles assigned to the document
A document assigned a CMYK profile (Edit > Assign Profile) keeps it when
it is saved, but the working spaces are the app's, and a new session
starts with the defaults. Opened again, such a document exported its CMYK
colours tagged with the working CMYK space (VectorCraft Generic CMYK
(SWOP-like)) instead of its own profile, made that profile the PDF/X
output intent, and separated its RGB colours through it; print
separations did the same. edit.assignProfile still reported the assigned
profile, and document.exportPdf's documentation says CMYK colours are
tagged with "the document's CMYK profile".

Found on a book cover assigned FOGRA39L Coated: built in several
vectorcraft-cli runs, each reopening the saved document, its PDF/X-4
came out tagged with the SWOP-like profile unless every run set FOGRA39
as the working CMYK space again.

Export and print now work in the document's colour spaces: the active
settings with the assigned profiles in their place (document_cms). An
assigned profile that isn't available leaves the working space, with a
warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:14:42 +07:00

122 lines
5.0 KiB
Rust

//! Separations: the inks of a job ([`print_inks`]) and the document as one ink's plate, every
//! colour (and image pixel) replaced by the grey of that ink's coverage.
use std::sync::Arc;
use vectorcraft_color::Color;
use vectorcraft_color::cms::{self, Cms, Model, ProfileKind};
use vectorcraft_color::swatch::REGISTRATION;
use vectorcraft_doc::inks::{Link, inks, plates, visit_node_colors};
use vectorcraft_doc::overprint::darken_overprints;
use vectorcraft_doc::{ColorMode, Document, Node};
use super::{DEFAULT_FREQUENCY, PrintInk, PrintSettings};
/// The inks of a separation of `doc`: the process inks, then one per spot swatch (none when
/// spots print as process), with the options of [`super::PrintOutput::inks`]; and a warning for
/// each option naming no ink.
pub fn print_inks(doc: &Document, set: &PrintSettings) -> (Vec<PrintInk>, Vec<String>) {
let out = &set.output;
let list: Vec<PrintInk> = plates(doc)
.into_iter()
.filter(|p| !(p.spot && out.spots_to_process))
.map(|p| {
let o = out.inks.iter().find(|i| i.name == p.name);
PrintInk {
print: o.is_none_or(|o| o.print),
frequency: o.and_then(|o| o.frequency).unwrap_or(DEFAULT_FREQUENCY),
angle: o.and_then(|o| o.angle).unwrap_or_else(|| PrintInk::default_angle(&p.name)),
spot: p.spot,
name: p.name,
}
})
.collect();
let warnings = out
.inks
.iter()
.filter(|i| !list.iter().any(|p| p.name == i.name))
.map(|i| {
format!("there is no ink named `{}` to print (inks: {})", i.name, list.iter().map(|p| p.name.as_str()).collect::<Vec<_>>().join(", "))
})
.collect();
(list, warnings)
}
/// The colour settings separations of `doc` convert with: the document's colour spaces
/// ([`crate::output::document_cms`]), with the printer profile as their CMYK profile when it is a
/// CMYK one.
fn separation_cms(doc: &Document, set: &PrintSettings) -> Arc<Cms> {
let (own, _) = crate::output::document_cms(doc);
match cms::profile(set.color.profile.trim()) {
Some(p) if p.kind == ProfileKind::Cmyk && cms::canonical_name(&own.settings().cmyk) != p.name => {
let settings = cms::ColorSettings { cmyk: p.name, ..own.settings().clone() };
Cms::new(&settings).map_or(own, Arc::new)
}
_ => own,
}
}
/// Separates colours into inks as the job's colour options say.
pub(crate) struct Separator<'a> {
pub doc: &'a Document,
cms: Arc<Cms>,
set: &'a PrintSettings,
}
impl<'a> Separator<'a> {
pub fn new(doc: &'a Document, set: &'a PrintSettings) -> Self {
Self { doc, cms: separation_cms(doc, set), set }
}
/// How much of ink `plate` colour `c` (with swatch link `link`) prints, 0..1.
fn ink(&self, plate: &str, c: &Color, link: Link) -> f32 {
let color = &self.set.color;
// Spot colours as process: only Registration keeps its link.
let link = link.filter(|(name, _)| !self.set.output.spots_to_process || *name == REGISTRATION);
// Without Preserve Numbers, CMYK colours are separated again from their appearance.
let lab;
let c = if !color.preserve_numbers && matches!(c, Color::Cmyk { .. }) {
lab = self.cms.convert(c, Model::Lab, color.intent);
&lab
} else {
c
};
inks(self.doc, &self.cms, c, link, color.intent).on_plate(plate)
}
/// Recolour `n`'s subtree as plate `plate`: the grey of each colour's coverage, unlinked.
pub fn node(&self, plate: &str, n: &mut Node) {
visit_node_colors(n, &mut |c, swatch, tint| {
*c = Color::Gray { k: self.ink(plate, c, swatch.as_deref().map(|s| (s, *tint))) };
*swatch = None;
*tint = 1.0;
});
}
/// The document as plate `plate`: art, symbols, pattern tiles and images in the grey of the
/// ink's coverage. Overprinting fills and strokes darken, so where they have none of this
/// ink the plate below shows through instead of being knocked out.
pub fn plate(&self, plate: &str) -> Document {
let mut d = self.doc.clone();
let discard_white = d.setup.discard_white_overprint;
let nodes = d.layers.iter_mut().chain(d.symbols.iter_mut().map(|s| &mut s.art)).chain(d.patterns.iter_mut().flat_map(|p| p.art.iter_mut()));
for n in nodes {
darken_overprints(n, discard_white);
self.node(plate, Arc::make_mut(n));
}
for blob in d.images.values_mut() {
let gray = blob.map_rgb(|[r, g, b]| {
let rgb = |v: u8| v as f32 / 255.0;
let v = (255.0 * (1.0 - self.ink(plate, &Color::rgb(rgb(r), rgb(g), rgb(b)), None))).round() as u8;
[v; 3]
});
if let Some(gray) = gray {
*blob = gray;
}
}
// Every colour is a grey now: nothing to separate on the way out.
d.color_mode = ColorMode::Rgb;
d
}
}