From c73eb76e920ac052bb51310aeb6145e8ef6395d9 Mon Sep 17 00:00:00 2001 From: Earl Co Date: Wed, 14 Oct 2026 23:03:26 +0200 Subject: [PATCH] fix(forms): draw the widget border inside /Tx BMC so Acrobat keeps it on focus changes (#1613) --- crates/forms/src/appearance.rs | 28 ++++++++++++------------- crates/forms/src/tests.rs | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 14 deletions(-) diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index dc5ffcef1..a9558bbe0 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -426,20 +426,20 @@ pub fn field_appearance_drawn(doc: &Document, f: &Field, w: &Widget, values: &[S } } } - let mut content = c.into_bytes(); - content.extend( - format!( - "/Tx BMC\nq\n{} {} {} {} re W n\nBT\n/{} {} Tf\n{}\n", - n(bw), - n(bw), - n(width - 2.0 * bw), - n(height - 2.0 * bw), - font_name, - n(size), - da.color - ) - .bytes(), - ); + // Acrobat regenerates only what is inside /Tx BMC when it redraws a field (e.g. on focus + // changes), discarding what came before it (issues #1613, #633). The background and border + // therefore go inside the marked-content block too, so a redraw keeps them with the text. + let mut content = format!( + "/Tx BMC\nq\n{c}{} {} {} {} re W n\nBT\n/{} {} Tf\n{}\n", + n(bw), + n(bw), + n(width - 2.0 * bw), + n(height - 2.0 * bw), + font_name, + n(size), + da.color + ) + .into_bytes(); content.extend(body); content.extend_from_slice(b"ET\nQ\nEMC\n"); let mut fonts = Dict::new(); diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index 0256bfafb..23691aa17 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -277,6 +277,44 @@ fn text_fields_get_new_appearances() { assert!(raw.windows(8).any(|x| x == b"(Z\xfcrich)")); } +/// Acrobat regenerates only the `/Tx BMC` block when it redraws a text field, so background and +/// border must live inside it rather than immediately before it (#1613). +fn marked_content_borders_field(appearance: &str, label: &str) { + let start = appearance.find("/Tx BMC").expect("no /Tx BMC"); + let marked = appearance[start..].split_once("EMC").expect("no matching EMC").0; + assert!(marked.contains("1 1 0.9 rg"), "background inside /Tx BMC: {appearance} {label}"); + assert!(marked.contains("0 0 0 RG\n1 w\n0.5 0.5 199 19 re S"), "border inside /Tx BMC: {appearance} {label}"); +} + +#[test] +fn text_and_date_fields_keep_background_and_border_inside_marked_content() { + let mut doc = one_page(); + add_field(&mut doc, 0, [50.0, 700.0, 250.0, 720.0], &NewField::Text { multiline: false }, Some("name")).unwrap(); + add_field(&mut doc, 0, [50.0, 660.0, 250.0, 680.0], &NewField::Date, Some("due")).unwrap(); + let props = || FieldProps { + look: Some(Look { + border: Some([0.0; 3]), + fill: Some([1.0, 1.0, 0.9]), + width: 1.0, + style: BorderStyle::Solid, + text: [0.0, 0.0, 0.0], + font: FieldFont::Helvetica, + }), + ..FieldProps::default() + }; + set_props(&mut doc, "name", &props()).unwrap(); + set_props(&mut doc, "due", &props()).unwrap(); + set_value(&mut doc, "name", &FieldValue::Text("Ada".into())).unwrap(); + set_value(&mut doc, "due", &FieldValue::Text("10/14/2026".into())).unwrap(); + let name_ap = ap(&doc, &field(&fields(&doc), "name").widgets[0]); + let due_ap = ap(&doc, &field(&fields(&doc), "due").widgets[0]); + marked_content_borders_field(&name_ap, "text field"); + marked_content_borders_field(&due_ap, "date field"); + assert!(name_ap.contains("(Ada) Tj"), "{name_ap}"); + assert!(due_ap.contains("(10/14/2026) Tj"), "{due_ap}"); + assert!(!need_appearances(&doc), "drawable ASCII fields do not ask other viewers to redraw"); +} + #[test] fn existing_field_font_encoding_keeps_italian_punctuation_visible() { // ReportLab-style Helvetica: an indirect encoding dictionary with PDFDoc-like Differences.