From 7855d8ede4542273175b3c796e905678e6ddb9bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 16:36:42 +0000 Subject: [PATCH 1/5] Fix #3627: convert to PDF omits editing data by default ## Summary `vectorcraft-cli convert` to PDF now passes `preserveEditing: false`, matching a command-line converter expectation while Save As PDF in the app still uses the VectorCraft Default preset. `--preserve-editing` opts in and adds a JSON warning when the native document attachment is written. ## Tests Integration test in `vectorcraft-cli` checks default PDF output has no editing attachment and `--preserve-editing` embeds it with a warning. Closes storytold/vectorcraft#3627 Co-authored-by: nrq6993-a11y --- apps/vectorcraft-cli/src/main.rs | 38 ++++++++++++++++++++----- apps/vectorcraft-cli/tests/it/fileio.rs | 20 +++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) 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"); From 4e0c422447e4e8689be51f7ef3c0adbb7c554c9e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 16:37:31 +0000 Subject: [PATCH 2/5] Fix duplicate test attribute in fileio test Co-authored-by: nrq6993-a11y --- apps/vectorcraft-cli/tests/it/fileio.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/vectorcraft-cli/tests/it/fileio.rs b/apps/vectorcraft-cli/tests/it/fileio.rs index 877a68dcc..22807b139 100644 --- a/apps/vectorcraft-cli/tests/it/fileio.rs +++ b/apps/vectorcraft-cli/tests/it/fileio.rs @@ -46,7 +46,6 @@ 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() { @@ -60,10 +59,7 @@ fn convert_pdf_omits_editing_data_unless_preserve_editing() { 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}" - ); + 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] From 1217313e8aa51aaac956feee98de8493dafad785 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 22 Oct 2026 16:37:40 +0000 Subject: [PATCH 3/5] Fix #3627: convert to PDF omits editing data by default ## Summary `vectorcraft-cli convert` to PDF now passes `preserveEditing: false`, matching a command-line converter expectation while Save As PDF in the app still uses the VectorCraft Default preset. `--preserve-editing` opts in and adds a JSON warning when the native document attachment is written. ## Tests Integration test in `vectorcraft-cli` checks default PDF output has no editing attachment and `--preserve-editing` embeds it with a warning. Closes storytold/vectorcraft#3627 Co-authored-by: nrq6993-a11y --- apps/vectorcraft-cli/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 76b052e2c..2425f5b4e 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -104,7 +104,7 @@ USAGE: --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. 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 + given (the app's Save As PDF 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 From 1579b65b07094bedcdc37dad53ba09dcacd01ab5 Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Sun, 25 Oct 2026 22:54:53 +0300 Subject: [PATCH 4/5] convert: attachment note from the export result's warnings, not a file scan --- apps/vectorcraft-cli/src/main.rs | 12 +++++++----- apps/vectorcraft-cli/tests/it/fileio.rs | 7 +++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 2425f5b4e..1f4c7e53e 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -104,8 +104,9 @@ USAGE: --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. A PDF OUT omits the embedded native document unless --preserve-editing is - given (the app's Save As PDF still defaults to keeping it). With --preserve-editing the - result's warnings say the attachment was written. + given (the app's Save As PDF still defaults to keeping it). With --preserve-editing, when the + attachment is written the result's warnings say so (it is left out, with its own warning, for + --artboard or a --range without every artboard). 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 @@ -340,9 +341,10 @@ fn convert(args: &[String]) -> Result<(), String> { && 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())); + // The export leaves the attachment out (with this warning) for --artboard or a --range + // without every artboard; otherwise it writes it. Read that from the result, not the file. + let skipped = vectorcraft_engine::cmd::fileio::pdf::EDITING_NEEDS_EVERY_ARTBOARD; + let embedded = !obj.get("warnings").and_then(Value::as_array).is_some_and(|w| w.iter().any(|w| w.as_str() == Some(skipped))); 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") { diff --git a/apps/vectorcraft-cli/tests/it/fileio.rs b/apps/vectorcraft-cli/tests/it/fileio.rs index 22807b139..1423b78b4 100644 --- a/apps/vectorcraft-cli/tests/it/fileio.rs +++ b/apps/vectorcraft-cli/tests/it/fileio.rs @@ -60,6 +60,13 @@ fn convert_pdf_omits_editing_data_unless_preserve_editing() { 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}"); + // A --range without every artboard leaves the attachment out: no "embeds" note, even when the + // output's own name contains the attachment's name. + let part = tmp("vectorcraft-editing.vectorcraft.pdf"); + let r: Value = serde_json::from_str(&ok(&["convert", &pdf, &part, "--preserve-editing", "--range", "2"])).unwrap(); + let warnings = r["warnings"].as_array().cloned().unwrap_or_default(); + assert!(warnings.iter().any(|w| w.as_str().is_some_and(|s| s.contains("every artboard"))), "{r}"); + assert!(!warnings.iter().any(|w| w.as_str().is_some_and(|s| s.contains("preserveEditing"))), "{r}"); } #[test] From aadf73becd898f56c6f4983742a1d224909989c2 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 26 Oct 2026 08:21:26 +0700 Subject: [PATCH 5/5] CLI help: one space before --editing-data, as before #4147 --- apps/vectorcraft-cli/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index 1f4c7e53e..6ec320ea2 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -101,7 +101,7 @@ 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. A PDF OUT omits the embedded native document unless --preserve-editing is given (the app's Save As PDF still defaults to keeping it). With --preserve-editing, when the