//! Phase 3 exit criterion, against a real parsed PDF rather than synthetic //! dictionaries: parse an AcroForm file, modify a text field through //! `DocumentFormEditor`, and verify `/V` changed and the dirty flag is set. //! //! The fixture is a hand-written PDF checked in beside this file. It has two //! pages so that widget page resolution is genuinely exercised: everything //! interactive lives on page index 1, so any code that silently defaults to //! page 0 fails these tests. use nigig_pdf_document::appearance::{generate_widget_appearance, regenerate_dirty}; use nigig_pdf_document::form::{apply_value_to_dict, DocumentFormEditor, FieldValue, FormError}; use nigig_pdf_document::{AnnotationAction, FieldType, PdfDocument}; const FIXTURE: &[u8] = include_bytes!("acroform.pdf"); #[test] fn parses_the_page_tree_of_a_real_file() { let mut doc = PdfDocument::parse(FIXTURE).expect("fixture should parse"); assert_eq!(doc.page_count(), 2); assert!(!doc.page(0).expect("page 0").content_data.is_empty()); assert!(!doc.page(1).expect("page 1").content_data.is_empty()); } #[test] fn parses_the_acroform_with_inherited_keys_and_qualified_names() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let form = doc .acroform() .expect("acroform lookup") .expect("fixture has an AcroForm"); // "name" inherits /FT and /DA from its structural parent "personal", and // its fully qualified name is built from both levels. let name = form .find_field("personal.name") .expect("qualified name should resolve"); assert_eq!(name.field_type, FieldType::Text); assert_eq!(name.value, FieldValue::Text("John".into())); assert_eq!( name.default_appearance.as_deref(), Some("/Helv 10 Tf 0 g"), "/DA must be inherited from the parent field" ); assert_eq!(name.max_len, Some(20)); let agree = form.find_field("agree").expect("checkbox"); assert_eq!(agree.field_type, FieldType::Checkbox); assert!(!agree.is_checked()); } #[test] fn widget_pages_resolve_to_the_real_page_and_not_page_zero() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let form = doc.acroform().expect("acroform").expect("present"); for field in form.fields() { for widget in &field.widgets { assert_eq!( widget.page_index, Some(1), "field {:?} is on page 1 in the fixture", field.full_name ); } } assert_eq!(form.widgets_on_page(1).len(), 2); assert!( form.widgets_on_page(0).is_empty(), "nothing in this fixture is on page 0" ); } /// The Phase 3 exit criterion. #[test] fn editing_a_field_changes_v_and_sets_the_dirty_flag() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let mut form = doc.acroform().expect("acroform").expect("present"); let field_ref = form.find_field("personal.name").expect("field").obj_ref; let original = form.field(field_ref).expect("field").value.clone(); assert_eq!(original, FieldValue::Text("John".into())); let mut editor = DocumentFormEditor::new(&mut form); editor .set_text(field_ref, "Jane") .expect("edit should apply"); assert_eq!( editor.invalidated_pages(), &[1], "editing must invalidate the page the widget is on" ); let edited = form.field(field_ref).expect("field"); assert_eq!(edited.value, FieldValue::Text("Jane".into())); assert!(edited.dirty, "an applied edit must mark the field dirty"); assert_eq!(form.dirty_fields().count(), 1); } #[test] fn the_edit_survives_serialization_back_into_the_field_dictionary() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let mut form = doc.acroform().expect("acroform").expect("present"); let field_ref = form.find_field("personal.name").expect("field").obj_ref; DocumentFormEditor::new(&mut form) .set_text(field_ref, "Jane") .expect("edit"); let field = form.field(field_ref).expect("field"); let updated = apply_value_to_dict(field, &field.raw_dict_snapshot()); assert_eq!( updated.get_str("V"), Some(&b"Jane"[..]), "/V must carry the new value" ); } #[test] fn max_len_from_the_file_is_enforced() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let mut form = doc.acroform().expect("acroform").expect("present"); let field_ref = form.find_field("personal.name").expect("field").obj_ref; let mut editor = DocumentFormEditor::new(&mut form); let too_long = "x".repeat(21); let err = editor .set_text(field_ref, &too_long) .expect_err("21 characters exceeds the fixture's /MaxLen of 20"); assert!(matches!(err, FormError::TooLong { max_len: 20, .. })); // The refused edit must have changed nothing. assert_eq!( form.field(field_ref).expect("field").value, FieldValue::Text("John".into()) ); assert!(!form.field(field_ref).expect("field").dirty); } #[test] fn checking_the_box_uses_the_on_state_declared_in_the_file() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let mut form = doc.acroform().expect("acroform").expect("present"); let field_ref = form.find_field("agree").expect("checkbox").obj_ref; DocumentFormEditor::new(&mut form) .set_check(field_ref, true) .expect("check"); let field = form.field(field_ref).expect("field"); assert_eq!( field.value, FieldValue::State("On".into()), "the fixture's appearance dictionary names the on state /On" ); assert!(field.is_checked()); } #[test] fn appearance_regeneration_reflects_the_edited_value() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let mut form = doc.acroform().expect("acroform").expect("present"); let field_ref = form.find_field("personal.name").expect("field").obj_ref; DocumentFormEditor::new(&mut form) .set_text(field_ref, "Jane") .expect("edit"); let (generated, errors) = regenerate_dirty(&form); assert!(errors.is_empty(), "unexpected errors: {errors:?}"); assert_eq!(generated.len(), 1); let content = generated[0].1.content_str(); assert!(content.contains("(Jane) Tj"), "got: {content}"); assert!(!content.contains("(John)"), "stale value must not survive"); } #[test] fn a_regenerated_appearance_is_a_wellformed_form_xobject() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); let form = doc.acroform().expect("acroform").expect("present"); let field = form.find_field("personal.name").expect("field"); let ap = generate_widget_appearance(field, &field.widgets[0], &form).expect("appearance"); let xobj = ap.to_form_xobject(); assert_eq!(xobj.dict.get_name("Subtype"), Some("Form")); assert_eq!(xobj.dict.get_int("Length"), Some(xobj.data.len() as i64)); // The widget is 200x20 in the fixture. assert_eq!(ap.bbox, [0.0, 0.0, 200.0, 20.0]); } #[test] fn annotations_report_their_real_page_and_typed_action() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); assert!( doc.page_annotations(0).expect("page 0").is_empty(), "page 0 has no annotations in this fixture" ); let annots = doc.page_annotations(1).expect("page 1 annotations"); assert_eq!(annots.len(), 3, "two widgets and one link"); for annot in &annots { assert_eq!( annot.page_index, Some(1), "every annotation must know its real page" ); } let link = annots .iter() .find(|a| a.action().is_some()) .expect("fixture has a link with a URI action"); assert_eq!( link.action(), Some(AnnotationAction::OpenUri("https://example.org/".into())), "the document reports the intent; it never opens the URL itself" ); assert!(link.contains_point(150.0, 510.0)); assert!(!link.contains_point(50.0, 510.0)); } #[test] fn annotations_for_a_missing_page_are_an_error() { let mut doc = PdfDocument::parse(FIXTURE).expect("parse"); assert!(doc.page_annotations(99).is_err()); } #[test] fn page_index_of_maps_references_back_and_rejects_strangers() { let doc = PdfDocument::parse(FIXTURE).expect("parse"); let page1 = doc.page_object_ref(1).expect("page 1 ref"); assert_eq!(doc.page_index_of(page1), Some(1)); let stranger = nigig_pdf_cos::ObjRef { num: 9999, gen: 0 }; assert_eq!( doc.page_index_of(stranger), None, "an unknown reference must be unknown, not page 0" ); }