mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 01:28:38 +00:00
M6: form_fields lists a check box's kids by on state and reports the kid that is on
form_fields listed a check box's options by /Opt export, so kids sharing the on state Yes with exports x and y showed as two options, though filling either ticks both (ISO 32000-2 §12.7.5.2.3, EXAMPLE 2). It now lists one option per distinct on state, named by its export unless two states share one (Table 230 allows that), then by the state. `checked` came from /V. It now follows the kid whose /AS is its on state, which §12.7.5.2.3 makes decide where /AS and /V differ, so it matches what the page shows. Test: automation form_fields on shared-state, /AS-over-/V and shared-export fixtures. Each assertion fails with its fix reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0175FWj4xFhMhSH3UpMU8DSC
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
880ccd4c82
commit
d16cbfb96b
@@ -296,13 +296,22 @@ impl Automation {
|
||||
o.insert("options".into(), json!((0..f.widgets.len()).filter_map(|i| f.export_of(i)).collect::<Vec<_>>()));
|
||||
}
|
||||
// A check box whose kids have different on states (ISO 32000-2 §12.7.5.2.3):
|
||||
// which one is ticked, and the names form_fill takes to tick each.
|
||||
// which one is ticked, and the names form_fill takes to tick each. One option
|
||||
// per distinct on state: kids sharing a state tick together (EXAMPLE 2), so
|
||||
// their `/Opt` exports are not separate choices. Exports name the options
|
||||
// unless two states share one (Table 230 allows it); then the states do.
|
||||
FormFieldKind::CheckBox => {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let options: Vec<&str> = (0..f.widgets.len()).filter_map(|i| f.export_of(i)).filter(|e| seen.insert(*e)).collect();
|
||||
if options.len() > 1 {
|
||||
o.insert("options".into(), json!(options));
|
||||
o.insert("checked".into(), json!(f.value.first().map(|v| f.export_for_state(v))));
|
||||
let states: Vec<&str> = f.widgets.iter().filter_map(|w| w.on_state.as_deref()).filter(|s| seen.insert(*s)).collect();
|
||||
if states.len() > 1 {
|
||||
let mut exports = std::collections::HashSet::new();
|
||||
let by_export = states.iter().all(|s| exports.insert(f.export_for_state(s)));
|
||||
let name = |s: &str| if by_export { f.export_for_state(s).to_owned() } else { s.to_owned() };
|
||||
// The ticked kid is the one whose `/AS` is its on state: §12.7.5.2.3
|
||||
// makes `/AS` decide where it and `/V` differ.
|
||||
let on = (0..f.widgets.len()).find(|&i| f.widget_on(i)).and_then(|i| f.widgets.get(i)?.on_state.as_deref());
|
||||
o.insert("options".into(), json!(states.iter().map(|s| name(s)).collect::<Vec<_>>()));
|
||||
o.insert("checked".into(), json!(on.map(name)));
|
||||
}
|
||||
}
|
||||
FormFieldKind::Combo | FormFieldKind::List => {
|
||||
|
||||
@@ -2657,6 +2657,49 @@ fn form_fill_ticks_one_kid_of_a_check_box_by_its_on_state() {
|
||||
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "pair": false } }));
|
||||
assert_eq!(ok(&mut a, "form_fields", json!({ "doc": doc }))["fields"][0]["checked"], Value::Null);
|
||||
}
|
||||
|
||||
/// `form_fields` on a check box's kids: one option per distinct on state, named by its `/Opt`
|
||||
/// export unless two states share one, and `checked` follows the kid whose `/AS` is on, which
|
||||
/// ISO 32000-2 §12.7.5.2.3 makes decide where `/AS` and `/V` differ.
|
||||
#[test]
|
||||
fn form_fields_lists_a_check_box_by_on_state_and_reports_the_kid_that_is_on() {
|
||||
// Each kid is (on state, /AS).
|
||||
let listed = |name: &str, field: &str, (sa, asa): (&str, &str), (sb, asb): (&str, &str)| {
|
||||
let kid = |on: &str, state: &str, x: u32| {
|
||||
format!(
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 5 0 R /AS /{state} /Rect [{x} 250 {} 266] /P 3 0 R /AP << /N << /{on} 8 0 R /Off 8 0 R >> >> >>",
|
||||
x + 16
|
||||
)
|
||||
};
|
||||
let (wa, wb) = (kid(sa, asa, 10), kid(sb, asb, 40));
|
||||
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 7 0 R] >>",
|
||||
"<< /Fields [5 0 R] /DA (/Helv 0 Tf 0 g) >>",
|
||||
field,
|
||||
&wa,
|
||||
&wb,
|
||||
"<< /Length 0 >>\nstream\n\nendstream",
|
||||
];
|
||||
let dir = workdir(name);
|
||||
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();
|
||||
let r = ok(&mut a, "form_fields", json!({ "doc": doc }));
|
||||
(r["fields"][0]["options"].clone(), r["fields"][0]["checked"].clone())
|
||||
};
|
||||
// Kids sharing the on state Yes tick together, whatever their exports: not two options.
|
||||
let shared =
|
||||
listed("check-box-shared-state", "<< /FT /Btn /T (pair) /V /Off /Opt [(x) (y)] /Kids [6 0 R 7 0 R] >>", ("Yes", "Off"), ("Yes", "Off"));
|
||||
assert_eq!(shared, (Value::Null, Value::Null));
|
||||
// /V names B, but A's /AS is on: A is the one ticked.
|
||||
let differ = listed("check-box-as-wins", "<< /FT /Btn /T (pair) /V /B /Kids [6 0 R 7 0 R] >>", ("A", "A"), ("B", "Off"));
|
||||
assert_eq!(differ, (json!(["A", "B"]), json!("A")));
|
||||
// Two on states with one export: the states name the options.
|
||||
let same = listed("check-box-same-export", "<< /FT /Btn /T (pair) /V /B /Opt [(x) (x)] /Kids [6 0 R 7 0 R] >>", ("A", "Off"), ("B", "B"));
|
||||
assert_eq!(same, (json!(["A", "B"]), json!("B")));
|
||||
}
|
||||
/// A form whose fields are only page widgets (the `/Fields` list is empty) still lists, fills and
|
||||
/// resets through the tools, and the leniency shows up as a repair note.
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user