M6: a field whose /Kids loop back or name a root field keeps its other kids (form.field-list) (#6446)

This commit is contained in:
mtlcl410 authored and GitHub committed 2026-11-09 08:50:11 +08:00
1 parent 608ff62dae
commit e83c200598
5 files changed
+280 -24

No files matched your search

+55 -11
View File
@@ -338,13 +338,19 @@ pub(crate) fn enumerate(doc: &Document) -> (Vec<Field>, usize) {
let af = acroform(doc);
let mut page_of = std::collections::HashMap::new();
let mut annot_index = std::collections::HashMap::new();
let mut by_parent: std::collections::HashMap<ObjRef, Vec<ObjRef>> = std::collections::HashMap::new();
let pages = page_refs(doc);
for (i, p) in pages.iter().enumerate() {
if let Some(a) = doc.get(*p).as_dict().and_then(|d| d.get(b"Annots").cloned()) {
for (k, e) in doc.resolve(&a).as_array().into_iter().flatten().enumerate() {
if let Some(r) = e.as_ref() {
page_of.entry(r).or_insert(i);
annot_index.entry(r).or_insert(k);
if let Some(r) = e.as_ref()
&& let std::collections::hash_map::Entry::Vacant(slot) = page_of.entry(r)
{
slot.insert(i);
annot_index.insert(r, k);
if let Some(parent) = widget_parent(doc, r) {
by_parent.entry(parent).or_default().push(r);
}
}
}
}
@@ -358,18 +364,24 @@ pub(crate) fn enumerate(doc: &Document) -> (Vec<Field>, usize) {
None => Inherited::default(),
};
let mut out = Vec::new();
let listed_roots: Vec<ObjRef> = af
.as_ref()
.and_then(|af| af.get(b"Fields").map(|o| doc.resolve(o)).and_then(|o| o.as_array().cloned()))
.unwrap_or_default()
.iter()
.filter_map(Object::as_ref)
.collect();
let mut seen = Seen {
fields: std::collections::HashSet::new(),
widgets: std::collections::HashSet::new(),
roots: listed_roots.iter().copied().collect(),
path: Vec::new(),
by_parent,
scripts: script_budget(),
values: DecodeBudget::new(MAX_VALUE_BYTES, 0),
};
if let Some(af) = &af {
for f in af.get(b"Fields").map(|o| doc.resolve(o)).and_then(|o| o.as_array().cloned()).unwrap_or_default() {
if let Some(r) = f.as_ref() {
walk(doc, r, &base, &page_of, &mut seen, &mut out, 0);
}
}
for r in listed_roots {
walk(doc, r, &base, &page_of, &mut seen, &mut out, 0);
}
let listed = out.len();
adopt_page_fields(doc, &pages, &base, &page_of, &mut seen, &mut out);
@@ -440,10 +452,36 @@ struct Seen {
fields: std::collections::HashSet<ObjRef>,
/// A widget belongs to the first field that lists it, however often it is listed.
widgets: std::collections::HashSet<ObjRef>,
/// The fields `/Fields` lists: root fields, with no ancestors (ISO 32000-2 §12.7.3, Table 224).
roots: std::collections::HashSet<ObjRef>,
/// The fields being walked, outermost first, whose `/Kids` are being read.
path: Vec<ObjRef>,
/// The page widgets (no `/T`) that name each field as their `/Parent`, in page-annotation order.
by_parent: std::collections::HashMap<ObjRef, Vec<ObjRef>>,
scripts: DecodeBudget,
values: DecodeBudget,
}
/// The field a page widget `r` names as its `/Parent`, when `r` is a widget and not a field of its
/// own (no `/T`).
fn widget_parent(doc: &Document, r: ObjRef) -> Option<ObjRef> {
let obj = doc.get(r);
let d = obj.as_dict()?;
if d.name(b"Subtype") != Some(b"Widget") || d.contains(b"T") {
return None;
}
d.reference(b"Parent")
}
/// Whether `k`, listed in the `/Kids` of field `r`, is not a child of `r` after all: `r` itself or
/// a field `r` descends from (a loop), or a root field (one `/Fields` lists) that doesn't name `r`
/// as its `/Parent`. Root fields have no ancestors (ISO 32000-2 §12.7.3, Table 224), and a
/// child names its parent (§12.7.4.1, Table 226): only the `/Kids` entry says otherwise, so the
/// field is read as the root it is, and `r` keeps its other kids.
fn not_a_child(doc: &Document, seen: &Seen, r: ObjRef, k: ObjRef) -> bool {
k == r || seen.path.contains(&k) || (seen.roots.contains(&k) && doc.get(k).as_dict().and_then(|d| d.reference(b"Parent")) != Some(r))
}
/// Read the field at `r` and the fields under it into `out`, with what they inherit from
/// `parent`: kids with a name are fields of their own, kids without are this field's widgets.
fn walk(
@@ -490,11 +528,14 @@ fn walk(
let kids: Vec<ObjRef> =
d.get(b"Kids").map(|k| doc.resolve(k)).and_then(|k| k.as_array().cloned()).unwrap_or_default().iter().filter_map(|k| k.as_ref()).collect();
// Kids with /T are fields; kids without are this field's widgets.
let field_kids: Vec<ObjRef> = kids.iter().copied().filter(|k| doc.get(*k).as_dict().is_some_and(|kd| kd.contains(b"T"))).collect();
let (field_kids, kids): (Vec<ObjRef>, Vec<ObjRef>) = kids.into_iter().partition(|k| doc.get(*k).as_dict().is_some_and(|kd| kd.contains(b"T")));
let field_kids: Vec<ObjRef> = field_kids.into_iter().filter(|k| !not_a_child(doc, seen, r, *k)).collect();
if !field_kids.is_empty() {
seen.path.push(r);
for k in field_kids {
walk(doc, k, &inh, page_of, seen, out, depth + 1);
}
seen.path.pop();
return;
}
// A field of no kind this reads is skipped before it claims widgets or scripts, which a later
@@ -510,7 +551,10 @@ fn walk(
Some(b"Sig") => FieldKind::Signature,
_ => return,
};
let widget_refs: Vec<ObjRef> = if d.contains(b"Rect") { vec![r] } else { kids };
// A child names its parent (ISO 32000-2 §12.7.4.1, Table 226): a page widget naming this field
// is one of its widgets even when its `/Kids` leave it out, as MuPDF and PDFium read it.
let widget_refs: Vec<ObjRef> =
if d.contains(b"Rect") { vec![r] } else { kids.into_iter().chain(seen.by_parent.get(&r).into_iter().flatten().copied()).collect() };
let widget_refs: Vec<ObjRef> = widget_refs.into_iter().filter(|w| seen.widgets.insert(*w)).collect();
let actions = actions_of(doc, d, &widget_refs, &mut seen.scripts);
let button = (inh.ft.as_deref() == Some(b"Btn") && inh.ff.unwrap_or(0) & flags::PUSH_BUTTON != 0)
+79
View File
@@ -305,6 +305,85 @@ fn field_flags_reject_values_outside_the_unsigned_32_bit_domain() {
}
}
/// A `/Kids` entry that loops back (the field itself, or a field it descends from), or names a root
/// field that doesn't name this one as its `/Parent`, is not a child (ISO 32000-2 §12.7.3,
/// Table 224; §12.7.4.1, Table 226). The field keeps its other kids: here its radio buttons.
#[test]
fn kids_that_loop_back_or_are_roots_are_not_children() {
let objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), // 1
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), // 2
"<< /Type /Page /Parent 2 0 R /Annots [6 0 R 9 0 R 12 0 R 15 0 R] >>".into(), // 3
"<< /Fields [5 0 R 7 0 R 8 0 R 10 0 R] /DA (/Helv 0 Tf 0 g) >>".into(), // 4
"<< /FT /Btn /Ff 49152 /T (size) /V /Off /Kids [5 0 R 6 0 R] >>".into(), // 5 lists itself
"<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [50 700 65 715] /P 3 0 R /AP << /N << /S 11 0 R /Off 11 0 R >> >> >>".into(), // 6
"<< /FT /Btn /Ff 49152 /T (left) /V /Off /Kids [8 0 R 9 0 R] >>".into(), // 7 lists the root 8
"<< /FT /Btn /Ff 49152 /T (right) /V /Off /Kids [7 0 R 12 0 R] >>".into(), // 8 lists the root 7
"<< /Type /Annot /Subtype /Widget /Parent 7 0 R /Rect [50 650 65 665] /P 3 0 R /AP << /N << /A 11 0 R /Off 11 0 R >> >> >>".into(), // 9
"<< /T (group) /Kids [13 0 R] >>".into(), // 10
"<< /Length 0 >>
stream
endstream"
.into(), // 11
"<< /Type /Annot /Subtype /Widget /Parent 8 0 R /Rect [50 600 65 615] /P 3 0 R /AP << /N << /B 11 0 R /Off 11 0 R >> >> >>".into(), // 12
"<< /FT /Tx /T (inner) /Parent 10 0 R /Kids [14 0 R] >>".into(), // 13
"<< /FT /Tx /T (deep) /Parent 13 0 R /Kids [13 0 R 15 0 R] >>".into(), // 14 lists its parent
"<< /Type /Annot /Subtype /Widget /Parent 14 0 R /Rect [50 550 250 570] /P 3 0 R >>".into(), // 15
];
let doc = document(&objs);
let all = fields(&doc);
let listed: Vec<(&str, FieldKind, Vec<ObjRef>)> =
all.iter().map(|f| (f.name.as_str(), f.kind, f.widgets.iter().map(|w| w.obj).collect())).collect();
let r = |num| ObjRef::new(num, 0);
assert_eq!(
listed,
[
("size", FieldKind::Radio, vec![r(6)]),
("left", FieldKind::Radio, vec![r(9)]),
("right", FieldKind::Radio, vec![r(12)]),
("group.inner.deep", FieldKind::Text, vec![r(15)]),
]
);
}
/// A page widget whose `/Parent` names a field is one of that field's widgets even when the
/// field's `/Kids` leave it out: a child names its parent (ISO 32000-2 §12.7.4.1, Table 226). Here
/// `radio` lists itself and one of its two widgets, and `pair` and `other` list each other (roots,
/// so not children) and none of their widgets. Each field gets every widget naming it, once.
#[test]
fn widgets_naming_a_field_that_does_not_list_them_join_it() {
let w = |parent: u32, state: &str, y: u32| {
format!(
"<< /Type /Annot /Subtype /Widget /Parent {parent} 0 R /Rect [50 {y} 65 {}] /P 3 0 R /AP << /N << /{state} 10 0 R /Off 10 0 R >> >> >>",
y + 15
)
};
let objs: Vec<String> = vec![
"<< /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >>".into(), // 1
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), // 2
"<< /Type /Page /Parent 2 0 R /Annots [4 0 R 7 0 R 11 0 R 12 0 R 13 0 R] >>".into(), // 3
w(5, "A", 700), // 4: not in 5's /Kids
"<< /FT /Btn /Ff 49152 /T (radio) /V /Off /Kids [5 0 R 7 0 R] >>".into(), // 5
"<< /Fields [5 0 R 8 0 R 9 0 R] /DA (/Helv 0 Tf 0 g) >>".into(), // 6
w(5, "B", 650), // 7: in 5's /Kids
"<< /FT /Btn /Ff 49152 /T (pair) /V /Off /Kids [9 0 R 11 0 R] >>".into(), // 8 lists the root 9
"<< /FT /Btn /Ff 49152 /T (other) /V /Off /Kids [8 0 R] >>".into(), // 9 lists the root 8
"<< /Length 0 >>\nstream\n\nendstream".into(), // 10
w(8, "C", 600), // 11: in 8's /Kids
w(8, "D", 550), // 12: not in 8's /Kids
w(9, "E", 500), // 13: not in 9's /Kids
];
let doc = document(&objs);
let (all, adopted) = enumerate(&doc);
let listed: Vec<(&str, Vec<ObjRef>)> = all.iter().map(|f| (f.name.as_str(), f.widgets.iter().map(|w| w.obj).collect())).collect();
let r = |num| ObjRef::new(num, 0);
assert_eq!(listed, [("radio", vec![r(7), r(4)]), ("pair", vec![r(11), r(12)]), ("other", vec![r(13)])]);
assert_eq!(adopted, 0, "no widget is read as a field of its own");
let states: Vec<_> = field(&all, "radio").widgets.iter().filter_map(|w| w.on_state.as_deref()).collect();
assert_eq!(states, ["B", "A"]);
}
/// Some writers leave every field out of `/Fields` and put the widgets only in the page
/// annotations: the tree walk alone finds nothing, and Acrobat and the browsers fill them anyway.
/// The pages' widgets are adopted as fields, after the listed tree fields.
+75 -7
View File
@@ -370,14 +370,14 @@ impl<'a> Inspector<'a> {
o: &'a Object,
parent: Option<(&str, Option<String>, Option<i64>)>,
out: &mut Vec<Field>,
seen: &mut HashSet<ObjectId>,
walk: &mut FieldWalk,
depth: u32,
) {
if depth > 32 {
return;
}
if let Object::Reference(id) = o
&& !seen.insert(*id)
&& !walk.seen.insert(*id)
{
return;
}
@@ -396,10 +396,16 @@ impl<'a> Inspector<'a> {
Ok(Object::Array(k)) => k.as_slice(),
_ => &[],
};
let field_kids: Vec<_> = kids.iter().filter(|k| self.dict(k).is_some_and(|kd| kd.get(b"T").is_ok())).collect();
let (field_kids, widget_kids): (Vec<&Object>, Vec<&Object>) = kids.iter().partition(|k| self.dict(k).is_some_and(|kd| kd.get(b"T").is_ok()));
let field_kids: Vec<_> = field_kids.into_iter().filter(|k| !self.not_a_child(walk, o, k)).collect();
if !field_kids.is_empty() {
let id = if let Object::Reference(id) = o { Some(*id) } else { None };
walk.path.extend(id);
for k in field_kids {
self.field(k, Some((&full, ft.clone(), ff)), out, seen, depth + 1);
self.field(k, Some((&full, ft.clone(), ff)), out, walk, depth + 1);
}
if id.is_some() {
walk.path.pop();
}
return;
}
@@ -429,14 +435,67 @@ impl<'a> Inspector<'a> {
Ok(Object::Dictionary(_)) if kind == FieldKind::Signature => Some("signed".into()),
_ => None,
};
// The widget is either this dict (merged field/widget) or its first kid.
let widget = if d.get(b"Rect").is_ok() { Some(d) } else { kids.first().and_then(|k| self.dict(k)) };
let page = widget.and_then(|w| w.get(b"P").ok()).and_then(|p| self.page_of(p)).or_else(|| self.find_widget_page(o, kids));
// The widget is either this dict (merged field/widget) or its first kid. With no widget
// among its kids, it is the first page widget naming this field as its `/Parent`: a child
// names its parent (ISO 32000-2 §12.7.4.1, Table 226), as MuPDF and PDFium read it.
let merged = d.get(b"Rect").is_ok();
let named = if merged || !widget_kids.is_empty() { None } else { self.widget_naming(o) };
let widget = if merged {
Some(d)
} else {
widget_kids.first().and_then(|k| self.dict(k)).or_else(|| named.and_then(|id| self.doc.get_dictionary(id).ok()))
};
let page = widget
.and_then(|w| w.get(b"P").ok())
.and_then(|p| self.page_of(p))
.or_else(|| self.find_widget_page(o, kids))
.or_else(|| named.and_then(|id| self.annotation_pages.get_or_init(|| self.annotation_pages()).get(&id).copied()));
let has_actions = d.get(b"AA").is_ok() || widget.is_some_and(|w| w.get(b"AA").is_ok() || w.get(b"A").is_ok());
let rect = widget.and_then(|w| w.get(b"Rect").ok()).map(|r| rect4(self.resolve(r)));
out.push(Field { name: full, kind, value, page, tooltip: self.text(d, b"TU"), has_actions, rect });
}
/// Whether `k`, listed in the `/Kids` of field `o`, is not a child of `o` after all: `o` itself
/// or a field `o` descends from (a loop), or a root field (one `/Fields` lists) that doesn't
/// name `o` as its `/Parent`. Root fields have no ancestors (ISO 32000-2 §12.7.3, Table 224),
/// and a child names its parent (§12.7.4.1, Table 226).
fn not_a_child(&self, walk: &FieldWalk, o: &Object, k: &Object) -> bool {
let Object::Reference(k) = k else { return false };
let parent = self.dict(&Object::Reference(*k)).and_then(|d| match d.get(b"Parent") {
Ok(Object::Reference(p)) => Some(*p),
_ => None,
});
let o = if let Object::Reference(o) = o { Some(*o) } else { None };
o == Some(*k) || walk.path.contains(k) || (walk.roots.contains(k) && (o.is_none() || parent != o))
}
/// The first page widget that names field `o` as its `/Parent` and is not a field of its own
/// (no `/T`), in page-annotation order.
fn widget_naming(&self, o: &Object) -> Option<ObjectId> {
let Object::Reference(id) = o else { return None };
self.parent_widgets.get_or_init(|| self.parent_widgets()).get(id).copied()
}
/// The first page widget (no `/T`) naming each field as its `/Parent`, in page-annotation order.
fn parent_widgets(&self) -> HashMap<ObjectId, ObjectId> {
let mut first = HashMap::new();
for &pid in &self.page_ids {
let Some(annots) = self.doc.get_dictionary(pid).ok().and_then(|d| d.get(b"Annots").ok()) else { continue };
let Ok(annots) = self.resolve(annots).as_array() else { continue };
for a in annots {
let Object::Reference(id) = a else { continue };
let Ok(w) = self.doc.get_dictionary(*id) else { continue };
if self.name(w, b"Subtype").as_deref() != Some("Widget") || w.get(b"T").is_ok() {
continue;
}
if let Ok(Object::Reference(parent)) = w.get(b"Parent") {
first.entry(*parent).or_insert(*id);
}
}
}
first
}
/// The first page whose `/Annots` lists the field or one of its kids.
fn find_widget_page(&self, field: &Object, kids: &[Object]) -> Option<usize> {
let pages = self.annotation_pages.get_or_init(|| self.annotation_pages());
@@ -486,6 +545,15 @@ impl<'a> Inspector<'a> {
}
}
/// What one reading of the form's fields keeps: the fields read, the root fields `/Fields` lists,
/// and the fields whose `/Kids` are being read, outermost first.
#[derive(Default)]
pub(super) struct FieldWalk {
pub(super) seen: HashSet<ObjectId>,
pub(super) roots: HashSet<ObjectId>,
path: Vec<ObjectId>,
}
/// An annotation's `/F` as the renderer reads it: a real is truncated, and a value that isn't a
/// `u32` counts as no flags.
fn draw_flags(o: &Object) -> u32 {
+10 -6
View File
@@ -1,4 +1,4 @@
use super::annotations::MAX_LINK_ATTACHMENT_TEXT_BYTES;
use super::annotations::{FieldWalk, MAX_LINK_ATTACHMENT_TEXT_BYTES};
use super::model::{AttachmentSource, PageInfo, Xfa};
use super::navigation::text_string;
use super::{
@@ -144,13 +144,16 @@ pub(super) struct Inspector<'a> {
/// has no `/P`. Searching every page's `/Annots` for each such field was O(fields ×
/// annotations): a second on a form of 8,000 fields.
pub(super) annotation_pages: OnceCell<HashMap<ObjectId, usize>>,
/// The first page widget naming each field as its `/Parent`, built when a field has no widget
/// among its `/Kids`.
pub(super) parent_widgets: OnceCell<HashMap<ObjectId, ObjectId>>,
}
impl<'a> Inspector<'a> {
pub(super) fn new(doc: &'a dyn Structure) -> Self {
let page_ids = doc.page_ids();
let page_index = page_ids.iter().copied().enumerate().map(|(index, id)| (id, index)).collect();
let mut me = Self { doc, page_ids, page_index, named: HashMap::new(), annotation_pages: OnceCell::new() };
let mut me = Self { doc, page_ids, page_index, named: HashMap::new(), annotation_pages: OnceCell::new(), parent_widgets: OnceCell::new() };
let mut named = HashMap::new();
if let Some(tree) =
doc.catalog().ok().and_then(|c| c.get(b"Names").ok()).and_then(|o| me.dict(o)).and_then(|n| n.get(b"Dests").ok()).and_then(|o| me.dict(o))
@@ -210,12 +213,13 @@ impl<'a> Inspector<'a> {
let mut text_budget = MAX_LINK_ATTACHMENT_TEXT_BYTES;
self.annotations(info, &mut text_budget);
self.transitions(info);
let mut field_seen = HashSet::new();
let mut walk = FieldWalk::default();
if let Some(form) = catalog.get(b"AcroForm").ok().and_then(|o| self.dict(o))
&& let Ok(fields) = form.get(b"Fields").and_then(|o| self.resolve(o).as_array())
{
walk.roots = fields.iter().filter_map(|f| if let Object::Reference(id) = f { Some(*id) } else { None }).collect();
for f in fields {
self.field(f, None, &mut info.fields, &mut field_seen, 0);
self.field(f, None, &mut info.fields, &mut walk, 0);
}
}
// Widgets no `/Fields` entry reaches are fields too (Acrobat and the browsers fill them);
@@ -231,9 +235,9 @@ impl<'a> Inspector<'a> {
continue;
}
if let Object::Reference(id) = a
&& let Some(root) = self.unlisted_field(*id, &field_seen)
&& let Some(root) = self.unlisted_field(*id, &walk.seen)
{
self.field(&Object::Reference(root), None, &mut info.fields, &mut field_seen, 0);
self.field(&Object::Reference(root), None, &mut info.fields, &mut walk, 0);
}
}
}
+61
View File
@@ -1986,3 +1986,64 @@ fn inspection_keeps_no_private_page_data() {
};
assert_eq!(report(with), report(without), "what inspection reports does not change");
}
/// A `/Kids` entry that loops back, or names a root field that doesn't name this one as its
/// `/Parent`, is not a child (ISO 32000-2 §12.7.3, Table 224; §12.7.4.1, Table 226): the field
/// keeps its other kids and is listed with them.
#[test]
fn kids_that_loop_back_or_are_roots_are_not_children() {
let pdf = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [6 0 R 9 0 R 12 0 R 15 0 R] >> endobj
4 0 obj << /Fields [5 0 R 7 0 R 8 0 R 10 0 R] >> endobj
5 0 obj << /FT /Btn /Ff 49152 /T (size) /Kids [5 0 R 6 0 R] >> endobj
6 0 obj << /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [10 150 20 160] >> endobj
7 0 obj << /FT /Btn /Ff 49152 /T (left) /Kids [8 0 R 9 0 R] >> endobj
8 0 obj << /FT /Btn /Ff 49152 /T (right) /Kids [7 0 R 12 0 R] >> endobj
9 0 obj << /Type /Annot /Subtype /Widget /Parent 7 0 R /Rect [10 120 20 130] >> endobj
10 0 obj << /T (group) /Kids [13 0 R] >> endobj
12 0 obj << /Type /Annot /Subtype /Widget /Parent 8 0 R /Rect [10 90 20 100] >> endobj
13 0 obj << /FT /Tx /T (inner) /Parent 10 0 R /Kids [14 0 R] >> endobj
14 0 obj << /FT /Tx /T (deep) /Parent 13 0 R /Kids [13 0 R 15 0 R] >> endobj
15 0 obj << /Type /Annot /Subtype /Widget /Parent 14 0 R /Rect [10 60 90 80] >> endobj
trailer << /Root 1 0 R >>
%%EOF";
let info = inspect(Arc::new(pdf.to_vec()), None).expect("opens");
let listed: Vec<_> = info.fields.iter().map(|f| (f.name.as_str(), f.kind, f.page, f.rect.map(|r| r[1]))).collect();
assert_eq!(
listed,
[
("size", FieldKind::Radio, Some(0), Some(150.0)),
("left", FieldKind::Radio, Some(0), Some(120.0)),
("right", FieldKind::Radio, Some(0), Some(90.0)),
("group.inner.deep", FieldKind::Text, Some(0), Some(60.0)),
],
"{:?}",
info.fields
);
}
/// A field with no widget among its `/Kids` takes the page widget that names it as its
/// `/Parent`: a child names its parent (ISO 32000-2 §12.7.4.1, Table 226). Here `pair` and `other`
/// list each other (roots, so not children), and only a page widget says it belongs to `other`.
#[test]
fn a_field_takes_the_page_widget_naming_it_when_its_kids_list_none() {
let pdf = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm 6 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Annots [4 0 R 7 0 R 11 0 R 13 0 R] >> endobj
4 0 obj << /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [10 150 20 160] >> endobj
5 0 obj << /FT /Btn /Ff 49152 /T (radio) /Kids [5 0 R 7 0 R] >> endobj
6 0 obj << /Fields [5 0 R 8 0 R 9 0 R] >> endobj
7 0 obj << /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [10 120 20 130] >> endobj
8 0 obj << /FT /Btn /Ff 49152 /T (pair) /Kids [9 0 R 11 0 R] >> endobj
9 0 obj << /FT /Btn /Ff 49152 /T (other) /Kids [8 0 R] >> endobj
11 0 obj << /Type /Annot /Subtype /Widget /Parent 8 0 R /Rect [10 90 20 100] >> endobj
13 0 obj << /Type /Annot /Subtype /Widget /Parent 9 0 R /Rect [10 60 20 70] >> endobj
trailer << /Root 1 0 R >>
%%EOF";
let info = inspect(Arc::new(pdf.to_vec()), None).expect("opens");
let listed: Vec<_> = info.fields.iter().map(|f| (f.name.as_str(), f.page, f.rect.map(|r| r[1]))).collect();
assert_eq!(listed, [("radio", Some(0), Some(120.0)), ("pair", Some(0), Some(90.0)), ("other", Some(0), Some(60.0))], "{:?}", info.fields);
}