diff --git a/crates/automation/src/content.rs b/crates/automation/src/content.rs index 39cdaf96..0e97aa8b 100644 --- a/crates/automation/src/content.rs +++ b/crates/automation/src/content.rs @@ -164,6 +164,9 @@ impl Automation { if a.get("text").is_some() { return Err(bad("images have no text")); } + if ["font", "size", "bold", "italic", "color", "align"].iter().any(|k| a.get(k).is_some()) { + return Err(bad("font, size, bold, italic, color and align apply to text")); + } if let Some(deg) = a.opt_int("rotate")? { if deg.rem_euclid(90) != 0 { return Err(bad("rotate must be a multiple of 90")); diff --git a/crates/automation/tests/automation.rs b/crates/automation/tests/automation.rs index abcf8e86..644ae404 100644 --- a/crates/automation/tests/automation.rs +++ b/crates/automation/tests/automation.rs @@ -3020,6 +3020,53 @@ fn adding_content_through_tools() { assert!(matches!(a.call("content_delete", &json!({ "doc": doc, "page": 1, "index": 5 })), Err(ToolError::Failed(_)))); } +/// Text styling on an added image is refused (#1331), as image transforms are on text, and the +/// refusal leaves the content, the dirty flag and the undo and redo steps as they were. +#[test] +fn text_styling_on_an_image_is_refused_without_an_edit() { + let dir = workdir("content-style"); + let mut png = Vec::new(); + { + let mut enc = png::Encoder::new(&mut png, 1, 1); + enc.set_color(png::ColorType::Rgb); + enc.write_header().unwrap().write_image_data(&[80, 160, 200]).unwrap(); + } + std::fs::write(dir.join("logo.png"), png).unwrap(); + let mut a = auto(&dir); + let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap(); + ok(&mut a, "page_add_text", json!({ "doc": doc, "page": 1, "text": "sample", "at": [20, 20] })); + ok(&mut a, "page_add_image", json!({ "doc": doc, "page": 1, "path": "logo.png", "rect": [10, 100, 40, 130] })); + ok(&mut a, "content_update", json!({ "doc": doc, "page": 1, "index": 2, "rotate": 90 })); + ok(&mut a, "edit_undo", json!({ "doc": doc })); + ok(&mut a, "doc_save", json!({ "doc": doc, "path": "saved.pdf" })); + let state = |a: &Automation| { + let d = a.session().docs().iter().find(|d| d.id.0 == doc).unwrap(); + (format!("{:?}", d.added), d.dirty, d.edit_generation(), d.can_undo().map(str::to_owned), d.can_redo().map(str::to_owned)) + }; + let before = state(&a); + assert!(!before.1 && before.4.is_some(), "a clean document with a step to redo: {before:?}"); + for (key, value) in [ + ("font", json!("times")), + ("size", json!(24)), + ("bold", json!(true)), + ("italic", json!(true)), + ("color", json!("red")), + ("align", json!("center")), + ] { + for rect in [None, Some(json!([20, 100, 50, 130]))] { + let mut args = json!({ "doc": doc, "page": 1, "index": 2 }); + args[key] = value.clone(); + if let Some(r) = rect { + args["rect"] = r; + } + assert!(matches!(a.call("content_update", &args), Err(ToolError::InvalidArgs(_))), "{args}"); + assert_eq!(state(&a), before, "{args}"); + } + } + ok(&mut a, "content_update", json!({ "doc": doc, "page": 1, "index": 1, "font": "times", "size": 24, "align": "center" })); + assert!(state(&a).1, "text still takes its styling"); +} + #[test] fn form_formats_and_calculations_through_tools() { let dir = workdir("formcalc");