O: File › Package and Copy Links To (file.package, links.copyTo)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
67dd4852ca
commit
acfb542391
|
|
@ -30,6 +30,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
|
|||
- Edit › Paste Into (copied objects become a frame's content, clipped by it; rendered, PDF and IDML both ways — verified in InDesign 2026); Polygon Settings (sides, star inset; double-click the Polygon tool).
|
||||
- Color panel (fill/stroke proxy, CMYK/RGB/Lab sliders with channel ramps, tint slider for swatches, spectrum ramp, Add to Swatches) and Color Picker (double-click the proxy); mixed colours are unnamed (not listed in Swatches until added); `object.color`, `swatch.addToSwatches`, `swatch.addUnnamed`.
|
||||
- Place text files: Word (.docx: styles by name with their attributes, bold/italic/underline/size/font, footnotes, tables, tabs, breaks), RTF and plain text, into the insertion point, the selected frame or a new frame; autoflow adds pages and threaded frames until the text fits; Remove Styles option (`designcraft-textimport`).
|
||||
- File › Package (⌥⇧⌘P): the document relinked to a Links folder, the placed files, an IDML copy, an optional PDF and a report (fonts, links, preflight, instructions); Copy Links To (`links.copyTo`).
|
||||
- Load Swatches / Save Swatches for Exchange (.ase: CMYK, RGB, Lab, Gray; spot or process) from the Swatches panel menu; `swatch.load` / `swatch.save`.
|
||||
- Pathfinder (Object › Pathfinder and a Pathfinder panel): Add, Subtract, Intersect, Exclude Overlap, Minus Back; curves stay curves (flo_curves); `object.pathfinder`.
|
||||
- Make / Release Compound Path (⌘8; nested paths become holes), Create Outlines (⇧⌘O; one path per text colour), Scissors tool (C) with `path.split`.
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ mod links;
|
|||
mod notes;
|
||||
mod object;
|
||||
mod overrides;
|
||||
mod package;
|
||||
mod paths;
|
||||
mod place_text;
|
||||
pub mod preflight;
|
||||
|
|
@ -128,6 +129,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
|||
v.extend(object::specs());
|
||||
v.extend(anchors::specs());
|
||||
v.extend(paths::specs());
|
||||
v.extend(package::specs());
|
||||
v.extend(text::specs());
|
||||
v.extend(style::specs());
|
||||
v.extend(table::specs());
|
||||
|
|
|
|||
|
|
@ -0,0 +1,173 @@
|
|||
//! File › Package and Links › Copy Links To: gather a document with the files it uses.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use designcraft_doc::{AssetId, Document};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, cmd, has_doc, str_param};
|
||||
use crate::{EngineError, Result, Session};
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
noundo "file.package",
|
||||
"Package…",
|
||||
["File"],
|
||||
None,
|
||||
"{dir: folder to create, idml?: true, pdf?: false, instructions?: text} — the document (its placed files relinked to Links/), Links/, an IDML copy, an optional PDF and a report → {dir, files, report}",
|
||||
has_doc,
|
||||
package
|
||||
),
|
||||
cmd!(
|
||||
"links.copyTo",
|
||||
"Copy Link(s) To…",
|
||||
[],
|
||||
None,
|
||||
"{dir, assets?: [asset ids] (default: all)} — write the placed files to `dir` and relink to the copies → {copied}",
|
||||
has_doc,
|
||||
copy_links
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// A file name for asset `a` in a Links folder, unique among `taken`.
|
||||
fn link_name(name: &str, id: AssetId, taken: &mut Vec<String>) -> String {
|
||||
let base =
|
||||
Path::new(name).file_name().map(|n| n.to_string_lossy().to_string()).filter(|n| !n.is_empty()).unwrap_or_else(|| format!("asset-{}", id.0));
|
||||
let (stem, ext) = match base.rsplit_once('.') {
|
||||
Some((s, e)) => (s.to_string(), format!(".{e}")),
|
||||
None => (base.clone(), String::new()),
|
||||
};
|
||||
let mut n = base;
|
||||
let mut k = 2;
|
||||
while taken.iter().any(|t| t.eq_ignore_ascii_case(&n)) {
|
||||
n = format!("{stem} {k}{ext}");
|
||||
k += 1;
|
||||
}
|
||||
taken.push(n.clone());
|
||||
n
|
||||
}
|
||||
|
||||
/// Write each asset's bytes into `dir`; returns the relinked document and the paths written.
|
||||
fn write_links(d: &Document, dir: &Path, only: Option<&[AssetId]>) -> Result<(Document, Vec<PathBuf>)> {
|
||||
std::fs::create_dir_all(dir).map_err(|e| EngineError::Other(format!("{}: {e}", dir.display())))?;
|
||||
let mut out = d.clone();
|
||||
let mut taken = Vec::new();
|
||||
let mut written = Vec::new();
|
||||
let mut ids: Vec<AssetId> = d.assets.keys().copied().collect();
|
||||
ids.sort_by_key(|i| i.0);
|
||||
for id in ids {
|
||||
if only.is_some_and(|o| !o.contains(&id)) {
|
||||
continue;
|
||||
}
|
||||
let a = &d.assets[&id];
|
||||
let name = link_name(a.link.as_deref().unwrap_or(&a.name), id, &mut taken);
|
||||
let path = dir.join(&name);
|
||||
std::fs::write(&path, a.data.as_slice()).map_err(|e| EngineError::Other(format!("{}: {e}", path.display())))?;
|
||||
let mut na = (**a).clone();
|
||||
na.link = Some(path.to_string_lossy().to_string());
|
||||
out.assets.insert(id, Arc::new(na));
|
||||
written.push(path);
|
||||
}
|
||||
Ok((out, written))
|
||||
}
|
||||
|
||||
fn package(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = PathBuf::from(str_param(p, "dir").ok_or_else(|| bad("file.package", "missing `dir`"))?);
|
||||
let st = s.doc()?;
|
||||
let title = st.doc.title.clone();
|
||||
let fonts = s.execute("font.list", &json!({}))?;
|
||||
let links = s.execute("links.list", &json!({}))?;
|
||||
let pre = s.execute("preflight.run", &json!({})).unwrap_or(Value::Null);
|
||||
let d = s.doc()?.doc.clone();
|
||||
let (packed, mut files) = write_links(&d, &dir.join("Links"), None)?;
|
||||
let doc_path = dir.join(format!("{title}.designcraft"));
|
||||
std::fs::write(&doc_path, super::to_bytes(&packed)).map_err(|e| EngineError::Other(format!("{}: {e}", doc_path.display())))?;
|
||||
files.insert(0, doc_path);
|
||||
if p.get("idml").and_then(Value::as_bool).unwrap_or(true) {
|
||||
let path = dir.join(format!("{title}.idml"));
|
||||
std::fs::write(&path, designcraft_idml::export_idml(&packed)).map_err(|e| EngineError::Other(format!("{}: {e}", path.display())))?;
|
||||
files.push(path);
|
||||
}
|
||||
if p.get("pdf").and_then(Value::as_bool).unwrap_or(false) {
|
||||
let path = dir.join(format!("{title}.pdf"));
|
||||
let bytes = designcraft_pdf::export_pdf(&packed, &s.cache, &Default::default()).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
std::fs::write(&path, bytes).map_err(|e| EngineError::Other(format!("{}: {e}", path.display())))?;
|
||||
files.push(path);
|
||||
}
|
||||
// The report: fonts (missing first), links, preflight, the user's instructions.
|
||||
let mut r = format!("Package report: {title}\n\n");
|
||||
if let Some(t) = str_param(p, "instructions").filter(|t| !t.trim().is_empty()) {
|
||||
r += &format!("Instructions\n{t}\n\n");
|
||||
}
|
||||
r += "Fonts\n";
|
||||
for f in fonts.as_array().into_iter().flatten() {
|
||||
let missing = f["missing"].as_bool().unwrap_or(false) || f["styleMissing"].as_bool().unwrap_or(false);
|
||||
r += &format!(" {} {}{}\n", f["family"].as_str().unwrap_or(""), f["style"].as_str().unwrap_or(""), if missing { " — MISSING" } else { "" });
|
||||
}
|
||||
r += "\nLinks\n";
|
||||
for l in links.as_array().into_iter().flatten() {
|
||||
r += &format!(" {} ({})\n", l["name"].as_str().unwrap_or(""), l["status"].as_str().unwrap_or(""));
|
||||
}
|
||||
if let Some(issues) = pre.as_array() {
|
||||
r += &format!("\nPreflight: {} issue(s)\n", issues.len());
|
||||
for i in issues {
|
||||
r += &format!(" {}\n", i.get("message").and_then(Value::as_str).unwrap_or(""));
|
||||
}
|
||||
}
|
||||
let report = dir.join("Instructions.txt");
|
||||
std::fs::write(&report, &r).map_err(|e| EngineError::Other(format!("{}: {e}", report.display())))?;
|
||||
files.push(report);
|
||||
Ok(json!({"dir": dir.to_string_lossy(), "files": files.iter().map(|f| f.to_string_lossy().to_string()).collect::<Vec<_>>(), "report": r}))
|
||||
}
|
||||
|
||||
fn copy_links(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = PathBuf::from(str_param(p, "dir").ok_or_else(|| bad("links.copyTo", "missing `dir`"))?);
|
||||
let only: Option<Vec<AssetId>> = p.get("assets").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_u64).map(AssetId).collect());
|
||||
let d = s.doc()?.doc.clone();
|
||||
let (relinked, written) = write_links(&d, &dir, only.as_deref())?;
|
||||
s.edit(|doc, _| {
|
||||
doc.assets = relinked.assets;
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({"copied": written.len()}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn package_writes_document_links_and_report() {
|
||||
let dir = std::env::temp_dir().join(format!("dc-package-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let svg = dir.join("logo.svg");
|
||||
std::fs::write(&svg, r##"<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><rect width="40" height="40"/></svg>"##).unwrap();
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.execute("file.place", &json!({"path": svg.to_string_lossy(), "x": 100, "y": 100})).unwrap();
|
||||
s.execute("frame.create", &json!({"rect": [72, 300, 400, 400], "content": "text", "text": "Hello"})).unwrap();
|
||||
let out = dir.join("Pkg");
|
||||
let r = s.execute("file.package", &json!({"dir": out.to_string_lossy(), "pdf": true, "instructions": "Print on matte."})).unwrap();
|
||||
let files: Vec<String> = r["files"].as_array().unwrap().iter().map(|f| f.as_str().unwrap().to_string()).collect();
|
||||
assert!(files.iter().any(|f| f.ends_with(".designcraft")));
|
||||
assert!(files.iter().any(|f| f.ends_with(".idml")));
|
||||
assert!(files.iter().any(|f| f.ends_with(".pdf")));
|
||||
assert!(out.join("Links").join("logo.svg").exists());
|
||||
let report = std::fs::read_to_string(out.join("Instructions.txt")).unwrap();
|
||||
assert!(report.contains("Print on matte.") && report.contains("logo.svg"), "{report}");
|
||||
// The packaged document links to its Links folder.
|
||||
let doc_file = files.iter().find(|f| f.ends_with(".designcraft")).unwrap();
|
||||
let packed = super::super::from_bytes(&std::fs::read(doc_file).unwrap()).unwrap();
|
||||
let link = packed.assets.values().next().unwrap().link.clone().unwrap();
|
||||
assert!(link.contains("Links"), "{link}");
|
||||
// Copy Links To relinks the open document.
|
||||
let r = s.execute("links.copyTo", &json!({"dir": dir.join("Copies").to_string_lossy()})).unwrap();
|
||||
assert_eq!(r["copied"], 1);
|
||||
assert!(s.doc().unwrap().doc.assets.values().next().unwrap().link.as_deref().unwrap().contains("Copies"));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[
|
|||
("app.save", "Save", Some("Cmd+S"), "{}"),
|
||||
("app.saveCopyDialog", "Save a Copy…", Some("Cmd+Alt+S"), "{} — choose where to write a copy (the document stays as it is)"),
|
||||
("app.loadSwatches", "Load Swatches…", None, "{} — pick a swatch exchange (.ase) file"),
|
||||
("app.packageDialog", "Package…", Some("Cmd+Alt+Shift+P"), "{} — choose the package folder to create (file.package)"),
|
||||
("app.saveSwatches", "Save Swatches for Exchange…", None, "{path?} — write the colour swatches as .ase"),
|
||||
("app.exportPng", "Export Page as PNG…", Some("Cmd+E"), "{}"),
|
||||
("app.exportIdml", "Export IDML…", None, "{path?} — InDesign Markup (IDML) package"),
|
||||
|
|
@ -92,6 +93,7 @@ pub const MENUS: &[(&str, &[&str])] = &[
|
|||
"ui:app.placeDialog",
|
||||
"-",
|
||||
"ui:app.exportPdf",
|
||||
"ui:app.packageDialog",
|
||||
"ui:app.exportPng",
|
||||
"ui:app.exportIdml",
|
||||
"cmd:file.exportEpub",
|
||||
|
|
@ -474,6 +476,23 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option<Result<Value,
|
|||
}
|
||||
"app.exportPng" => export_png(app, p),
|
||||
"app.exportIdml" => export_idml(app, p),
|
||||
"app.packageDialog" => {
|
||||
let name = app.session.active().map(|d| format!("{} Folder", d.doc.title)).unwrap_or_default();
|
||||
match app.services.pick_save.as_mut().and_then(|f| f(&name)) {
|
||||
Some(dir) => {
|
||||
let r = app.run("file.package", json!({"dir": dir}));
|
||||
if let Ok(v) = &r {
|
||||
app.status(format!(
|
||||
"Packaged {} files into {}",
|
||||
v["files"].as_array().map_or(0, |f| f.len()),
|
||||
v["dir"].as_str().unwrap_or("")
|
||||
));
|
||||
}
|
||||
r
|
||||
}
|
||||
None => Ok(Value::Null),
|
||||
}
|
||||
}
|
||||
"app.loadSwatches" => {
|
||||
if let Some(pick) = app.services.pick_open.as_mut() {
|
||||
return Some(match pick("swatches") {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Priority | Features | Done | Partial | Missing | Parity |
|
||||
|---|---|---|---|---|---|
|
||||
| P0 | 102 | 99 | 3 | 0 | 99% |
|
||||
| P1 | 97 | 44 | 23 | 30 | 57% |
|
||||
| P1 | 97 | 46 | 23 | 28 | 59% |
|
||||
| P2 | 76 | 8 | 7 | 61 | 15% |
|
||||
|
||||
| Area | Parity |
|
||||
|
|
@ -22,14 +22,14 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|
|||
| Transform | 93% |
|
||||
| Fill, stroke, colour | 74% |
|
||||
| Effects & transparency | 50% |
|
||||
| Placing & links | 66% |
|
||||
| Placing & links | 72% |
|
||||
| Type & text frames | 80% |
|
||||
| Typography | 79% |
|
||||
| Styles | 71% |
|
||||
| Tables | 73% |
|
||||
| Long documents | 58% |
|
||||
| Interactivity & digital | 30% |
|
||||
| Output & production | 48% |
|
||||
| Output & production | 58% |
|
||||
| XML & automation | 39% |
|
||||
| View & navigation | 68% |
|
||||
| Undo, history, saving | 100% |
|
||||
|
|
@ -175,7 +175,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Placing & links | Links panel | P0 | D |
|
||||
| Placing & links | Effective/actual PPI | P0 | D |
|
||||
| Placing & links | Relink to folder / extension | P1 | M |
|
||||
| Placing & links | Copy Links To / Package | P1 | M |
|
||||
| Placing & links | Copy Links To / Package | P1 | D |
|
||||
| Placing & links | Captions | P2 | M |
|
||||
| Placing & links | Image import options | P1 | M |
|
||||
| Placing & links | Linked text/stories | P2 | M |
|
||||
|
|
@ -280,7 +280,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
|
|||
| Output & production | Interactive PDF | P2 | P |
|
||||
| Output & production | Print dialog | P1 | M |
|
||||
| Output & production | Print booklet | P2 | M |
|
||||
| Output & production | Package | P1 | M |
|
||||
| Output & production | Package | P1 | D |
|
||||
| Output & production | Preflight | P1 | D |
|
||||
| Output & production | Export JPEG/PNG | P1 | D |
|
||||
| Output & production | Export IDML | P0 | D |
|
||||
|
|
|
|||
Loading…
Reference in New Issue