C: Ink Manager — spots to process, ink aliases (PDF, IDML)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8b4290f4c4
commit
efb563673f
|
|
@ -53,6 +53,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- High Contrast interface theme (fifth Interface Color Theme: black chrome, white text, yellow selection).
|
||||
- Place Excel workbooks (.xlsx): the first worksheet's used range becomes a table (shared/inline strings, numbers, booleans).
|
||||
- Object Library (`library.new/open/add/place/remove/list/json/close`, Library panel): `.dclib` files of snippets that keep stories, styles, swatches and images.
|
||||
- Ink Manager (`ink.list`, `ink.options`; Swatches panel menu): All Spots to Process, per-ink conversion and ink aliases in PDF output; IDML ConvertToProcess/AliasInkName.
|
||||
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
|
||||
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
|
||||
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ impl Document {
|
|||
swatches: designcraft_color::default_swatches(),
|
||||
color_groups: vec![],
|
||||
conditions: vec![],
|
||||
inks: Default::default(),
|
||||
endnote_options: Default::default(),
|
||||
endnote_story: None,
|
||||
sections: vec![Section {
|
||||
|
|
|
|||
|
|
@ -160,6 +160,37 @@ impl Document {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ink Manager settings for output.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", default)]
|
||||
pub struct InkManager {
|
||||
/// All Spots to Process.
|
||||
pub all_to_process: bool,
|
||||
/// Spot inks converted to process one by one.
|
||||
pub to_process: Vec<String>,
|
||||
/// Ink Alias: (spot ink, the ink it prints on).
|
||||
pub aliases: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// A Swatches panel colour group: a named folder of swatches (by name).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -337,6 +368,9 @@ pub struct Document {
|
|||
/// the top level.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub color_groups: Vec<ColorGroup>,
|
||||
/// Ink Manager: spot inks printed as process, and spot inks aliased to others.
|
||||
#[serde(default, skip_serializing_if = "InkManager::is_default")]
|
||||
pub inks: InkManager,
|
||||
/// Document Endnote Options, and the story shown in the endnote frame (generated).
|
||||
#[serde(default)]
|
||||
pub endnote_options: EndnoteOptions,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,71 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
ok()
|
||||
})
|
||||
}),
|
||||
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: 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})
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({"allToProcess": d.inks.all_to_process, "inks": inks}))
|
||||
}),
|
||||
cmd!(
|
||||
"ink.options",
|
||||
"Ink Manager",
|
||||
[],
|
||||
None,
|
||||
"{allToProcess?: bool, ink?: spot swatch, toProcess?: bool, alias?: spot swatch | null} — output: spots to process, ink aliases",
|
||||
has_doc,
|
||||
|s, p| {
|
||||
let all = p.get("allToProcess").and_then(Value::as_bool);
|
||||
let ink = str_param(p, "ink").map(str::to_string);
|
||||
let to_process = p.get("toProcess").and_then(Value::as_bool);
|
||||
let alias = p.get("alias").cloned();
|
||||
s.edit(|d, _| {
|
||||
if let Some(v) = all {
|
||||
d.inks.all_to_process = v;
|
||||
}
|
||||
if let Some(name) = &ink {
|
||||
let is_spot = |n: &str| {
|
||||
d.swatch(n).is_some_and(|w| {
|
||||
matches!(
|
||||
w.value,
|
||||
designcraft_color::swatch::SwatchValue::Color { color_type: designcraft_color::swatch::ColorType::Spot, .. }
|
||||
)
|
||||
})
|
||||
};
|
||||
if !is_spot(name) {
|
||||
return Err(bad("ink.options", format!("`{name}` isn't a spot ink")));
|
||||
}
|
||||
if let Some(Value::String(to)) = &alias
|
||||
&& (!is_spot(to) || to == name)
|
||||
{
|
||||
return Err(bad("ink.options", format!("can't alias to `{to}`")));
|
||||
}
|
||||
if let Some(v) = to_process {
|
||||
d.inks.to_process.retain(|x| x != name);
|
||||
if v {
|
||||
d.inks.to_process.push(name.clone());
|
||||
}
|
||||
}
|
||||
match alias {
|
||||
Some(Value::String(to)) => {
|
||||
d.inks.aliases.retain(|(a, _)| a != name);
|
||||
d.inks.aliases.push((name.clone(), to));
|
||||
}
|
||||
Some(Value::Null) => d.inks.aliases.retain(|(a, _)| a != name),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
ok()
|
||||
})
|
||||
}
|
||||
),
|
||||
cmd!(
|
||||
"swatch.newColorGroup",
|
||||
"New Color Group",
|
||||
|
|
@ -936,3 +1001,38 @@ mod color_group_tests {
|
|||
assert!(s.doc().unwrap().doc.swatch(&a).is_some(), "ungrouping keeps the swatches");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ink_tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
#[test]
|
||||
fn ink_manager_converts_and_aliases_spots_in_pdf() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
for (n, c) in [("Pantone A", [0, 50, 100, 0]), ("Pantone B", [100, 0, 0, 0])] {
|
||||
s.execute("swatch.create", &json!({"name": n, "spot": true, "color": {"c": c[0], "m": c[1], "y": c[2], "k": c[3]}})).unwrap();
|
||||
}
|
||||
let id = s.execute("frame.create", &json!({"rect": [72, 72, 200, 200]})).unwrap()["id"].clone();
|
||||
s.execute("object.fill", &json!({"swatch": "Pantone A", "ids": [id]})).unwrap();
|
||||
let pdf = |s: &mut Session| {
|
||||
let b = s.execute("file.exportPdf", &json!({})).unwrap()["base64"].as_str().unwrap().to_string();
|
||||
String::from_utf8_lossy(&super::super::file::base64_decode(&b)).into_owned()
|
||||
};
|
||||
assert!(pdf(&mut s).contains("Pantone"), "a separation");
|
||||
s.execute("ink.options", &json!({"ink": "Pantone A", "alias": "Pantone B"})).unwrap();
|
||||
let p = pdf(&mut s);
|
||||
assert!(p.contains("Pantone#20B") || p.contains("Pantone B"), "aliased onto B");
|
||||
assert!(!p.contains("Pantone#20A") && !p.contains("/Pantone A"));
|
||||
s.execute("ink.options", &json!({"allToProcess": true})).unwrap();
|
||||
assert!(!pdf(&mut s).contains("Pantone"), "all spots to process");
|
||||
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!(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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -485,12 +485,16 @@ impl<'a> Ex<'a> {
|
|||
colors.push(color_el(&id, &s.name, model, color, "Normal", true, true, true));
|
||||
by_value.push((*color, id.clone()));
|
||||
if *color_type == ColorType::Spot {
|
||||
let mut ink = El::new("Ink");
|
||||
if let Some((_, to)) = d.inks.aliases.iter().find(|(a, _)| *a == s.name) {
|
||||
ink.set("AliasInkName", to);
|
||||
}
|
||||
let convert = d.inks.all_to_process || d.inks.to_process.contains(&s.name);
|
||||
inks.push(
|
||||
El::new("Ink")
|
||||
.attr("Self", format!("Ink/{}", escape_id(&s.name)))
|
||||
ink.attr("Self", format!("Ink/{}", escape_id(&s.name)))
|
||||
.attr("Name", &s.name)
|
||||
.attr("Angle", "45")
|
||||
.attr("ConvertToProcess", "false")
|
||||
.attr("ConvertToProcess", bool_s(convert))
|
||||
.attr("Frequency", "70")
|
||||
.attr("NeutralDensity", "0.5")
|
||||
.attr("PrintInk", "true")
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ struct Importer<'r> {
|
|||
color_groups: Vec<designcraft_doc::ColorGroup>,
|
||||
conditions: Vec<designcraft_doc::Condition>,
|
||||
condition_names: HashMap<String, String>,
|
||||
inks: designcraft_doc::InkManager,
|
||||
styles: Styles,
|
||||
para_names: HashMap<String, String>,
|
||||
char_names: HashMap<String, String>,
|
||||
|
|
@ -181,6 +182,7 @@ impl<'r> Importer<'r> {
|
|||
color_groups: Vec::new(),
|
||||
conditions: Vec::new(),
|
||||
condition_names: HashMap::new(),
|
||||
inks: Default::default(),
|
||||
styles,
|
||||
para_names: HashMap::new(),
|
||||
char_names: HashMap::new(),
|
||||
|
|
@ -513,6 +515,21 @@ impl<'r> Importer<'r> {
|
|||
Swatch { name, value: SwatchValue::Tint { base, tint: (t / 100.0) as f32 }, locked: false, named: true, hidden: false },
|
||||
));
|
||||
}
|
||||
// Ink Manager.
|
||||
for e in top.iter().filter(|e| e.local() == "Ink") {
|
||||
let Some(name) = e
|
||||
.get("Name")
|
||||
.filter(|n| !n.starts_with("$ID/") && !["Process Cyan", "Process Magenta", "Process Yellow", "Process Black"].contains(n))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if e.get("ConvertToProcess") == Some("true") {
|
||||
self.inks.to_process.push(name.to_string());
|
||||
}
|
||||
if let Some(a) = e.get("AliasInkName").filter(|a| !a.is_empty() && *a != name && !a.starts_with("$ID/")) {
|
||||
self.inks.aliases.push((name.to_string(), a.to_string()));
|
||||
}
|
||||
}
|
||||
for e in top.iter().filter(|e| e.local() == "Gradient") {
|
||||
let Some(id) = e.get("Self") else { continue };
|
||||
let mut stops: Vec<GradientStop> = Vec::new();
|
||||
|
|
@ -1988,6 +2005,7 @@ impl<'r> Importer<'r> {
|
|||
swatches: std::mem::take(&mut self.swatches),
|
||||
color_groups: std::mem::take(&mut self.color_groups),
|
||||
conditions: std::mem::take(&mut self.conditions),
|
||||
inks: std::mem::take(&mut self.inks),
|
||||
endnote_options: Default::default(),
|
||||
endnote_story: None,
|
||||
sections: std::mem::take(&mut self.sections),
|
||||
|
|
|
|||
|
|
@ -466,8 +466,16 @@ impl Exporter<'_> {
|
|||
n = base;
|
||||
}
|
||||
SwatchValue::Registration => return Some(registration(t, self.rgb_only)),
|
||||
SwatchValue::Color { color, color_type: ColorType::Spot } => {
|
||||
let space = SeparationSpace::new(SeparationColorant::Custom(sw.name.clone()), regular(color, 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 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 };
|
||||
if process {
|
||||
return Some(device(&designcraft_color::swatch::apply_tint(*color, t), self.rgb_only));
|
||||
}
|
||||
let space = SeparationSpace::new(SeparationColorant::Custom(target.name.clone()), regular(color, self.rgb_only));
|
||||
return Some(SepColor::new(q(t), space).into());
|
||||
}
|
||||
_ => break,
|
||||
|
|
|
|||
|
|
@ -66,6 +66,27 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
|
|||
let _ = app.run("swatch.create", json!({"color": {"c": 0, "m": 50, "y": 100, "k": 0}}));
|
||||
}
|
||||
ui.menu_button("☰", |ui| {
|
||||
ui.menu_button("Ink Manager", |ui| {
|
||||
let Ok(l) = app.session.execute("ink.list", &json!({})) else { return };
|
||||
let mut all = l["allToProcess"].as_bool().unwrap_or(false);
|
||||
if ui.checkbox(&mut all, "All Spots to Process").changed() {
|
||||
let _ = app.run("ink.options", json!({"allToProcess": all}));
|
||||
}
|
||||
ui.separator();
|
||||
let inks = l["inks"].as_array().cloned().unwrap_or_default();
|
||||
if inks.is_empty() {
|
||||
ui.label("No spot inks");
|
||||
}
|
||||
for ink in inks {
|
||||
let name = ink["name"].as_str().unwrap_or("").to_string();
|
||||
let mut process = ink["process"].as_bool().unwrap_or(false);
|
||||
ui.add_enabled_ui(!all, |ui| {
|
||||
if ui.checkbox(&mut process, format!("{name} → process")).changed() {
|
||||
let _ = app.run("ink.options", json!({"ink": name, "toProcess": process}));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if ui.button("New Color Group").clicked() {
|
||||
let _ = app.run("swatch.newColorGroup", json!({}));
|
||||
ui.close();
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 75 | 21 | 1 | 88% |
|
||||
| P2 | 76 | 22 | 13 | 41 | 38% |
|
||||
| P2 | 76 | 23 | 13 | 40 | 39% |
|
||||
|
||||
| Area | Parity |
|
||||
|---|---|
|
||||
|
|
@ -20,7 +20,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Layers | 95% |
|
||||
| Frames, shapes & paths | 93% |
|
||||
| Transform | 100% |
|
||||
| Fill, stroke, colour | 88% |
|
||||
| Fill, stroke, colour | 90% |
|
||||
| Effects & transparency | 71% |
|
||||
| Placing & links | 82% |
|
||||
| Type & text frames | 92% |
|
||||
|
|
@ -147,7 +147,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Fill, stroke, colour | Tints | P0 | D |
|
||||
| Fill, stroke, colour | Special swatches | P0 | D |
|
||||
| Fill, stroke, colour | Spot colours | P1 | D |
|
||||
| Fill, stroke, colour | Ink Manager | P2 | M |
|
||||
| Fill, stroke, colour | Ink Manager | P2 | D |
|
||||
| Fill, stroke, colour | Load/Save Swatches (.ase) | P1 | D |
|
||||
| Fill, stroke, colour | Color groups | P2 | D |
|
||||
| Fill, stroke, colour | Unnamed colour to swatch | P1 | D |
|
||||
|
|
|
|||
Loading…
Reference in New Issue