From c6d980b25ffad28a9d26e83ce28a81aefeec2d25 Mon Sep 17 00:00:00 2001 From: aiongg Date: Tue, 20 Oct 2026 12:53:54 +0000 Subject: [PATCH] Print color: inks on craft-prepress's InkSet The Ink Manager settings stay saved by swatch name; PDF export and the ink commands build a craft_prepress::InkSet from the document's spot swatches and those settings, and ink.options refuses alias loops. The hayro crates come from craft-libs at the craft-pdf revision, which craft-prepress's plate mode needs. --- Cargo.lock | 36 ++++++++++--- Cargo.toml | 11 ++-- crates/doc/src/lib.rs | 16 +----- crates/engine/Cargo.toml | 1 + crates/engine/src/cmd/style.rs | 13 ++++- crates/pdf/Cargo.toml | 1 + crates/pdf/src/export.rs | 7 ++- crates/pdf/src/inks.rs | 97 ++++++++++++++++++++++++++++++++++ crates/pdf/src/lib.rs | 1 + 9 files changed, 155 insertions(+), 28 deletions(-) create mode 100644 crates/pdf/src/inks.rs diff --git a/Cargo.lock b/Cargo.lock index 8c9ed8c8c..eaae5efd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -816,6 +816,14 @@ dependencies = [ "rustix 1.1.5", ] +[[package]] +name = "craft-cms" +version = "0.1.0" +source = "git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc#0fe6156614de2e8780f34612fa6a577c745f35bc" +dependencies = [ + "rayon", +] + [[package]] name = "craft-cms" version = "0.1.0" @@ -834,10 +842,24 @@ name = "craft-pdf" version = "0.1.0" source = "git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc#0fe6156614de2e8780f34612fa6a577c745f35bc" dependencies = [ + "bytemuck", + "hayro", + "hayro-interpret", + "hayro-syntax", "krilla", "thiserror 2.0.21", ] +[[package]] +name = "craft-prepress" +version = "0.1.0" +source = "git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc#0fe6156614de2e8780f34612fa6a577c745f35bc" +dependencies = [ + "craft-cms 0.1.0 (git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc)", + "craft-pdf", + "thiserror 2.0.21", +] + [[package]] name = "craft-print" version = "0.1.0" @@ -969,7 +991,7 @@ dependencies = [ name = "designcraft-color" version = "0.9.0" dependencies = [ - "craft-cms", + "craft-cms 0.1.0 (git+https://github.com/storytold/craft-libs?rev=e9546e191dd62f2bc1d61eca45dbdc2f1b3c1d8e)", "kurbo", "proptest", "serde", @@ -1016,6 +1038,7 @@ dependencies = [ "caseless", "craft-atomic", "craft-docpath", + "craft-prepress", "craft-print", "designcraft-color", "designcraft-compose", @@ -1114,7 +1137,7 @@ dependencies = [ name = "designcraft-images" version = "0.9.0" dependencies = [ - "craft-cms", + "craft-cms 0.1.0 (git+https://github.com/storytold/craft-libs?rev=e9546e191dd62f2bc1d61eca45dbdc2f1b3c1d8e)", "designcraft-color", "designcraft-fonts", "image", @@ -1143,6 +1166,7 @@ name = "designcraft-pdf" version = "0.9.0" dependencies = [ "craft-pdf", + "craft-prepress", "designcraft-color", "designcraft-compose", "designcraft-doc", @@ -2105,7 +2129,7 @@ dependencies = [ [[package]] name = "hayro" version = "0.7.1" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc#0fe6156614de2e8780f34612fa6a577c745f35bc" dependencies = [ "bytemuck", "hayro-interpret", @@ -2135,7 +2159,7 @@ dependencies = [ [[package]] name = "hayro-interpret" version = "0.7.0" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc#0fe6156614de2e8780f34612fa6a577c745f35bc" dependencies = [ "bitflags 2.13.2", "hayro-cmap", @@ -2153,7 +2177,7 @@ dependencies = [ [[package]] name = "hayro-jbig2" version = "0.3.0" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc#0fe6156614de2e8780f34612fa6a577c745f35bc" dependencies = [ "fearless_simd", "hayro-ccitt", @@ -2177,7 +2201,7 @@ checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa" [[package]] name = "hayro-syntax" version = "0.7.2" -source = "git+https://github.com/storytold/pdfcraft?rev=b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9#b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" +source = "git+https://github.com/aiongg/craft-libs?rev=0fe6156614de2e8780f34612fa6a577c745f35bc#0fe6156614de2e8780f34612fa6a577c745f35bc" dependencies = [ "flate2", "hayro-ccitt", diff --git a/Cargo.toml b/Cargo.toml index 7d563d07a..14aabc7e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,8 +120,9 @@ debug = 0 [patch.crates-io] winit = { git = "https://github.com/skylartaylor/winit.git", rev = "3d51077fd1a966f84df9ef287839dc84fd5b4ca7" } krilla = { git = "https://github.com/aiongg/craft-libs", rev = "0fe6156614de2e8780f34612fa6a577c745f35bc", version = "=0.8.2" } -# The hayro crates from pdfcraft's vendor/ directory, with the patches its README lists. -hayro = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } -hayro-interpret = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } -hayro-syntax = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } -hayro-jbig2 = { git = "https://github.com/storytold/pdfcraft", rev = "b5467d93a0ffaa5e5e6658bccdf8655a3ba689c9" } +# The hayro crates from craft-libs' vendor/ directory (pdfcraft's, plus the plate mode and decode +# limits craft-pdf and craft-prepress need); same revision as craft-pdf. +hayro = { git = "https://github.com/aiongg/craft-libs", rev = "0fe6156614de2e8780f34612fa6a577c745f35bc" } +hayro-interpret = { git = "https://github.com/aiongg/craft-libs", rev = "0fe6156614de2e8780f34612fa6a577c745f35bc" } +hayro-syntax = { git = "https://github.com/aiongg/craft-libs", rev = "0fe6156614de2e8780f34612fa6a577c745f35bc" } +hayro-jbig2 = { git = "https://github.com/aiongg/craft-libs", rev = "0fe6156614de2e8780f34612fa6a577c745f35bc" } diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 1bf834ae0..ed6691bd4 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -225,7 +225,8 @@ impl Document { } } -/// Ink Manager settings for output. +/// Ink Manager settings for output, by swatch name. Output applies them through a +/// `craft_prepress::InkSet` built from the document (`designcraft_pdf::inks`). #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", default)] pub struct InkManager { @@ -241,19 +242,6 @@ impl InkManager { fn is_default(&self) -> bool { *self == Self::default() } - - /// The ink a spot swatch prints on (after aliases; at most a few hops) and whether it's - /// converted to process. - pub fn resolve<'a>(&'a self, ink: &'a str) -> (&'a str, bool) { - let mut n = ink; - for _ in 0..8 { - match self.aliases.iter().find(|(a, _)| a == n) { - Some((_, to)) if to != n => n = to, - _ => break, - } - } - (n, self.all_to_process || self.to_process.iter().any(|x| x == n)) - } } /// XML tags and mappings. diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 69e0e8d50..c1c0f1d33 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -23,6 +23,7 @@ designcraft-epub = { workspace = true } designcraft-pptx = { workspace = true } designcraft-textimport = { workspace = true } designcraft-pdf = { workspace = true } +craft-prepress = { workspace = true } designcraft-tools = { workspace = true } designcraft-idml = { workspace = true } indd = { workspace = true } diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index d1bd06ad0..5936eecca 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -498,13 +498,14 @@ pub fn specs() -> Vec { }), cmd!(query "ink.list", "Ink Manager", [], None, "{} → {allToProcess, inks: [{name, process: bool (printed as process), alias?}]} — the spot inks", has_doc, |s, _| { let d = &s.doc()?.doc; + let inks = designcraft_pdf::inks::ink_set(d); let inks: Vec = d .swatches .iter() .filter(|w| matches!(w.value, designcraft_color::swatch::SwatchValue::Color { color_type: designcraft_color::swatch::ColorType::Spot, .. })) .map(|w| { let alias = d.inks.aliases.iter().find(|(a, _)| *a == w.name).map(|(_, t)| t.clone()); - json!({"name": w.name, "process": d.inks.resolve(&w.name).1, "alias": alias}) + json!({"name": w.name, "process": designcraft_pdf::inks::spot_output(d, &inks, &w.name).1, "alias": alias}) }) .collect(); Ok(json!({"allToProcess": d.inks.all_to_process, "inks": inks})) @@ -550,6 +551,13 @@ pub fn specs() -> Vec { } match alias { Some(Value::String(to)) => { + let mut set = designcraft_pdf::inks::ink_set(d); + let ids = (set.lookup(name), set.lookup(&to)); + if let (Some(from), Some(target)) = ids + && !set.alias(from, target) + { + return Err(bad("ink.options", format!("aliasing `{name}` to `{to}` would make a loop"))); + } d.inks.aliases.retain(|(a, _)| a != name); d.inks.aliases.push((name.clone(), to)); } @@ -2789,9 +2797,10 @@ mod ink_tests { let l = s.execute("ink.list", &json!({})).unwrap(); assert_eq!(l["inks"][0]["alias"], "Pantone B"); let back = designcraft_idml::import_idml(&designcraft_idml::export_idml(&s.doc().unwrap().doc)).unwrap(); - assert!(back.inks.resolve("Pantone A").1, "IDML ConvertToProcess"); + assert!(designcraft_pdf::inks::spot_output(&back, &designcraft_pdf::inks::ink_set(&back), "Pantone A").1, "IDML ConvertToProcess"); assert!(back.inks.aliases.iter().any(|(a, b)| a == "Pantone A" && b == "Pantone B"), "IDML AliasInkName"); assert!(s.execute("ink.options", &json!({"ink": "[Black]", "toProcess": true})).is_err()); + assert!(s.execute("ink.options", &json!({"ink": "Pantone B", "alias": "Pantone A"})).is_err(), "an alias loop is refused"); } } diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index 6f40462b7..efb059bfc 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -15,6 +15,7 @@ designcraft-doc = { workspace = true } designcraft-fonts = { workspace = true } designcraft-compose = { workspace = true } craft-pdf = { workspace = true, features = ["write-pdf"] } +craft-prepress = { workspace = true } krilla-svg = { workspace = true } designcraft-images = { workspace = true } image = { workspace = true } diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index 4e7627e9e..d528a7b7c 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -159,6 +159,7 @@ fn export_pdf_with_report_inner(doc: &Document, cache: &Cache, opts: &PdfOptions let mut ex = Exporter { drawing_items: Vec::new(), + inks: crate::inks::ink_set(doc), doc, cache, opts, @@ -346,6 +347,7 @@ fn export_booklet_inner(doc: &Document, cache: &Cache, opts: &BookletOptions) -> let pdf = writer.document_mut(); let mut ex = Exporter { drawing_items: Vec::new(), + inks: crate::inks::ink_set(doc), doc, cache, opts: &pdf_opts, @@ -480,6 +482,8 @@ impl std::hash::Hasher for ConstantImageHasher { pub(crate) struct Exporter<'a> { pub doc: &'a Document, + /// The document's inks with its Ink Manager settings. + inks: craft_prepress::InkSet, pub cache: &'a Cache, pub opts: &'a PdfOptions, /// Current sheet's visible area (spread space) for culling. @@ -644,7 +648,7 @@ impl Exporter<'_> { SwatchValue::Registration => return Some(registration(t, self.rgb_only)), SwatchValue::Color { color: _, color_type: ColorType::Spot } => { // Ink Manager: an alias prints on another ink; converted inks go to process. - let (ink, process) = self.doc.inks.resolve(&sw.name); + let (ink, process) = crate::inks::spot_output(self.doc, &self.inks, &sw.name); let target = self.doc.swatch(ink).filter(|w| matches!(w.value, SwatchValue::Color { color_type: ColorType::Spot, .. })).unwrap_or(sw); let SwatchValue::Color { color, .. } = &target.value else { break }; @@ -1800,6 +1804,7 @@ mod image_content_cache_tests { fn exporter<'a>(doc: &'a Document, cache: &'a Cache, opts: &'a PdfOptions) -> Exporter<'a> { Exporter { drawing_items: Vec::new(), + inks: crate::inks::ink_set(doc), doc, cache, opts, diff --git a/crates/pdf/src/inks.rs b/crates/pdf/src/inks.rs new file mode 100644 index 000000000..72009e9e1 --- /dev/null +++ b/crates/pdf/src/inks.rs @@ -0,0 +1,97 @@ +//! The document's inks as a [`craft_prepress::InkSet`]: the four process inks, each spot swatch +//! with its color as the alternate, and the Ink Manager settings (aliases, spots to process). +//! +//! The document saves the Ink Manager by swatch name ([`designcraft_doc::InkManager`]); the set +//! is built from it whenever output needs it, so renaming or editing a swatch never leaves a +//! stale copy. + +use craft_prepress::{Alternate, Ink, InkId, InkSet, Spot}; +use designcraft_color::swatch::{ColorType, Swatch, SwatchValue}; +use designcraft_doc::Document; + +fn is_spot(w: &Swatch) -> bool { + matches!(w.value, SwatchValue::Color { color_type: ColorType::Spot, .. }) +} + +/// The inks of `doc` with its Ink Manager settings applied. An alias whose target isn't a spot +/// swatch, or that would make a loop, is left out: that spot prints on its own plate. +pub fn ink_set(doc: &Document) -> InkSet { + let mut set = InkSet::new(); + for w in doc.swatches.iter().filter(|w| is_spot(w)) { + let SwatchValue::Color { color, .. } = w.value else { continue }; + let alternate = match color { + designcraft_color::Color::Cmyk { c, m, y, k } => Alternate::Cmyk([c, m, y, k]), + other => { + let lab = other.to_lab(); + Alternate::Lab([lab.l, lab.a, lab.b]) + } + }; + set.add(Ink::Spot(Spot { name: w.name.clone(), alternate, solidity: None })); + } + let spot_id = |set: &InkSet, name: &str| doc.swatch(name).filter(|w| is_spot(w)).and_then(|_| set.lookup(name)); + for name in &doc.inks.to_process { + if let Some(id) = spot_id(&set, name) { + set.set_to_process(id, true); + } + } + for (from, to) in &doc.inks.aliases { + if let (Some(from), Some(to)) = (spot_id(&set, from), spot_id(&set, to)) { + set.alias(from, to); + } + } + set.set_all_to_process(doc.inks.all_to_process); + set +} + +/// How spot swatch `name` prints: the spot swatch whose plate (or, converted, whose color) it +/// takes after aliases, and whether it prints as process color. A name the set doesn't hold +/// prints as itself on its own plate. +pub fn spot_output<'a>(doc: &'a Document, set: &InkSet, name: &'a str) -> (&'a str, bool) { + let Some(id) = set.lookup(name) else { return (name, false) }; + let end = std::iter::successors(Some(id), |i| set.alias_of(*i)).take(set.len()).last().unwrap_or(id); + let target = if end == id { name } else { swatch_of(doc, set, end).unwrap_or(name) }; + (target, set.plate_of(id).is_none()) +} + +/// The first spot swatch that is ink `id`. +fn swatch_of<'a>(doc: &'a Document, set: &InkSet, id: InkId) -> Option<&'a str> { + doc.swatches.iter().filter(|w| is_spot(w)).find(|w| set.lookup(&w.name) == Some(id)).map(|w| w.name.as_str()) +} + +#[cfg(test)] +mod tests { + use super::*; + use designcraft_color::Color; + use designcraft_doc::build::NewDocument; + + fn spot(d: &mut Document, name: &str, color: Color) { + d.swatches.push(Swatch { + name: name.into(), + value: SwatchValue::Color { color, color_type: ColorType::Spot }, + locked: false, + named: true, + hidden: false, + }); + } + + #[test] + fn aliases_follow_chains_and_converted_inks_print_as_process() { + let mut d = Document::new(&NewDocument::default()); + spot(&mut d, "Gold", Color::Cmyk { c: 0.0, m: 0.2, y: 0.8, k: 0.1 }); + spot(&mut d, "Bronze", Color::Rgb { r: 0.6, g: 0.4, b: 0.2 }); + spot(&mut d, "Copper", Color::Cmyk { c: 0.0, m: 0.5, y: 0.6, k: 0.2 }); + d.inks.aliases = vec![("Gold".into(), "Bronze".into()), ("Bronze".into(), "Copper".into()), ("Copper".into(), "Gold".into())]; + d.inks.to_process = vec!["Copper".into()]; + let set = ink_set(&d); + assert_eq!(set.spots().map(|(_, s)| s.name.as_str()).collect::>(), ["Gold", "Bronze", "Copper"]); + // The loop-closing alias is refused, so Gold → Bronze → Copper, which is converted. + assert_eq!(spot_output(&d, &set, "Gold"), ("Copper", true)); + assert_eq!(spot_output(&d, &set, "Copper"), ("Copper", true)); + d.inks.to_process.clear(); + let set = ink_set(&d); + assert_eq!(spot_output(&d, &set, "Bronze"), ("Copper", false)); + assert_eq!(spot_output(&d, &set, "Unknown"), ("Unknown", false)); + d.inks.all_to_process = true; + assert!(spot_output(&d, &ink_set(&d), "Gold").1); + } +} diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index ad1123897..92a73d206 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -30,6 +30,7 @@ #![forbid(unsafe_code)] mod export; +pub mod inks; mod links; mod marks; mod text;