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.
This commit is contained in:
aiongg
2026-10-20 12:53:54 +00:00
parent ce5e58e63b
commit c6d980b25f
9 changed files with 155 additions and 28 deletions
Generated
+30 -6
View File
@@ -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",
+6 -5
View File
@@ -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" }
+2 -14
View File
@@ -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.
+1
View File
@@ -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 }
+11 -2
View File
@@ -498,13 +498,14 @@ pub fn specs() -> Vec<CommandSpec> {
}),
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<Value> = 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<CommandSpec> {
}
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");
}
}
+1
View File
@@ -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 }
+6 -1
View File
@@ -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,
+97
View File
@@ -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::<Vec<_>>(), ["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);
}
}
+1
View File
@@ -30,6 +30,7 @@
#![forbid(unsafe_code)]
mod export;
pub mod inks;
mod links;
mod marks;
mod text;