From 2b8b2f262a882c4bef6c2e497341646d3c437d32 Mon Sep 17 00:00:00 2001 From: Vadim Kononov Date: Thu, 16 Jan 2020 03:38:29 -0600 Subject: [PATCH] Add close method. Suppress RJB warnings. Improve error handling. --- .rubocop.yml | 8 +- README.md | 203 +++++++++++++++++++++--------------- example/run.rb | 5 +- lib/field.rb | 9 +- lib/fillable-pdf.rb | 40 +++++-- lib/fillable-pdf/version.rb | 2 +- lib/kernel.rb | 9 ++ test/pdf_test.rb | 9 ++ 8 files changed, 181 insertions(+), 104 deletions(-) create mode 100644 lib/kernel.rb diff --git a/.rubocop.yml b/.rubocop.yml index 2a9facc..8559348 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,14 +8,14 @@ AllCops: Layout/EmptyLineAfterGuardClause: Enabled: false -Layout/SpaceInsideHashLiteralBraces: - Enabled: false - -Metrics/LineLength: +Layout/LineLength: Exclude: - fillable-pdf.gemspec Max: 120 +Layout/SpaceInsideHashLiteralBraces: + Enabled: false + Naming/AccessorMethodName: Enabled: false diff --git a/README.md b/README.md index e64a7b5..9ce459d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # FillablePDF [![Gem Version](https://badge.fury.io/rb/fillable-pdf.svg)](https://rubygems.org/gems/fillable-pdf) @@ -38,97 +39,126 @@ First of all, you should open a fillable PDF file: pdf = FillablePDF.new 'input.pdf' ``` -An instance of `FillablePDF` has the following methods at its disposal: - -```ruby -fillable-pdf -# output example: true -pdf.any_fields? -``` - -```ruby -# get the total number of fillable form fields -# output example: 10 -pdf.num_fields -``` - -```ruby -# retrieve a single field value by field name -# output example: 'Richard' -pdf.field(:full_name) -``` - -```ruby -# retrieve a field type by field name -# numeric types should -# output example: 4 -pdf.field_type(:football) - -# list of all field types -Field::BUTTON -Field::CHOICE -Field::SIGNATURE -Field::TEXT -``` - -```ruby -# retrieve a hash of field name and values -# output example: {:last_name=>"Rahl", :first_name=>"Richard"} -pdf.fields -``` - -```ruby -# set the value of a single field by field name -# result: changes the value of 'first_name' to 'Richard' -pdf.set_field(:first_name, 'Richard') -``` - -```ruby -# set the values of multiple fields by field names -# result: changes the values of 'first_name' and 'last_name' -pdf.set_fields(first_name: 'Richard', last_name: 'Rahl') -``` - -```ruby -# rename field (i.e. change the name of the field) -# result: renames field name 'last_name' to 'surname' -# NOTE: this action does not take effect until the document is saved -pdf.rename_field(:last_name, :surname) -``` +> **Always remember to close your document once you're finished working with it in order to avoid memory leaks:** ```ruby -# remove field (i.e. delete field and its value) -# result: physically removes field 'last_name' from document -pdf.remove_field(:last_name) +pdf.close ``` -```ruby -# get an array of all field names in the document -# output example: [:first_name, :last_name] -pdf.names -``` - -```ruby -# get an array of all field values in the document -# output example: ["Rahl", "Richard"] -pdf.values -``` +## Instance Methods -Once the PDF is filled out you can either overwrite it or save it as another file: - -```ruby -pdf.save -pdf.save_as('output.pdf') -``` - -Or if you prefer to flatten the file (i.e. make it non-editable), you can instead use: - -```ruby -pdf.save(flatten: true) -pdf.save_as('output.pdf', flatten: true) -``` +An instance of `FillablePDF` has the following methods at its disposal: -**NOTE:** Saving the file automatically closes the input file, so you would need to reinitialize the `FillabePDF` class before making any more changes or saving another copy. +* `any_fields?` + *Determines whether the form has any fields.* + ```ruby + pdf.any_fields? + # output example: true + ``` + +* `num_fields` + *Returns the total number of fillable form fields.* + ```ruby + # output example: 10 + pdf.num_fields + ``` + +* `field` + *Retrieves the value of a field given its unique field name.* + ```ruby + pdf.field(:full_name) + # output example: 'Richard' + ``` + +* `field_type` + *Retrieves the numeric type of a field given its unique field name.* + ```ruby + pdf.field_type(:football) + # output example: 4 + + # list of all field types + Field::BUTTON + Field::CHOICE + Field::SIGNATURE + Field::TEXT + ``` + +* `fields` + *Retrieves a hash of all fields and their values.* + ```ruby + pdf.fields + # output example: {first_name: "Richard", last_name: "Rahl"} + ``` + +* `set_field` + *Sets the value of a field given its unique field name and value.* + ```ruby + pdf.set_field(:first_name, 'Richard') + # result: changes the value of 'first_name' to 'Richard' + ``` + +* `set_fields` + *Sets the values of multiple fields given a set of unique field names and values.* + ```ruby + pdf.set_fields(first_name: 'Richard', last_name: 'Rahl') + # result: changes the values of 'first_name' and 'last_name' + ``` + +* `rename_field` + *Renames a field given its unique field name and the new field name.* + ```ruby + pdf.rename_field(:last_name, :surname) + # result: renames field name 'last_name' to 'surname' + # NOTE: this action does not take effect until the document is saved + ``` + +* `remove_field` + *Removes a field from the document given its unique field name.* + ```ruby + pdf.remove_field(:last_name) + # result: physically removes field 'last_name' from document + ``` + +* `names` + *Returns a list of all field keys used in the document.* + ```ruby + pdf.names + # output example: [:first_name, :last_name] + ``` + +* `values` + *Returns a list of all field values used in the document.* + ```ruby + pdf.values + # output example: ["Rahl", "Richard"] + ``` + +* `save` + *Overwrites the previously opened PDF document and flattens it if requested.* + ```ruby + pdf.save + # result: document is saved without flatenning + pdf.save_as(flatten: true) + # result: document is saved with flatenning + ``` + +* `save_as` + *Saves the filled out PDF document in a given path and flattens it if requested.* + ```ruby + pdf.save_as('output.pdf') + # result: document is saved in a given path without flatenning + pdf.save_as('output.pdf', flatten: true) + # result: document is saved in a given path with flatenning + ``` + + **NOTE:** Saving the file automatically closes the input file, so you would need to reinitialize the `FillabePDF` class before making any more changes or saving another copy. + +* `close` + *Closes the PDF document discarding all unsaved changes.* + ```ruby + pdf.close + # result: document is closed + ``` ## Example @@ -199,6 +229,9 @@ pdf.save_as('output.pdf') # saving another copy of the filled out PDF in another file and making it non-editable pdf = FillablePDF.new('output.pdf') pdf.save_as 'output.flat.pdf', flatten: true + +# closing the document +pdf.close ``` The example above produces the following output and also generates the output file [output.pdf](example/output.pdf). diff --git a/example/run.rb b/example/run.rb index 694b442..cc0b88c 100644 --- a/example/run.rb +++ b/example/run.rb @@ -1,4 +1,4 @@ -require 'fillable-pdf' +require_relative '../lib/fillable-pdf' # opening a fillable PDF pdf = FillablePDF.new('input.pdf') @@ -62,3 +62,6 @@ # saving another copy of the filled out PDF in another file and making it non-editable pdf = FillablePDF.new('output.pdf') pdf.save_as 'output.flat.pdf', flatten: true + +# closing the document +pdf.close \ No newline at end of file diff --git a/lib/field.rb b/lib/field.rb index 2e1f636..a1108c3 100644 --- a/lib/field.rb +++ b/lib/field.rb @@ -1,10 +1,15 @@ require_relative 'fillable-pdf/itext' +require_relative 'kernel' class Field - PDF_NAME = Rjb.import('com.itextpdf.kernel.pdf.PdfName') + # PdfName has a constant "A" and a constant "a". Unfortunately, RJB does not differentiate + # between these constants and tries to create the same constant ("A") for both, which causes + # an annoying warning "already initialized constant Rjb::Com_itextpdf_kernel_pdf_PdfName::A". + # As long as RJB has not fixed this issue, this warning will remain suppressed. + suppress_warnings { PDF_NAME = Rjb.import('com.itextpdf.kernel.pdf.PdfName') } BUTTON = PDF_NAME.Btn.toString CHOICE = PDF_NAME.Ch.toString SIGNATURE = PDF_NAME.Sig.toString TEXT = PDF_NAME.Tx.toString -end +end \ No newline at end of file diff --git a/lib/fillable-pdf.rb b/lib/fillable-pdf.rb index f7e3784..07b28ad 100644 --- a/lib/fillable-pdf.rb +++ b/lib/fillable-pdf.rb @@ -19,12 +19,16 @@ class FillablePDF def initialize(file_path) raise IOError, "File at `#{file_path}' is not found" unless File.exist?(file_path) @file_path = file_path - @byte_stream = BYTE_STREAM.new - @pdf_reader = PDF_READER.new @file_path - @pdf_writer = PDF_WRITER.new @byte_stream - @pdf_doc = PDF_DOCUMENT.new @pdf_reader, @pdf_writer - @pdf_form = PDF_ACRO_FORM.getAcroForm(@pdf_doc, true) - @form_fields = @pdf_form.getFormFields + begin + @byte_stream = BYTE_STREAM.new + @pdf_reader = PDF_READER.new @file_path + @pdf_writer = PDF_WRITER.new @byte_stream + @pdf_doc = PDF_DOCUMENT.new @pdf_reader, @pdf_writer + @pdf_form = PDF_ACRO_FORM.getAcroForm(@pdf_doc, true) + @form_fields = @pdf_form.getFormFields + rescue StandardError => ex + raise "#{ex.message} (input file may be corrupt, incompatible, or may not have any forms)" + end end ## @@ -37,7 +41,7 @@ def any_fields? end ## - # Returns the total number of form fields. + # Returns the total number of fillable form fields. # # @return the number of fields # @@ -147,7 +151,7 @@ def values end ## - # Overwrites the previously opened PDF file and flattens it if requested. + # Overwrites the previously opened PDF document and flattens it if requested. # # @param [bool] flatten true if PDF should be flattened, false otherwise # @@ -158,13 +162,27 @@ def save(flatten: false) end ## - # Saves the filled out PDF file with a given file and flattens it if requested. + # Saves the filled out PDF document in a given path and flattens it if requested. # # @param [String] file_path the name of the PDF file or file path # @param [Hash] flatten: true if PDF should be flattened, false otherwise # def save_as(file_path, flatten: false) - File.open(file_path, 'wb') { |f| f.write(finalize(flatten: flatten)) && f.close } + if @file_path == file_path + save(flatten: flatten) + else + File.open(file_path, 'wb') { |f| f.write(finalize(flatten: flatten)) && f.close } + end + end + + ## + # Closes the PDF document discarding all unsaved changes. + # + # @return [Boolean] true if document is closed, false otherwise + # + def close + @pdf_doc.close + @pdf_doc.isClosed end private @@ -176,7 +194,7 @@ def save_as(file_path, flatten: false) # def finalize(flatten: false) @pdf_form.flattenFields if flatten - @pdf_doc.close + close @byte_stream.toByteArray end diff --git a/lib/fillable-pdf/version.rb b/lib/fillable-pdf/version.rb index 30efa85..c93a7d7 100644 --- a/lib/fillable-pdf/version.rb +++ b/lib/fillable-pdf/version.rb @@ -1,3 +1,3 @@ class FillablePDF - VERSION = '0.7.2' + VERSION = '0.8.0' end diff --git a/lib/kernel.rb b/lib/kernel.rb new file mode 100644 index 0000000..59ef939 --- /dev/null +++ b/lib/kernel.rb @@ -0,0 +1,9 @@ +module Kernel + def suppress_warnings + original_verbosity = $VERBOSE + $VERBOSE = nil + result = yield + $VERBOSE = original_verbosity + result + end +end \ No newline at end of file diff --git a/test/pdf_test.rb b/test/pdf_test.rb index 394ba9b..dc6d73e 100644 --- a/test/pdf_test.rb +++ b/test/pdf_test.rb @@ -48,6 +48,11 @@ def test_that_a_field_value_can_be_modified assert_equal 'Richard', @pdf.field(:first_name) end + def test_that_an_asian_font_works + @pdf.set_field(:first_name, '理查德') + assert_equal '理查德', @pdf.field(:first_name) + end + def test_that_multiple_field_values_can_be_modified @pdf.set_fields(first_name: 'Richard', last_name: 'Rahl') assert_equal 'Richard', @pdf.field(:first_name) @@ -88,4 +93,8 @@ def test_that_a_file_can_be_saved @pdf.save refute_nil FillablePDF.new(@tmp) end + + def test_that_a_file_can_be_closed + assert @pdf.close + end end