mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-06 18:52:36 +00:00
fix(forms): clearing a field's length limit leaves it with none, not its parent field's
/MaxLen is inheritable (ISO 32000-2 12.7.5.3). Clearing a kid's limit removed its own key, so the parent's limit came back. When an ancestor sets /MaxLen, write /MaxLen 0, which read.rs already reads as no limit and which overrides the inherited one (#3323). Follow-up to #4991. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
1 parent
3109bb2359
commit
412fbb397a
4 files changed
+53
-2
No files matched your search
@@ -9433,6 +9433,31 @@ fn changing_a_lists_options_through_tools_keeps_its_saved_selection_on_the_same_
|
||||
assert_eq!(saved_selection_indices(&dir.join("letters.pdf"), "letters"), Some(vec![0, 1]), "B and C are now items 0 and 1");
|
||||
}
|
||||
|
||||
/// Clearing a field's length limit through `form_set_props` (`"max_length": 0`) leaves it with
|
||||
/// none, even when its parent field has one it would otherwise inherit.
|
||||
#[test]
|
||||
fn clearing_a_kids_length_limit_through_tools_does_not_bring_back_its_parents() {
|
||||
let objs = [
|
||||
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>",
|
||||
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 400 300] >>",
|
||||
"<< /Type /Page /Parent 2 0 R /Annots [6 0 R] >>",
|
||||
"<< /Fields [5 0 R] /DA (/Helv 12 Tf 0 g) /DR << /Font << /Helv 7 0 R >> >> >>",
|
||||
"<< /T (address) /FT /Tx /MaxLen 3 /Kids [6 0 R] >>",
|
||||
"<< /Type /Annot /Subtype /Widget /T (city) /Parent 5 0 R /MaxLen 8 /Rect [10 250 150 268] /P 3 0 R >>",
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>",
|
||||
];
|
||||
let dir = workdir("kid-max-len");
|
||||
std::fs::write(dir.join("form.pdf"), pdf_of(&objs)).unwrap();
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "form.pdf" }))["doc"].as_u64().unwrap();
|
||||
ok(&mut a, "form_set_props", json!({ "doc": doc, "field": "address.city", "max_length": 0 }));
|
||||
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "address.city": "Absalom" } }));
|
||||
let fields = ok(&mut a, "form_fields", json!({ "doc": doc }));
|
||||
let city = fields["fields"].as_array().unwrap().iter().find(|f| f["name"] == "address.city").unwrap().clone();
|
||||
assert!(city.get("max_length").is_none(), "no limit, not the parent's 3: {city}");
|
||||
assert_eq!(city["value"], "Absalom", "the value is not cut to the parent's 3 characters");
|
||||
}
|
||||
|
||||
/// Clearing a field's default through `form_set_props` (`"default": ""`) leaves it with none,
|
||||
/// even when its parent field has one it would otherwise inherit.
|
||||
#[test]
|
||||
|
||||
@@ -189,6 +189,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
||||
let mut list_picked = None;
|
||||
let (sort_items, old_selection) = items_to_sort(doc, &f, props);
|
||||
let parent_default = ancestor_sets(doc, f.obj, b"DV");
|
||||
let parent_limit = ancestor_sets(doc, f.obj, b"MaxLen");
|
||||
doc.update_dict(f.obj, |d| {
|
||||
d.set(b"Ff".to_vec(), Object::Int(ff as i64));
|
||||
if let Some(t) = &props.tooltip {
|
||||
@@ -201,6 +202,8 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
||||
if let Some(m) = props.max_len {
|
||||
match m {
|
||||
Some(m) => d.set(b"MaxLen".to_vec(), Object::Int(m as i64)),
|
||||
// An absent key would inherit the parent's limit; zero is "no limit" (`read.rs`).
|
||||
None if parent_limit => d.set(b"MaxLen".to_vec(), Object::Int(0)),
|
||||
None => {
|
||||
d.remove(b"MaxLen");
|
||||
}
|
||||
|
||||
@@ -5271,6 +5271,29 @@ fn sorting_keeps_which_of_two_items_with_one_export_value_is_selected() {
|
||||
assert_eq!(selection_indices(&doc, &list), Some(vec![2]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_a_kids_length_limit_does_not_bring_back_its_parents() {
|
||||
let mut doc = fixture();
|
||||
let city = field(&fields(&doc), "address.city").clone();
|
||||
let parent = doc.get(city.obj).as_dict().unwrap().reference(b"Parent").unwrap();
|
||||
doc.update_dict(parent, |d| d.set(b"MaxLen".to_vec(), Object::Int(3))).unwrap();
|
||||
set_props(&mut doc, "address.city", &FieldProps { max_len: Some(Some(8)), ..FieldProps::default() }).unwrap();
|
||||
assert_eq!(field(&fields(&doc), "address.city").max_len, Some(8));
|
||||
set_props(&mut doc, "address.city", &FieldProps { max_len: Some(None), ..FieldProps::default() }).unwrap();
|
||||
let mut doc = reopen(&doc);
|
||||
let f = field(&fields(&doc), "address.city").clone();
|
||||
assert_eq!(f.max_len, None, "no limit, not the parent's 3");
|
||||
let own = doc.get(f.obj).as_dict().and_then(|d| d.get(b"MaxLen")).and_then(Object::as_int);
|
||||
assert_eq!(own, Some(0), "the kid holds an explicit unlimited of its own");
|
||||
set_value(&mut doc, "address.city", &FieldValue::Text("Absalom".into())).unwrap();
|
||||
assert_eq!(field(&fields(&reopen(&doc)), "address.city").value, ["Absalom"], "not cut to 3 characters");
|
||||
// Without a parent limit, clearing still removes the key.
|
||||
set_props(&mut doc, "name", &FieldProps { max_len: Some(Some(4)), ..FieldProps::default() }).unwrap();
|
||||
set_props(&mut doc, "name", &FieldProps { max_len: Some(None), ..FieldProps::default() }).unwrap();
|
||||
let name = field(&fields(&doc), "name").clone();
|
||||
assert!(doc.get(name.obj).as_dict().unwrap().get(b"MaxLen").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_a_kids_default_does_not_bring_back_its_parents() {
|
||||
let mut doc = fixture();
|
||||
|
||||
@@ -5003,8 +5003,8 @@ milestone = "M6"
|
||||
status = "shipped"
|
||||
commands = ["form.field.properties"]
|
||||
tools = ["form_set_props"]
|
||||
evidence = ["crates/forms/src/tests.rs::button_caption_preserves_other_widgets_and_shared_dictionaries", "crates/forms/src/tests.rs::a_button_caption_is_drawn_in_its_fields_font_size_and_color", "crates/ui-egui/tests/it/prepare.rs::button_label_is_editable_without_renaming_the_field", "crates/automation/tests/it/button_caption.rs::button_caption_can_be_changed_after_creation", "crates/forms/src/tests.rs::properties_rename_and_delete", "crates/automation/tests/it/automation.rs::preparing_a_form_through_tools", "crates/forms/src/tests.rs::options_tab_flags_alignment_and_defaults", "crates/ui-egui/tests/it/prepare.rs::options_tab_sets_flags_alignment_and_defaults", "crates/automation/tests/it/form_bulk.rs::bulk_options_apply_per_field_and_refuse_an_invalid_comb_atomically", "crates/forms/src/tests.rs::changing_a_lists_options_repoints_its_selected_indices", "crates/forms/src/tests.rs::shrinking_a_lists_options_scrolls_a_widget_with_its_own_top_row", "crates/forms/src/tests.rs::each_widget_with_its_own_top_row_scrolls_by_itself", "crates/forms/src/tests.rs::a_list_boxs_top_row_follows_a_font_size_changed_with_its_items", "crates/forms/src/tests.rs::sorting_a_combo_boxs_items_repoints_its_selected_index", "crates/automation/tests/it/automation.rs::changing_a_lists_options_through_tools_keeps_its_saved_selection_on_the_same_items", "crates/forms/src/tests.rs::clearing_a_kids_default_does_not_bring_back_its_parents", "crates/forms/src/tests.rs::clearing_a_buttons_default_under_a_parent_with_one_leaves_it_off", "crates/automation/tests/it/automation.rs::clearing_a_kids_default_through_tools_does_not_bring_back_its_parents"]
|
||||
notes = "push buttons: per-widget normal label independent of the field name, including clearing and save/reopen, drawn in the font, size and color of the button's /DA (the form's /DR font, else Helvetica; an automatic size fits the button) and centered by Helvetica's widths; icon-only layout retained, rollover/down labels and icon layout editing remain unavailable; text: alignment, default (clearing it leaves none, not a parent field's), multi-line, scroll, rich text flag, limit, password, file selection, spell check, comb; choices: items (the selected indices /I follow their items when the list is edited or sorted), default, sort, custom text, multiple selection, commit immediately; check boxes and radios: checked by default, radios in unison. Shared GUI offers applicable text/choice flags, alignment, text limits and check styles; choices/defaults remain individual GUI edits"
|
||||
evidence = ["crates/forms/src/tests.rs::button_caption_preserves_other_widgets_and_shared_dictionaries", "crates/forms/src/tests.rs::a_button_caption_is_drawn_in_its_fields_font_size_and_color", "crates/ui-egui/tests/it/prepare.rs::button_label_is_editable_without_renaming_the_field", "crates/automation/tests/it/button_caption.rs::button_caption_can_be_changed_after_creation", "crates/forms/src/tests.rs::properties_rename_and_delete", "crates/automation/tests/it/automation.rs::preparing_a_form_through_tools", "crates/forms/src/tests.rs::options_tab_flags_alignment_and_defaults", "crates/ui-egui/tests/it/prepare.rs::options_tab_sets_flags_alignment_and_defaults", "crates/automation/tests/it/form_bulk.rs::bulk_options_apply_per_field_and_refuse_an_invalid_comb_atomically", "crates/forms/src/tests.rs::changing_a_lists_options_repoints_its_selected_indices", "crates/forms/src/tests.rs::shrinking_a_lists_options_scrolls_a_widget_with_its_own_top_row", "crates/forms/src/tests.rs::each_widget_with_its_own_top_row_scrolls_by_itself", "crates/forms/src/tests.rs::a_list_boxs_top_row_follows_a_font_size_changed_with_its_items", "crates/forms/src/tests.rs::sorting_a_combo_boxs_items_repoints_its_selected_index", "crates/automation/tests/it/automation.rs::changing_a_lists_options_through_tools_keeps_its_saved_selection_on_the_same_items", "crates/forms/src/tests.rs::clearing_a_kids_default_does_not_bring_back_its_parents", "crates/forms/src/tests.rs::clearing_a_kids_length_limit_does_not_bring_back_its_parents", "crates/automation/tests/it/automation.rs::clearing_a_kids_length_limit_through_tools_does_not_bring_back_its_parents", "crates/forms/src/tests.rs::clearing_a_buttons_default_under_a_parent_with_one_leaves_it_off", "crates/automation/tests/it/automation.rs::clearing_a_kids_default_through_tools_does_not_bring_back_its_parents"]
|
||||
notes = "push buttons: per-widget normal label independent of the field name, including clearing and save/reopen, drawn in the font, size and color of the button's /DA (the form's /DR font, else Helvetica; an automatic size fits the button) and centered by Helvetica's widths; icon-only layout retained, rollover/down labels and icon layout editing remain unavailable; text: alignment, default (clearing it leaves none, not a parent field's), multi-line, scroll, rich text flag, limit (clearing it leaves none, not a parent field's), password, file selection, spell check, comb; choices: items (the selected indices /I follow their items when the list is edited or sorted), default, sort, custom text, multiple selection, commit immediately; check boxes and radios: checked by default, radios in unison. Shared GUI offers applicable text/choice flags, alignment, text limits and check styles; choices/defaults remain individual GUI edits"
|
||||
|
||||
[[feature]]
|
||||
id = "form.props-actions"
|
||||
|
||||
Reference in new issue
Block a user