mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-03 06:30:00 -06:00
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>
122 lines
5.0 KiB
Rust
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
|
|
}
|
|
}
|