From 7fa8e91e5f717d01d97656c5223989552bd41bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Co=C3=AAlho?= Date: Thu, 30 Nov 2023 23:14:49 +0000 Subject: [PATCH] Handle inline Interactive Forms --- lib/rubrik/document.rb | 19 ++++++++- test/rubrik/document_test.rb | 35 +++++++++++++++++ test/rubrik/sign_test.rb | 36 ++++++++++++++++++ .../inline_interactive_form.expected.pdf | Bin 0 -> 9426 bytes test/support/inline_interactive_form.pdf | Bin 0 -> 526 bytes .../unexpected_value_interactive_form.pdf | Bin 0 -> 489 bytes 6 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 test/support/inline_interactive_form.expected.pdf create mode 100644 test/support/inline_interactive_form.pdf create mode 100644 test/support/unexpected_value_interactive_form.pdf diff --git a/lib/rubrik/document.rb b/lib/rubrik/document.rb index c7ca32c..0698701 100644 --- a/lib/rubrik/document.rb +++ b/lib/rubrik/document.rb @@ -118,11 +118,22 @@ def fetch_or_create_interactive_form! root_ref = objects.trailer[:Root] root = T.let(objects.fetch(root_ref), T::Hash[Symbol, T.untyped]) - if root.key?(:AcroForm) + interactive_form_value = root[:AcroForm] + case interactive_form_value + when PDF::Reader::Reference form_id = root[:AcroForm] modified_objects << {id: form_id, value: objects.fetch(form_id).dup} - else + when Hash + interactive_form_id = assign_new_object_id! + + modified_objects << {id: interactive_form_id, value: interactive_form_value.dup} + + new_root = root.dup + new_root[:AcroForm] = interactive_form_id + + modified_objects << {id: root_ref, value: new_root} + when NilClass interactive_form_id = assign_new_object_id! modified_objects << {id: interactive_form_id, value: {Fields: []}} @@ -131,6 +142,10 @@ def fetch_or_create_interactive_form! updated_root[:AcroForm] = interactive_form_id modified_objects << {id: root_ref, value: updated_root} + else + raise Error.new( + "Expected dictionary, reference or nil but got " \ + "#{interactive_form_value.class} on AcroForm entry.") end interactive_form[:SigFlags] = 3 # dont modify, append only diff --git a/test/rubrik/document_test.rb b/test/rubrik/document_test.rb index 6bdcb4b..e6534e6 100644 --- a/test/rubrik/document_test.rb +++ b/test/rubrik/document_test.rb @@ -48,6 +48,41 @@ def test_initialize_document_with_interactive_form input&.close end + def test_initialize_document_with_unexpected_interactive_form_input + # Arrange + input = File.open(SupportPDF["unexpected_value_interactive_form"], "rb") + + # Act + Assert + assert_raises("Expected dictionary, reference or nil but got Array on AcroForm entry.") do + Document.new(input) + end + ensure + input&.close + end + + def test_initialize_document_with_inline_interactive_form + # Arrange + input = File.open(SupportPDF["inline_interactive_form"], "rb") + + # Act + document = Document.new(input) + + # Assert + assert_equal(input, document.send(:io)) + assert_equal(5, document.last_object_id) + assert_kind_of(PDF::Reader::ObjectHash, document.objects) + + root_ref = PDF::Reader::Reference.new(1, 0) + assert_pattern do + document.modified_objects => [ + {id: PDF::Reader::Reference, value: {Fields: [], SigFlags: 3, NeedAppearances: true}}, + {id: ^root_ref, value: Hash} + ] + end + ensure + input&.close + end + def test_add_signature_field # Arrange input = File.open(SupportPDF["with_interactive_form"], "rb") diff --git a/test/rubrik/sign_test.rb b/test/rubrik/sign_test.rb index c6217be..cf2027f 100644 --- a/test/rubrik/sign_test.rb +++ b/test/rubrik/sign_test.rb @@ -115,5 +115,41 @@ def test_document_with_indirect_annots output_pdf&.close File.delete(output_pdf.path) if output_pdf end + + def test_document_with_inline_interactive_form + # Arrange + input_pdf = File.open(SupportPDF["inline_interactive_form"], "rb") + output_pdf = StringIO.new + certificate_file = File.open("test/support/demo_cert.pem", "rb") + + private_key = OpenSSL::PKey::RSA.new(certificate_file, "") + certificate_file.rewind + certificate = OpenSSL::X509::Certificate.new(certificate_file) + + # Act + Sign.call(input_pdf, output_pdf, private_key:, certificate:) + + # Assert + expected_output = File.open(SupportPDF["inline_interactive_form.expected"], "rb") + + expected_output.readlines.zip(output_pdf.readlines).each do |(expected_line, actual_line)| + # We must erase the signature because it is timestampped + if actual_line&.match?("/Type /Sig") + actual_line.sub!(/<[a-f0-9]+>/, "") + expected_line.sub!(/<[a-f0-9]+>/, "") + # The signature field name is also random + elsif actual_line&.match?(/Signature-[a-f0-9]{4}/) + actual_line.sub!(/Signature-[a-f0-9]{4}/, "") + expected_line.sub!(/Signature-[a-f0-9]{4}/, "") + end + + assert_equal(expected_line, actual_line) + end + ensure + certificate_file&.close + output_pdf&.close + input_pdf&.close + expected_output&.close + end end end diff --git a/test/support/inline_interactive_form.expected.pdf b/test/support/inline_interactive_form.expected.pdf new file mode 100644 index 0000000000000000000000000000000000000000..df68028571db7df39058bb06d1a7ffd502404635 GIT binary patch literal 9426 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 literal 0 HcmV?d00001 diff --git a/test/support/unexpected_value_interactive_form.pdf b/test/support/unexpected_value_interactive_form.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d986f21b742b39bde1d11177d6af36de8138e393 GIT binary patch literal 489 zcmZWm%TB{E5WM><_L3ux?bvxpRV50910brxE$YD#Hw3jYa*R~?de*@ZP%Z6YXJ%&~ zPS%(88K(u|z`#DelEor@{CG2vUTClPwu5x7J2QenegoKa9{5}qh?BC=*clYY&UdEm^~xThVJKIXqEbRBInB$- zbMPo0aUg6mTZc4S-Wy5AcY{_3Is bG@u~eSc@l(yN_NwKTVX0k|fF1eNDaq!>xNX literal 0 HcmV?d00001