Save: include a PDF in new native files by default

Squashed from #5320 onto main.
This commit is contained in:
dotCooCoo
2026-11-09 00:25:34 +07:00
committed by bflatastic
parent f5be571d47
commit d61a7abc96
18 changed files with 579 additions and 36 deletions
+5 -1
View File
@@ -222,7 +222,8 @@ fn a_version_1_documents_twist_turns_as_it_was_saved() {
/// A save records the date the document was made in, so the same commands twice gave different
/// bytes (#2880). A pinned date makes them identical — `file.new {created}` here, and
/// `SOURCE_DATE_EPOCH` for an export that takes no date param.
/// `SOURCE_DATE_EPOCH` for an export that takes no date param. The PDF a native file carries by
/// default is dated by the document's dates, not by the time of writing.
#[test]
fn a_pinned_date_makes_saves_byte_identical() {
let once = |t: i64| {
@@ -232,6 +233,9 @@ fn a_pinned_date_makes_saves_byte_identical() {
saved(&mut s, json!({}))
};
let at = 1_791_208_980;
let pdf = vectorcraft_format::pdf_content(&once(at)).expect("a native save carries a PDF by default");
let info = vectorcraft_pdf::file_info(&pdf, None).unwrap();
assert_eq!((info.created, info.modified), (Some(at), Some(at)), "the PDF has the document's dates");
assert_eq!(once(at), once(at), "the same document and date give the same bytes");
// The date really is in the bytes, so the comparison above is not vacuous.
assert_ne!(once(at), once(at + 1), "another date shows in the bytes");
+51
View File
@@ -59,6 +59,57 @@ fn restored(r: serde_json::Value) -> usize {
r["restored"].as_array().unwrap().len()
}
#[test]
fn native_recovery_copies_leave_out_the_pdf() {
let (mut s, store) = session();
let r = s.execute("file.recovery.save", &json!({})).unwrap();
let file = format!("{}.vectorcraft", r["saved"][0]["file"].as_str().unwrap());
let bytes = store.read(&file).unwrap();
assert!(vectorcraft_format::pdf_content(&bytes).is_none());
assert_eq!(vectorcraft_format::load(&bytes).unwrap().node_count(), s.doc().unwrap().doc.node_count());
}
#[test]
fn restored_recovery_copies_keep_the_original_pdf_preference() {
let dir = crate::tests_links::Folder::new("recovered-pdf-preference");
for pdf in [false, true] {
let (mut s, store) = session();
s.execute("file.saveAs", &json!({"path": dir.file(&format!("original-{pdf}.vectorcraft")), "pdfCompatible": pdf})).unwrap();
s.execute("shape.ellipse", &json!({"x": 80, "y": 20, "width": 20, "height": 20})).unwrap();
let copied = s.execute("file.recovery.save", &json!({})).unwrap();
let file = format!("{}.vectorcraft", copied["saved"][0]["file"].as_str().unwrap());
assert!(vectorcraft_format::pdf_content(&store.read(&file).unwrap()).is_none());
drop(s);
let mut s = relaunch(&store);
assert_eq!(restored(s.execute("file.recovery.restore", &json!({})).unwrap()), 1);
assert_eq!(s.doc().unwrap().save_options.get("pdfCompatible"), Some(&json!(pdf)));
let saved = s.execute("document.save", &json!({})).unwrap();
let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap();
assert_eq!(vectorcraft_format::pdf_content(&bytes).is_some(), pdf);
}
}
#[test]
fn recovered_unsaved_documents_keep_inheriting_the_current_pdf_preference() {
let dir = crate::tests_links::Folder::new("recovered-inherited-pdf-choice");
let (mut s, store) = session();
s.execute("file.recovery.save", &json!({})).unwrap();
assert!(s.doc().unwrap().save_options.get("pdfCompatible").is_none());
s.execute("file.new", &json!({"width": 10, "height": 10})).unwrap();
s.execute("file.saveAs", &json!({"path":dir.file("other.vectorcraft"),"pdfCompatible":false})).unwrap();
assert!(s.execute("file.recovery.save", &json!({})).unwrap()["saved"].as_array().unwrap().is_empty());
let latest_preferences = s.prefs.clone();
drop(s);
let mut s = relaunch(&store);
// The host loads its latest preferences before offering recovery.
s.prefs = latest_preferences;
assert_eq!(restored(s.execute("file.recovery.restore", &json!({})).unwrap()), 1);
assert!(s.doc().unwrap().save_options.get("pdfCompatible").is_none());
let saved = s.execute("document.save", &json!({})).unwrap();
let bytes = vectorcraft_format::base64_decode(saved["dataBase64"].as_str().unwrap()).unwrap();
assert!(vectorcraft_format::pdf_content(&bytes).is_none());
}
#[test]
fn recovery_save_writes_modified_documents_once() {
let (mut s, store) = session();
+141 -2
View File
@@ -157,7 +157,146 @@ fn pdf_compatible_files_carry_a_pdf() {
assert!(pdf.starts_with(b"%PDF"));
let info = s.execute("document.pdfInfo", &json!({"dataBase64": vectorcraft_format::base64_encode(&pdf)})).unwrap();
assert_eq!(info["pages"], 3);
assert!(vectorcraft_format::pdf_content(&bytes_of(&s.execute("file.saveCopy", &json!({})).unwrap()["dataBase64"])).is_none());
assert!(
vectorcraft_format::pdf_content(&bytes_of(&s.execute("file.saveCopy", &json!({"pdfCompatible": false})).unwrap()["dataBase64"])).is_none()
);
}
/// A native save is portable to the other apps by default. The option is still an explicit
/// opt-out, and a remembered opt-out survives later saves of the document.
#[test]
fn native_saves_carry_all_artboards_as_pdf_by_default() {
let mut s = three_boards();
for compress in [false, true] {
let result = s.execute("file.saveCopy", &json!({"compress": compress})).unwrap();
let file = bytes_of(&result["dataBase64"]);
let pdf = vectorcraft_format::pdf_content(&file).expect("the default native save carries a PDF");
let info = s.execute("document.pdfInfo", &json!({"dataBase64": vectorcraft_format::base64_encode(&pdf)})).unwrap();
assert_eq!(info["pages"], 3);
assert_eq!(vectorcraft_format::load(&file).unwrap().artboards.len(), 3);
}
let dir = Folder::new("pdf-opt-out");
let path = dir.file("compact.vectorcraft");
s.execute("file.saveAs", &json!({"path": path, "pdfCompatible": false})).unwrap();
s.execute("shape.rectangle", &json!({"x": 60, "y": 10, "width": 10, "height": 10})).unwrap();
s.execute("document.save", &json!({})).unwrap();
assert!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_none());
let options = s.execute("file.formatOptions", &json!({"format":"vectorcraft"})).unwrap();
assert_eq!(options["options"]["pdfCompatible"]["value"], false);
s.execute("document.save", &json!({"compress": true})).unwrap();
assert!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_none());
s.execute("document.open", &json!({"path": path})).unwrap();
assert_eq!(s.doc().unwrap().save_options["pdfCompatible"], false);
let options = s.execute("file.formatOptions", &json!({"format": "vectorcraft"})).unwrap();
assert_eq!(options["options"]["pdfCompatible"]["value"], false);
s.execute("document.save", &json!({})).unwrap();
assert!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_none());
s.execute("document.save", &json!({"pdfCompatible": true})).unwrap();
assert!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_some());
}
#[test]
fn native_saves_without_artboards_keep_the_editable_document() {
let mut s = three_boards();
std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).artboards.clear();
let result = s.execute("file.saveCopy", &json!({})).unwrap();
let file = bytes_of(&result["dataBase64"]);
let restored = vectorcraft_format::load(&file).unwrap();
assert!(restored.artboards.is_empty());
assert_eq!(restored.node_count(), s.doc().unwrap().doc.node_count());
assert!(vectorcraft_format::pdf_content(&file).is_none());
s.execute("document.open", &json!({"name": "no-artboards.vectorcraft", "dataBase64": vectorcraft_format::base64_encode(&file)})).unwrap();
assert_eq!(s.doc().unwrap().save_options["pdfCompatible"], true);
s.execute("artboard.new", &json!({"x": 0, "y": 0, "width": 100, "height": 80})).unwrap();
let result = s.execute("file.saveCopy", &json!({})).unwrap();
assert!(vectorcraft_format::pdf_content(&bytes_of(&result["dataBase64"])).is_some());
}
#[test]
fn native_template_saves_keep_the_document_pdf_preference() {
let mut s = three_boards();
let dir = Folder::new("template-pdf-preference");
s.execute("file.saveAs", &json!({"path": dir.file("compact.vectorcraft"), "pdfCompatible": false, "version": 3})).unwrap();
let options = s.execute("file.formatOptions", &json!({"format": "template"})).unwrap();
assert_eq!(options["options"]["pdfCompatible"]["value"], false, "the save dialog must offer the same retained choice");
let r = s.execute("file.saveAsTemplate", &json!({"format": "template"})).unwrap();
let bytes = bytes_of(&r["dataBase64"]);
assert!(vectorcraft_format::pdf_content(&bytes).is_none());
assert_eq!(
vectorcraft_format::load_file(&bytes).unwrap().info.version,
vectorcraft_format::VERSION,
"unrelated format options are not carried over"
);
let r = s.execute("file.saveAsTemplate", &json!({"format": "template", "pdfCompatible": true})).unwrap();
assert!(vectorcraft_format::pdf_content(&bytes_of(&r["dataBase64"])).is_some());
}
#[test]
fn a_default_native_save_keeps_its_pdf_after_another_document_opts_out() {
let dir = Folder::new("default-native-pdf-choice");
for (index, options) in [json!({}), json!({"options":{"pdfCompatible":null}})].into_iter().enumerate() {
let mut s = three_boards();
let path = dir.file(&format!("first-{index}.vectorcraft"));
let mut params = options.as_object().unwrap().clone();
params.insert("path".into(), json!(path));
s.execute("file.saveAs", &Value::Object(params)).unwrap();
assert!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_some());
s.execute("file.new", &json!({"width": 10, "height": 10})).unwrap();
s.execute("file.saveAs", &json!({"path":dir.file(&format!("other-{index}.vectorcraft")),"pdfCompatible":false})).unwrap();
s.execute("document.activate", &json!({"index":0})).unwrap();
s.execute("document.save", &json!({})).unwrap();
assert!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_some(), "the first document keeps the PDF it was saved with");
assert_eq!(s.doc().unwrap().save_options.get("pdfCompatible"), Some(&json!(true)));
}
}
#[test]
fn a_null_nested_pdf_option_keeps_the_saved_opt_out() {
let dir = Folder::new("null-nested-pdf-option");
let mut s = three_boards();
let path = dir.file("compact.vectorcraft");
s.execute("file.saveAs", &json!({"path": path, "pdfCompatible": false})).unwrap();
s.execute("document.save", &json!({"options": {"pdfCompatible": null}})).unwrap();
assert!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_none(), "null leaves the saved choice");
assert_eq!(s.doc().unwrap().save_options.get("pdfCompatible"), Some(&json!(false)));
}
#[test]
fn reopening_native_files_keeps_each_files_pdf_compatibility() {
let dir = Folder::new("pdf-reopened-options");
let mut s = three_boards();
let pdf = dir.file("portable.vectorcraft");
let compact = dir.file("compact.vectorcraft");
s.execute("file.saveAs", &json!({"path": pdf, "pdfCompatible": true})).unwrap();
s.execute("file.saveAs", &json!({"path": compact, "pdfCompatible": false})).unwrap();
s.execute("file.new", &json!({"width": 20, "height": 20})).unwrap();
let copy = s.execute("file.saveCopy", &json!({})).unwrap();
assert!(vectorcraft_format::pdf_content(&bytes_of(&copy["dataBase64"])).is_none(), "new files use the last native preference");
for (path, expected) in [(&pdf, true), (&compact, false)] {
s.execute("document.open", &json!({"path": path})).unwrap();
let options = s.execute("file.formatOptions", &json!({"format": "vectorcraft"})).unwrap();
assert_eq!(options["options"]["pdfCompatible"]["value"], expected);
s.execute("document.save", &json!({})).unwrap();
assert_eq!(vectorcraft_format::pdf_content(&std::fs::read(path).unwrap()).is_some(), expected);
}
}
#[test]
fn reverting_native_files_updates_their_pdf_compatibility() {
let dir = Folder::new("pdf-revert-options");
for saved in [false, true] {
let mut s = three_boards();
let path = dir.file(if saved { "with-pdf.vectorcraft" } else { "compact.vectorcraft" });
s.execute("file.saveAs", &json!({"path": path, "pdfCompatible": saved})).unwrap();
let replacement = s.execute("file.saveCopy", &json!({"pdfCompatible": !saved})).unwrap();
std::fs::write(&path, bytes_of(&replacement["dataBase64"])).unwrap();
s.execute("shape.rectangle", &json!({"x": 70, "y": 20, "width": 8, "height": 8})).unwrap();
s.execute("file.revert", &json!({})).unwrap();
let options = s.execute("file.formatOptions", &json!({"format": "vectorcraft"})).unwrap();
assert_eq!(options["options"]["pdfCompatible"]["value"], !saved);
s.execute("document.save", &json!({})).unwrap();
assert_eq!(vectorcraft_format::pdf_content(&std::fs::read(&path).unwrap()).is_some(), !saved);
}
}
#[test]
@@ -429,7 +568,7 @@ fn format_options_list_the_save_options() {
assert_eq!(v["options"][k]["value"], value, "{k}");
}
let v = s.execute("file.formatOptions", &json!({"format": "vectorcraft"})).unwrap();
assert_eq!((v["options"]["pdfCompatible"]["value"].as_bool(), v["options"]["range"]["value"].is_null()), (Some(false), true));
assert_eq!((v["options"]["pdfCompatible"]["value"].as_bool(), v["options"]["range"]["value"].is_null()), (Some(true), true));
// Saved with them, a document remembers them for Save.
let dir = Folder::new("remember");
let path = dir.file("r.vectorcraft");