From 0584425e67c0cf89d92569e994e331abe116efe4 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Mon, 12 Oct 2026 13:19:06 +0200 Subject: [PATCH] fix(automation): content_update refuses text styling on an image (#1331) (#1391) font, size, bold, italic, color and align were never read for an added image, so content_update reported success, wrote the same image back, marked the document dirty and added an "Edit content" undo step. They are now refused with an error, as rotate, flip, crop and image already are on text, before anything is applied. Adds automation::text_styling_on_an_image_is_refused_without_an_edit, which fails before this change. --- crates/automation/src/content.rs | 3 ++ crates/automation/tests/automation.rs | 47 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) 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");