Skip to content

Commit

Permalink
Add close method. Suppress RJB warnings. Improve error handling.
Browse files Browse the repository at this point in the history
  • Loading branch information
vkononov committed Jan 16, 2020
1 parent 08cfacd commit 2b8b2f2
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 104 deletions.
8 changes: 4 additions & 4 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
203 changes: 118 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

# FillablePDF

[![Gem Version](https://badge.fury.io/rb/fillable-pdf.svg)](https://rubygems.org/gems/fillable-pdf)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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).
Expand Down
5 changes: 4 additions & 1 deletion example/run.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require 'fillable-pdf'
require_relative '../lib/fillable-pdf'

# opening a fillable PDF
pdf = FillablePDF.new('input.pdf')
Expand Down Expand Up @@ -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
9 changes: 7 additions & 2 deletions lib/field.rb
Original file line number Diff line number Diff line change
@@ -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
40 changes: 29 additions & 11 deletions lib/fillable-pdf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

##
Expand All @@ -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
#
Expand Down Expand Up @@ -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
#
Expand All @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion lib/fillable-pdf/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
class FillablePDF
VERSION = '0.7.2'
VERSION = '0.8.0'
end
9 changes: 9 additions & 0 deletions lib/kernel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module Kernel
def suppress_warnings
original_verbosity = $VERBOSE
$VERBOSE = nil
result = yield
$VERBOSE = original_verbosity
result
end
end
9 changes: 9 additions & 0 deletions test/pdf_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

0 comments on commit 2b8b2f2

Please sign in to comment.