diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 9f915f6fc..76b052e2c 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -4,7 +4,7 @@ //! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]... //! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2] //! vectorcraft-cli commands -//! vectorcraft-cli convert IN OUT [--scale 2] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] +//! vectorcraft-cli convert IN OUT [--scale 2] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] //! vectorcraft-cli info FILE //! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5] //! vectorcraft-cli perf [--paths 50000] [--deep] @@ -90,7 +90,7 @@ USAGE: vectorcraft-cli commands Print the command catalogue as JSON. - vectorcraft-cli convert IN OUT [--scale N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] + vectorcraft-cli convert IN OUT [--scale N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] Open IN (any readable format) and export OUT in the format its extension picks (see Writable formats). --scale sizes a raster OUT (pixels per point, default 1); other formats leave it out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard @@ -101,10 +101,12 @@ USAGE: SVG's data-* attributes; --outline-text writes SVG text as paths, --decimals sets SVG's coordinate precision (1–7, default 3). OUT may not be IN when reading IN left things out (hidden text, art or layers it could not read; the notes say which) unless - --replace-lossy is given. --editing-data opens a .ai or an EPS with editing data from its editing copy + --replace-lossy is given. --editing-data opens a .ai or an EPS with editing data from its editing copy even where that draws differently from its PDF part or page (which can be older), as long as - it can be read. The - result has the export's warnings, and importWarnings for what reading IN left out. + it can be read. A PDF OUT omits the embedded native document unless --preserve-editing is + given (Save As PDF in the app still defaults to keeping it). With --preserve-editing the + result's warnings say the attachment was written. + The result has the export's warnings, and importWarnings for what reading IN left out. --ai-version 7 writes a .ai as the legacy version 7 (layers, groups and type kept; one file per artboard, OUT-.ai when there are several; default pdf, PDF-compatible), with --hidden-objects (default layer: a hidden layer above each layer's hidden objects; omit; @@ -274,8 +276,8 @@ fn convert(args: &[String]) -> Result<(), String> { return help(); } let mut files = vec![]; - let (mut scale, mut artboard, mut range, mut outline_text, mut replace_lossy, mut editing_data) = - (1.0f64, None::, None::, false, false, false); + let (mut scale, mut artboard, mut range, mut outline_text, mut replace_lossy, mut editing_data, mut preserve_editing) = + (1.0f64, None::, None::, false, false, false, false); let mut decimals = None::; // Legacy `.ai` options, passed on as the export's own (`aiVersion`, `hiddenObjects`, // `transparentImages`); the engine checks their values. @@ -290,6 +292,7 @@ fn convert(args: &[String]) -> Result<(), String> { "--decimals" => decimals = Some(it.next().and_then(|v| v.parse::().ok()).ok_or("--decimals needs a number")?), "--replace-lossy" => replace_lossy = true, "--editing-data" => editing_data = true, + "--preserve-editing" => preserve_editing = true, "--ai-version" => legacy.push(("aiVersion", it.next().cloned().ok_or("--ai-version needs 7 or pdf")?)), "--hidden-objects" => legacy.push(("hiddenObjects", it.next().cloned().ok_or("--hidden-objects needs layer, omit or visible")?)), "--transparent-images" => legacy.push(("transparentImages", it.next().cloned().ok_or("--transparent-images needs flatten or omit")?)), @@ -319,6 +322,9 @@ fn convert(args: &[String]) -> Result<(), String> { if let Some(decimals) = decimals { params["decimals"] = json!(decimals); } + if vectorcraft_engine::cmd::fileio::format_for_name(&output).is_some_and(|f| f.id == "pdf") { + params["preserveEditing"] = json!(preserve_editing); + } ("document.export", params) }; for (key, value) in legacy { @@ -330,6 +336,24 @@ fn convert(args: &[String]) -> Result<(), String> { if let (Some(w), Some(out)) = (opened.get("warnings").filter(|w| w.as_array().is_some_and(|a| !a.is_empty())), r.as_object_mut()) { out.insert("importWarnings".into(), w.clone()); } + if preserve_editing + && vectorcraft_engine::cmd::fileio::format_for_name(&output).is_some_and(|f| f.id == "pdf") + && let Some(obj) = r.as_object_mut() + { + let path = obj.get("path").and_then(|p| p.as_str()).unwrap_or(&output); + const ATTACHMENT: &str = "vectorcraft-editing.vectorcraft"; + let embedded = std::fs::read(path).is_ok_and(|bytes| bytes.windows(ATTACHMENT.len()).any(|w| w == ATTACHMENT.as_bytes())); + if embedded { + let note = "The PDF embeds the native VectorCraft document as an attachment (preserveEditing)."; + if let Some(Value::Array(w)) = obj.get_mut("warnings") { + if !w.iter().any(|w| w.as_str() == Some(note)) { + w.push(json!(note)); + } + } else { + obj.insert("warnings".into(), json!([note])); + } + } + } outln!("{r}"); Ok(()) } diff --git a/apps/vectorcraft-cli/tests/it/fileio.rs b/apps/vectorcraft-cli/tests/it/fileio.rs index 104aeb0fe..877a68dcc 100644 --- a/apps/vectorcraft-cli/tests/it/fileio.rs +++ b/apps/vectorcraft-cli/tests/it/fileio.rs @@ -46,6 +46,26 @@ fn info(file: &str) -> Value { serde_json::from_str(&ok(&["info", file])).unwrap() } +#[test] +/// #3627: `convert` to PDF omits the embedded native document unless asked. +#[test] +fn convert_pdf_omits_editing_data_unless_preserve_editing() { + let pdf = tmp("flyer.pdf"); + make(&pdf, 120, 80); + let plain = tmp("plain.pdf"); + ok(&["convert", &pdf, &plain]); + let bytes = std::fs::read(&plain).unwrap(); + assert!(!bytes.windows(b"vectorcraft-editing.vectorcraft".len()).any(|w| w == b"vectorcraft-editing.vectorcraft")); + let kept = tmp("kept.pdf"); + let r: Value = serde_json::from_str(&ok(&["convert", &pdf, &kept, "--preserve-editing"])).unwrap(); + let bytes = std::fs::read(&kept).unwrap(); + assert!(bytes.windows(b"vectorcraft-editing.vectorcraft".len()).any(|w| w == b"vectorcraft-editing.vectorcraft"), "{r}"); + assert!( + r["warnings"].as_array().is_some_and(|w| w.iter().any(|w| w.as_str().is_some_and(|s| s.contains("preserveEditing")))), + "{r}" + ); +} + #[test] fn convert_pdf_to_svg_and_info_on_ai() { let pdf = tmp("in.pdf");