mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
M6: a widget whose /Parent names a missing object is a field of its own (form.field-list) (#6445)
This commit is contained in:
1 parent
de7ed00a1a
commit
608ff62dae
5 files changed
+66
-6
No files matched your search
@@ -106,12 +106,19 @@ fn qualified_name(doc: &Document, r: ObjRef) -> Option<String> {
|
||||
if let Some(t) = d.get(b"T").and_then(|o| text_of(&doc.resolve(o))) {
|
||||
parts.push(t);
|
||||
}
|
||||
cur = d.get(b"Parent").and_then(|p| p.as_ref());
|
||||
cur = parent_of(doc, d);
|
||||
}
|
||||
parts.reverse();
|
||||
(!parts.is_empty()).then(|| parts.join("."))
|
||||
}
|
||||
|
||||
/// The field `d` names as its `/Parent`, when that is a dictionary. A reference to an object the
|
||||
/// file doesn't hold reads as `null`, and a `null` entry is the same as an absent one (ISO
|
||||
/// 32000-2 §7.3.9): the field is then a root, as if it named no parent.
|
||||
fn parent_of(doc: &Document, d: &Dict) -> Option<ObjRef> {
|
||||
d.reference(b"Parent").filter(|p| doc.get(*p).as_dict().is_some())
|
||||
}
|
||||
|
||||
/// JavaScript streams one reading of the form (its fields, or its document scripts) may decode
|
||||
/// in all, however many actions name the same stream.
|
||||
pub(crate) const MAX_SCRIPT_BYTES: usize = 64 << 20;
|
||||
@@ -413,7 +420,7 @@ fn unlisted_root(doc: &Document, r: ObjRef, seen: &std::collections::HashSet<Obj
|
||||
let mut top = r;
|
||||
let mut visited = std::collections::HashSet::from([r]);
|
||||
loop {
|
||||
let parent = doc.get(top).as_dict().and_then(|d| d.reference(b"Parent"));
|
||||
let parent = doc.get(top).as_dict().and_then(|d| parent_of(doc, d));
|
||||
let Some(p) = parent else {
|
||||
return if seen.contains(&top) { None } else { Some(top) };
|
||||
};
|
||||
|
||||
@@ -353,6 +353,37 @@ fn fields_listed_only_on_the_pages_are_adopted() {
|
||||
assert_eq!(adopted_page_fields(&doc), 4);
|
||||
}
|
||||
|
||||
/// A widget whose `/Parent` names an object the file doesn't hold, or one that isn't a dictionary,
|
||||
/// has no parent: the reference reads as `null`, the same as an absent entry (ISO
|
||||
/// 32000-2 §7.3.9). It is adopted as a field of its own, under its own name, and can be filled.
|
||||
#[test]
|
||||
fn a_widget_whose_parent_is_missing_is_a_field_of_its_own() {
|
||||
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 [5 0 R 7 0 R 8 0 R] >>".into(), // 3
|
||||
"<< /Fields [] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 6 0 R >> >> >>".into(), // 4
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (city) /V (Oslo) /Parent 40 0 R /Rect [50 700 250 720] /P 3 0 R >>".into(), // 5 parent not in the file
|
||||
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), // 6
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (zip) /Parent 9 0 R /Rect [50 660 250 680] /P 3 0 R >>".into(), // 7 parent is no dictionary
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 40 0 R /Rect [400 700 415 715] /P 3 0 R >>".into(), // 8 no /FT or /T: not a field
|
||||
"[1 2 3]".into(), // 9
|
||||
];
|
||||
let mut doc = document(&objs);
|
||||
assert_combined_matches_legacy(&doc, 2);
|
||||
let all = fields(&doc);
|
||||
let names: Vec<&str> = all.iter().map(|f| f.name.as_str()).collect();
|
||||
assert_eq!(names, ["city", "zip"]);
|
||||
let city = field(&all, "city");
|
||||
assert_eq!(
|
||||
(city.kind, city.value.as_slice(), city.widgets.len(), city.widgets[0].page),
|
||||
(FieldKind::Text, &["Oslo".to_string()][..], 1, Some(0))
|
||||
);
|
||||
set_value(&mut doc, "zip", &FieldValue::Text("0150".into())).unwrap();
|
||||
let doc = reopen(&doc);
|
||||
assert_eq!(field(&fields(&doc), "zip").value, ["0150"]);
|
||||
}
|
||||
|
||||
/// pdf-lib and other writers give radio groups and check boxes an `/Opt` array and name the on
|
||||
/// states by position (`/0`, `/1`): the export values select them, as in Acrobat.
|
||||
#[test]
|
||||
|
||||
@@ -458,7 +458,7 @@ impl<'a> Inspector<'a> {
|
||||
pages
|
||||
}
|
||||
|
||||
/// The topmost field above widget `id` (itself without a `/Parent`), or `None` when it or an
|
||||
/// The topmost field above widget `id` (itself without a `/Parent` that is a field), or `None` when it or an
|
||||
/// ancestor is already reachable from the `/Fields` tree, or a `/Parent` loop hides the top.
|
||||
pub(super) fn unlisted_field(&self, id: ObjectId, seen: &HashSet<ObjectId>) -> Option<ObjectId> {
|
||||
let mut top = id;
|
||||
@@ -466,8 +466,10 @@ impl<'a> Inspector<'a> {
|
||||
loop {
|
||||
let o = Object::Reference(top);
|
||||
let d = self.dict(&o)?;
|
||||
// A `/Parent` naming an object the file doesn't hold, or no dictionary, reads as
|
||||
// `null`, the same as no parent (ISO 32000-2 §7.3.9).
|
||||
let parent = match d.get(b"Parent") {
|
||||
Ok(Object::Reference(p)) => Some(*p),
|
||||
Ok(Object::Reference(p)) if self.dict(&Object::Reference(*p)).is_some() => Some(*p),
|
||||
_ => None,
|
||||
};
|
||||
let Some(p) = parent else {
|
||||
|
||||
@@ -786,6 +786,26 @@ fn fields_listed_only_on_the_pages_are_listed() {
|
||||
assert_eq!(listed, [("alpha", Some("one"), Some(0)), ("beta", None, Some(0))], "{:?}", info.fields);
|
||||
}
|
||||
|
||||
/// A widget whose `/Parent` names an object the file doesn't hold, or one that isn't a dictionary,
|
||||
/// is listed as a field of its own: that parent reads as `null`, the same
|
||||
/// as none (ISO 32000-2 §7.3.9).
|
||||
#[test]
|
||||
fn a_widget_whose_parent_is_missing_is_listed() {
|
||||
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 [5 0 R 6 0 R] >> endobj
|
||||
4 0 obj << /Fields [] >> endobj
|
||||
5 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (alpha) /V (one) /Parent 40 0 R /Rect [10 100 90 120] >> endobj
|
||||
6 0 obj << /Type /Annot /Subtype /Widget /FT /Tx /T (beta) /Parent 7 0 R /Rect [10 60 90 80] >> endobj
|
||||
7 0 obj [1 2 3] 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.value.as_deref(), f.page)).collect();
|
||||
assert_eq!(listed, [("alpha", Some("one"), Some(0)), ("beta", None, Some(0))], "{:?}", info.fields);
|
||||
}
|
||||
|
||||
/// A `Structure` that counts the objects read through it.
|
||||
struct CountingReads<'a> {
|
||||
doc: &'a Document,
|
||||
|
||||
@@ -4590,8 +4590,8 @@ milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = ["form.fields"]
|
||||
tools = ["doc_info"]
|
||||
evidence = ["crates/ui-egui/tests/it/view.rs::field_list_jumps_to_a_fields_page", "crates/render/src/inspect/tests.rs::fields_listed_only_on_the_pages_are_listed", "crates/ui-egui/tests/it/bulk_fields.rs::fields_panel_selects_multiple_fields_and_opens_their_shared_properties", "crates/ui-egui/tests/it/bulk_fields.rs::double_clicking_a_field_in_the_panel_opens_its_individual_properties"]
|
||||
notes = "list with jump-to-field; in Prepare a form, Shift/Ctrl/Command-click selects several fields and Properties edits their shared properties; double-click opens field properties; fields a file lists only as page widgets (empty /Fields) are adopted too"
|
||||
evidence = ["crates/ui-egui/tests/it/view.rs::field_list_jumps_to_a_fields_page", "crates/render/src/inspect/tests.rs::fields_listed_only_on_the_pages_are_listed", "crates/render/src/inspect/tests.rs::a_widget_whose_parent_is_missing_is_listed", "crates/forms/src/tests.rs::a_widget_whose_parent_is_missing_is_a_field_of_its_own", "crates/ui-egui/tests/it/bulk_fields.rs::fields_panel_selects_multiple_fields_and_opens_their_shared_properties", "crates/ui-egui/tests/it/bulk_fields.rs::double_clicking_a_field_in_the_panel_opens_its_individual_properties"]
|
||||
notes = "list with jump-to-field; in Prepare a form, Shift/Ctrl/Command-click selects several fields and Properties edits their shared properties; double-click opens field properties; fields a file lists only as page widgets (empty /Fields) are adopted too, and a widget whose /Parent names a missing object is a field of its own"
|
||||
|
||||
[[feature]]
|
||||
id = "form.highlight-fields"
|
||||
|
||||
Reference in new issue
Block a user