diff --git a/crates/engine/src/cmd/fileio/native.rs b/crates/engine/src/cmd/fileio/native.rs index b36e387de..ac6cbe6b7 100644 --- a/crates/engine/src/cmd/fileio/native.rs +++ b/crates/engine/src/cmd/fileio/native.rs @@ -199,13 +199,19 @@ pub(super) fn encode_to(cmd: &str, f: &Format, doc: &Document, p: &Value, w: imp } else { std::borrow::Cow::Borrowed(doc) }; + // With a precision, the preview and PDF show the rounded art the file holds. + let drawn = match so.precision { + Some(decimals) if extras.preview || extras.pdf => Cow::Owned(rounded(&full, so.version, decimals)), + _ => Cow::Borrowed(full.as_ref()), + }; if extras.preview { - so.preview = preview_png(&full)?; + so.preview = preview_png(&drawn)?; } if extras.pdf { // Every artboard as PDF pages (no editing data: the file is the native document). - so.pdf = Some(super::pdf::encode(cmd, &full, &json!({ "range": "all", "preserveEditing": false }))?.0); + so.pdf = Some(super::pdf::encode(cmd, &drawn, &json!({ "range": "all", "preserveEditing": false }))?.0); } + drop(drawn); let native = match full { Cow::Owned(full) if so.include_linked && so.version >= vectorcraft_format::VERSION => { // Include available bytes without replacing missing, relinkable placed objects. @@ -219,6 +225,20 @@ pub(super) fn encode_to(cmd: &str, f: &Format, doc: &Document, p: &Value, w: imp vectorcraft_format::save_to(&native, &so, w).map_err(|e| bad(cmd, e.to_string())) } +/// `doc` with its path geometry rounded to `decimals` places, as [`vectorcraft_format::save_to`] +/// writes it for `version`: placed documents become groups of their art for older versions. +fn rounded(doc: &Document, version: u32, decimals: u8) -> Document { + let mut d = if version < vectorcraft_format::VERSION && doc.has_placed() { + let mut d = doc.with_placed_art().into_owned(); + d.placed_as_groups(); + d + } else { + doc.clone() + }; + d.round_geometry(decimals.min(vectorcraft_format::MAX_PRECISION)); + d +} + /// The ICC files of the profiles `doc` is tagged with that were loaded from files (the built-in /// ones are there wherever VectorCraft runs). fn embedded_profiles(doc: &Document) -> BTreeMap> { diff --git a/crates/engine/src/tests_placed_document.rs b/crates/engine/src/tests_placed_document.rs index 4a0f359ae..de0181e1e 100644 --- a/crates/engine/src/tests_placed_document.rs +++ b/crates/engine/src/tests_placed_document.rs @@ -730,6 +730,12 @@ fn saved_for_older_apps_with_a_precision_the_placed_art_is_rounded_too() { let rounded = saved(json!({"format": "vectorcraft", "version": 3, "precision": 4})); assert_eq!(rounded.len(), exact.len()); assert!(rounded.iter().all(|x| at4(*x)), "{rounded:?}"); + // The preview shows the rounded art the older app opens, not the placed file's exact art. + let file = decode(&s.execute("file.saveCopy", &json!({"version": 3, "precision": 0, "preview": true})).unwrap()); + let preview = vectorcraft_format::preview(&file); + assert!(preview.is_some()); + assert_eq!(preview, crate::cmd::fileio::preview_png(&vectorcraft_format::load(&file).unwrap()).unwrap()); + assert_ne!(preview, crate::cmd::fileio::preview_png(&s.doc().unwrap().doc).unwrap()); } #[test] diff --git a/crates/engine/src/tests_saveoptions.rs b/crates/engine/src/tests_saveoptions.rs index f1f449b75..2f9d3cafe 100644 --- a/crates/engine/src/tests_saveoptions.rs +++ b/crates/engine/src/tests_saveoptions.rs @@ -423,3 +423,39 @@ fn a_ai_file_ignores_precision_as_save_does() { } } } + +/// The anchors of every path in `n` and the art it holds. +fn all_anchors(n: &vectorcraft_doc::Node, out: &mut Vec) { + if let NodeKind::Path { path, .. } = &n.kind { + out.extend(path.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|a| a.p))); + } + for c in n.children().into_iter().flatten() { + all_anchors(c, out); + } +} + +#[test] +fn the_preview_and_pdf_of_a_save_at_a_precision_show_the_rounded_art() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + let anchors = json!([{"x": 10.4, "y": 20.3}, {"x": 80.6, "y": 30.7}, {"x": 45.35, "y": 85.65}]); + s.execute("path.create", &json!({"anchors": anchors, "closed": true})).unwrap(); + let file = bytes_of(&s.execute("file.saveCopy", &json!({"precision": 0, "pdfCompatible": true, "preview": true})).unwrap()["dataBase64"]); + // The embedded PDF, opened, has the path on whole points. + let pdf = vectorcraft_format::pdf_content(&file).unwrap(); + let mut t = Session::new(); + t.execute("document.open", &json!({"name": "x.pdf", "dataBase64": vectorcraft_format::base64_encode(&pdf)})).unwrap(); + let mut got = vec![]; + for l in &t.doc().unwrap().doc.layers { + all_anchors(l, &mut got); + } + for (x, y) in [(10.0, 20.0), (81.0, 31.0), (45.0, 86.0)] { + assert!(got.iter().any(|p| (p.x - x).abs() < 1e-3 && (p.y - y).abs() < 1e-3), "({x}, {y}) not in {got:?}"); + } + assert!(got.iter().all(|p| (p.x - p.x.round()).abs() < 1e-3 && (p.y - p.y.round()).abs() < 1e-3), "{got:?}"); + // The preview is the one the saved document draws, not the exact art's. + let preview = vectorcraft_format::preview(&file); + assert!(preview.is_some()); + assert_eq!(preview, crate::cmd::fileio::preview_png(&vectorcraft_format::load(&file).unwrap()).unwrap()); + assert_ne!(preview, crate::cmd::fileio::preview_png(&s.doc().unwrap().doc).unwrap()); +} diff --git a/docs/mcp.md b/docs/mcp.md index 48f061964..3ef070691 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -599,7 +599,7 @@ their content). `version: 2` or `1` writes a file older VectorCraft versions ope compressed), and `preview: true` embeds a PNG of the first artboard, at most 256 px on its longer side. `precision: 4` (0 to 9) rounds the anchors and handles of paths to that many decimal places (in the layers, symbols, graph designs, patterns, opacity masks and brushes); colors, transforms, opacity and type are written exactly. -Without `precision` a save writes the coordinates exactly, and Save does not remember it. A `.ai` file ignores +The embedded preview and the PDF that `pdfCompatible: true` adds show the rounded paths. Without `precision` a save writes the coordinates exactly, and Save does not remember it. A `.ai` file ignores `precision`. A save writes a temporary file and renames it over the old one, so a failed save never damages the file; only the images the document uses are written, and document keys written by a newer version are kept.