mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 06:12:19 +00:00
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:
1 parent
608ff62dae
commit
e83c200598
5 files changed
+280
-24
No files matched your search
+55
-11
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user