//! PDF and .ai files that reopen as they were: Preserve Editing embeds the native document, which //! `document.open` restores unless the pages changed elsewhere; `.ai` saves always carry it and //! `.ait` templates open from it untitled. use serde_json::{Value, json}; use super::tests_svgedit::{comparable, rich}; use super::*; fn b64(v: &Value) -> Vec { vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap_or_else(|| panic!("no dataBase64 in {v}"))).unwrap() } fn open(s: &mut Session, name: &str, bytes: &[u8]) -> Value { s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(bytes)})).unwrap() } fn tmp(name: &str) -> String { let dir = std::env::temp_dir().join(format!("vc-pdfedit-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); dir.join(name).to_string_lossy().to_string() } /// `pdf` with `from` replaced by `to` (the same length). fn patched(pdf: &[u8], from: &[u8], to: &[u8]) -> Vec { assert_eq!(from.len(), to.len()); let at = pdf.windows(from.len()).position(|w| w == from).unwrap_or_else(|| panic!("no {:?}", String::from_utf8_lossy(from))); let mut out = pdf.to_vec(); out[at..at + to.len()].copy_from_slice(to); out } #[test] fn a_pdf_saved_with_preserve_editing_reopens_as_the_same_document() { let mut s = rich(); let before = comparable(&s.doc().unwrap().doc); // On in the default preset. let v = s.execute("document.exportPdf", &json!({})).unwrap(); assert_eq!(v["warnings"].as_array().map(|w| w.iter().any(|w| w.as_str().unwrap().contains("Preserve"))), Some(false), "{v}"); let pdf = b64(&v); let r = open(&mut s, "rich.pdf", &pdf); assert_eq!((&r["warnings"], &r["restored"]), (&json!([]), &json!(true)), "{r}"); assert_eq!(comparable(&s.doc().unwrap().doc), before, "the document came back exactly"); assert_eq!(s.doc().unwrap().path, None, "a PDF is not saved over"); // Picking pages imports them as artwork. let r = s.execute("document.open", &json!({"name": "rich.pdf", "dataBase64": vectorcraft_format::base64_encode(&pdf), "pages": "1"})).unwrap(); assert_eq!(r["restored"], false, "{r}"); // Off: the pages import as artwork. let plain = b64(&s.execute("document.exportPdf", &json!({"preserveEditing": false})).unwrap()); assert!(vectorcraft_pdf::editing(&plain).is_none()); assert_eq!(open(&mut s, "plain.pdf", &plain)["restored"], false); // document.export and serialize take the same option. let v = s.execute("document.serialize", &json!({"format": "pdf"})).unwrap(); assert!(vectorcraft_pdf::editing(&b64(&v)).is_some_and(|e| e.intact)); } #[test] fn pages_changed_elsewhere_open_as_artwork_with_a_warning() { let mut s = rich(); let pdf = b64(&s.execute("document.exportPdf", &json!({"compression": {"compressText": false}})).unwrap()); // Another app changes the artboard's height (300 × 200 pt). let edited = patched(&pdf, b"/MediaBox[0 0 300 200]", b"/MediaBox[0 0 300 201]"); let r = open(&mut s, "edited.pdf", &edited); assert_eq!((&r["warnings"][0], &r["restored"]), (&json!(load::EDITING_STALE), &json!(false)), "{r}"); assert!(s.doc().unwrap().doc.symbols.is_empty(), "plain import: no symbols"); // Editing data that isn't a native document can't be read. let damaged = patched(&pdf, b"{\"format\":\"vectorcraft\"", b"{\"format\":\"vectorcrafX\""); let r = open(&mut s, "damaged.pdf", &damaged); assert_eq!(r["warnings"][0], load::EDITING_DAMAGED, "{r}"); } #[test] fn the_legacy_payload_name_is_accepted() { let mut s = rich(); let before = comparable(&s.doc().unwrap().doc); let pdf = b64(&s.execute("document.exportPdf", &json!({"compression": {"compressText": false}})).unwrap()); let (name, legacy) = (format!("({})", vectorcraft_pdf::EDITING_FILE), format!("({})", vectorcraft_pdf::LEGACY_EDITING_FILE)); let legacy = format!("{legacy}{}", " ".repeat(name.len() - legacy.len())); let mut old = pdf.clone(); while let Some(at) = old.windows(name.len()).position(|w| w == name.as_bytes()) { old[at..at + name.len()].copy_from_slice(legacy.as_bytes()); } assert!(!String::from_utf8_lossy(&old).contains(vectorcraft_pdf::EDITING_FILE)); assert_eq!(open(&mut s, "old.pdf", &old)["restored"], true); assert_eq!(comparable(&s.doc().unwrap().doc), before); } #[test] fn ai_files_are_pdfs_that_save_and_reopen_editable() { let mut s = rich(); let path = tmp("art.ai"); let r = s.execute("document.save", &json!({"path": path, "preserveEditing": false, "range": "1"})).unwrap(); assert_eq!(r["path"], path.as_str()); // As saved (Save stamps the File Info dates). let before = comparable(&s.doc().unwrap().doc); let bytes = std::fs::read(&path).unwrap(); assert!(bytes.starts_with(b"%PDF"), ".ai is PDF-compatible"); assert!(vectorcraft_pdf::editing(&bytes).is_some_and(|e| e.intact), "always carries the document"); assert_eq!(s.doc().unwrap().path.as_deref(), Some(path.as_str()), "the document takes the path"); // Reopening restores it and keeps the path: Save writes .ai again. let r = s.execute("document.open", &json!({"path": path})).unwrap(); assert_eq!((&r["format"], &r["restored"]), (&json!("ai"), &json!(true)), "{r}"); assert_eq!(comparable(&s.doc().unwrap().doc), before); assert_eq!(s.doc().unwrap().path.as_deref(), Some(path.as_str())); s.execute("shape.rectangle", &json!({"x": 1, "y": 1, "width": 5, "height": 5})).unwrap(); s.execute("document.save", &json!({})).unwrap(); let again = vectorcraft_pdf::editing(&std::fs::read(&path).unwrap()).unwrap(); assert_eq!(vectorcraft_format::load(&again.data).unwrap().layers, s.doc().unwrap().doc.layers); // Save As also writes a plain PDF (the document then saves back as PDF); PNG stays an export. assert_eq!(s.execute("document.save", &json!({"path": tmp("x.pdf")})).unwrap()["format"], "pdf"); assert!(s.execute("document.save", &json!({"path": tmp("x.png")})).is_err(), "PNG is written with document.export"); // A plain PDF named .ai opens as artwork, without a path to save over. let plain = b64(&s.execute("document.exportPdf", &json!({"preserveEditing": false})).unwrap()); let plain_path = tmp("plain.ai"); std::fs::write(&plain_path, plain).unwrap(); let r = s.execute("document.open", &json!({"path": plain_path})).unwrap(); assert_eq!(r["restored"], false); assert_eq!(s.doc().unwrap().path, None); } /// #526: the exports (`document.export`, the CLI's `--export` and `convert`) write `.ai` as Save /// As does, leaving the document's own path alone. #[test] fn exports_write_ai_as_save_as_does() { let mut s = rich(); let before = comparable(&s.doc().unwrap().doc); let path = tmp("exported.ai"); let r = s.execute("document.export", &json!({"path": path})).unwrap(); assert_eq!(r["format"], "ai", "{r}"); let bytes = std::fs::read(&path).unwrap(); assert!(bytes.starts_with(b"%PDF") && vectorcraft_pdf::editing(&bytes).is_some_and(|e| e.intact), "PDF-compatible, carrying the document"); assert_eq!(s.doc().unwrap().path, None, "an export doesn't give the document a path"); let r = s.execute("document.open", &json!({"path": path})).unwrap(); assert_eq!((&r["format"], &r["restored"]), (&json!("ai"), &json!(true)), "{r}"); assert_eq!(comparable(&s.doc().unwrap().doc), before); // Its options as Save As takes them; no path gives the bytes. let r = s.execute("document.export", &json!({"format": "ai", "pdfCompatible": false})).unwrap(); assert!(r["warnings"].as_array().unwrap().iter().any(|w| w.as_str().is_some_and(|w| w.contains("without PDF content"))), "{r}"); assert!(b64(&r).starts_with(b"%PDF")); } #[test] fn ait_templates_open_their_document_untitled() { let mut s = rich(); let before = comparable(&s.doc().unwrap().doc); // A never-saved document's .ai comes back as bytes. let ai = b64(&s.execute("document.save", &json!({"format": "ai"})).unwrap()); assert!(ai.starts_with(b"%PDF")); let r = open(&mut s, "brochure.ait", &ai); assert_eq!((&r["format"], &r["restored"]), (&json!("ait"), &json!(true)), "{r}"); assert!(r["title"].as_str().unwrap().starts_with("Untitled-"), "{r}"); assert_eq!(s.doc().unwrap().path, None); assert_eq!(comparable(&s.doc().unwrap().doc), before); } #[test] fn standards_and_screens_leave_the_editing_data_out() { let mut s = rich(); // Choosing PDF/A turns Preserve Editing off unless it is asked for, which PDF/A refuses. let pdf_a = b64(&s.execute("document.exportPdf", &json!({"standard": "pdfA2b"})).unwrap()); assert!(vectorcraft_pdf::editing(&pdf_a).is_none()); let e = s.execute("document.exportPdf", &json!({"standard": "pdfA2b", "preserveEditing": true})).unwrap_err(); assert!(matches!(e, crate::EngineError::BadParams { .. }), "{e}"); // Export for Screens writes a PDF per artboard for viewing. let r = s.execute("document.exportForScreens", &json!({"formats": [{"format": "pdf"}]})).unwrap(); let file = vectorcraft_format::base64_decode(r["files"][0]["dataBase64"].as_str().unwrap()).unwrap(); assert!(vectorcraft_pdf::editing(&file).is_none()); } #[test] fn save_dialogs_offer_every_format_save_writes() { let labels = |name: &str| save_filters(name).into_iter().map(|(l, _)| l).collect::>(); assert_eq!(labels("a.ai")[0], "PDF-compatible .ai"); assert_eq!(labels("a.vectorcraft").len(), SAVE_FORMATS.len()); assert_eq!(labels("a.svg")[0], "SVG"); assert!(labels("a.png").is_empty() && labels("noext").is_empty(), "exports pick their own name"); assert_eq!(save_format(Some("ai"), None).unwrap().id, "ai"); assert_eq!(save_format(None, Some("x.AI")).unwrap().id, "ai"); assert!(save_format(Some("png"), None).is_err()); } #[test] fn a_pdf_of_some_artboards_carries_no_editing_data() { let mut s = Session::new(); s.execute("file.new", &json!({"width": 100, "height": 80, "artboards": 3})).unwrap(); for (range, kept) in [("2", false), ("1,3", false), ("3,2,1", false), ("1-3", true), ("all", true)] { let v = s.execute("document.exportPdf", &json!({ "range": range })).unwrap(); assert_eq!(vectorcraft_pdf::editing(&b64(&v)).is_some(), kept, "{range}"); let warned = v["warnings"].as_array().unwrap().iter().any(|w| w == super::pdf::EDITING_NEEDS_EVERY_ARTBOARD); assert_eq!(warned, !kept, "{range}: {v}"); } }