diff --git a/crates/ui-egui/src/dialogs/save_pdf.rs b/crates/ui-egui/src/dialogs/save_pdf.rs index 5b4f99188..175943697 100644 --- a/crates/ui-egui/src/dialogs/save_pdf.rs +++ b/crates/ui-egui/src/dialogs/save_pdf.rs @@ -95,8 +95,20 @@ pub fn open(app: &mut VectorcraftApp, params: &Value) -> Result { /// dialog fields. A standard the writer can't produce yet still shows (Save PDF then says so). fn settings_fields(app: &VectorcraftApp, params: &Value) -> Result, String> { let settings = pdf::resolve("ui.savePdfDialog", params, &app.session.prefs.pdf_presets).map_err(|e| e.to_string())?; - match serde_json::to_value(settings).map_err(|e| e.to_string())? { - Value::Object(fields) => Ok(fields), + match serde_json::to_value(&settings).map_err(|e| e.to_string())? { + Value::Object(mut fields) => { + // Serialization omits passwords from presets and summaries; the live dialog needs + // the validated passwords so confirming it still encrypts the PDF. + let security = fields.get_mut("security").and_then(Value::as_object_mut).ok_or("PDF security settings are not an object")?; + for (key, password) in + [("openPassword", settings.security.open_password), ("permissionsPassword", settings.security.permissions_password)] + { + if !password.is_empty() { + security.insert(key.into(), json!(password)); + } + } + Ok(fields) + } _ => Err("PDF settings are not an object".into()), } } @@ -1020,6 +1032,46 @@ mod tests { assert_eq!(SECTIONS[section_of("standard")], "General"); } + #[test] + fn confirming_preserves_passwords_supplied_when_opening_the_dialog() { + for (open, owner) in [("open", ""), ("", "owner"), ("open", "owner")] { + let (mut app, written, _) = app(); + app.run( + "ui.savePdfDialog", + json!({"path": "/tmp/locked.pdf", "security": {"openPassword": open, "permissionsPassword": owner, "printing": "none"}}), + ) + .unwrap(); + // Drawing the Security and Summary sections must retain the live passwords. + for section in ["Security", "Summary"] { + set_field(&mut app, "__section", json!(section)); + frame(&mut app); + } + let p = params(app.ui.dialog.as_ref().unwrap()); + let summary = summary_of(&mut app, &egui::Context::default(), &p).unwrap(); + assert!(!summary.to_string().contains("Password"), "summaries omit passwords"); + let mut d = app.ui.dialog.clone().unwrap(); + set(&mut d, SAVE_AS, json!("Protected")); + save_preset(&mut app, &mut d).unwrap(); + let saved = &app.session.prefs.pdf_presets[0].settings.security; + assert!(saved.open_password.is_empty() && saved.permissions_password.is_empty(), "presets forget passwords"); + + super::super::confirm(&mut app).unwrap(); + assert!(app.ui.dialog.is_none()); + let written = written.borrow(); + let bytes = &written[0].1; + assert!(String::from_utf8_lossy(bytes).contains("/Encrypt")); + if open.is_empty() { + assert_eq!(vectorcraft_pdf::info(bytes, None).unwrap().pages.len(), 3); + } else { + assert_eq!(vectorcraft_pdf::info(bytes, None).unwrap_err(), vectorcraft_pdf::PdfError::NeedsPassword); + } + assert_eq!(vectorcraft_pdf::info(bytes, Some("wrong")).unwrap_err(), vectorcraft_pdf::PdfError::WrongPassword); + for password in [open, owner].into_iter().filter(|p| !p.is_empty()) { + assert_eq!(vectorcraft_pdf::info(bytes, Some(password)).unwrap().pages.len(), 3); + } + } + } + #[test] fn the_security_section_takes_passwords_that_presets_drop() { let (mut app, _, _) = app();