Native format: the preview and PDF of a save at a fixed precision show the rounded art

A save with `precision` rounded only the native document, and drew the
embedded preview and the `pdfCompatible` PDF from the exact document.
An app reading the PDF showed paths up to 0.5 pt away from what
VectorCraft reopens at `precision: 0`.

When `precision` is set, the preview and the PDF are now drawn from a
copy of the document rounded as the file holds it. For an older version,
placed documents in that copy become groups of their art first, as the
file stores them, so their rounded art shows too.

Tests: a save at precision 0 puts the path in the embedded PDF on the
rounded coordinates, and its preview matches the one drawn from the
saved document; the same for a version 3 save of a placed document.
This commit is contained in:
dotCooCoo
2026-10-25 08:35:53 -07:00
parent 60700e00fc
commit a4878b873e
4 changed files with 65 additions and 3 deletions
+22 -2
View File
@@ -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<String, Vec<u8>> {
@@ -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]
+36
View File
@@ -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<vectorcraft_geom::Point>) {
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());
}
+1 -1
View File
@@ -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.