Skip to content

Commit

Permalink
Fix an issue with incorrect or blank checkbox appearances. (#29)
Browse files Browse the repository at this point in the history
In a previous commit, iText's `generateAppearance` field was set to true for all fields except checkboxes in order to enable custom checkboxes to show correctly. However, this had unintended consequences for some developers where checkboxes with the default appearance were completely blank.

In order to work correctly with both custom checkboxes (such as square, circle, diamond, or other) and default checkboxes, developers can now set `generate_appearance` themselves to whatever suits their type of PDF file.
  • Loading branch information
vkononov authored Nov 19, 2022
1 parent b1aca9a commit 7692f67
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 31 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Layout/LineLength:
- README.md
- fillable-pdf.gemspec
Max: 120
AllowedPatterns: ['^(\s*#)']

Layout/SpaceInsideHashLiteralBraces:
Enabled: false
Expand Down
86 changes: 67 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,35 @@ FillablePDF is an extremely simple and lightweight utility that bridges iText an

4. Read-only, write-protected or encrypted PDF files are currently not supported.

5. Adobe generated field arrays (i.e. fields with names such as `array.0` or `array.1.0`) are not supported.
5. Adobe generated field arrays (i.e. fields with names such as `array.0` or `array.1.0`) are not supported.


## Troubleshooting Issues

### Blank Fields

* **Actual Result:**

![Blank](images/blank.png)

* **Expected Result:**

![Blank](images/checked.png)

If only of the fields are blank, try setting the `generate_appearance` flag to `true` when calling `set_field` or `set_fields`.

### Invalid Checkbox Appearances

* **Actual Result:**

![Blank](images/checked.png)

* **Expected Result:**

![Blank](images/distinct.png)

If your checkboxes are showing incorrectly, it's likely because iText is overwriting your checkbox appearances. Try setting the `generate_appearance` flag to `false` when calling `set_field` or `set_fields`.
## Installation
**Prerequisites:** Java SE Development Kit v8, v11
Expand Down Expand Up @@ -131,19 +157,21 @@ An instance of `FillablePDF` has the following methods at its disposal:
pdf.num_fields
```
* `field`
* `field(key)`
*Retrieves the value of a field given its unique field name.*
```ruby
pdf.field(:full_name)
pdf.field('full_name')
# output example: 'Richard'
```
* `field_type`
* `field_type(key)`
*Retrieves the string type of a field given its unique field name.*
```ruby
pdf.field_type(:football)
pdf.field_type('football')
# output example: '/Btn'
# list of all field types
Expand All @@ -157,6 +185,7 @@ An instance of `FillablePDF` has the following methods at its disposal:
```ruby
pdf.field_type(:football) == Field::BUTTON
pdf.field_type('football') == Field::BUTTON
```
* `fields`
Expand All @@ -167,52 +196,71 @@ An instance of `FillablePDF` has the following methods at its disposal:
# output example: {first_name: "Richard", last_name: "Rahl"}
```
* `set_field`
*Sets the value of a field given its unique field name and value.*
* `set_field(key, value, generate_appearance: nil)`
*Sets the value of a field given its unique field name and value, with an optional `generate_appearance` directive.*
```ruby
pdf.set_field(:first_name, 'Richard')
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.*
Optionally, you can choose to override iText's `generateAppearance` flag to take better control of your field's appearance, using `generate_appearance`. Passing `true` will force the field to generate its own appearance, while setting it to `false` would leave the appearance generation up to the PDF viewer application. Omitting the parameter would allow iText to decide what should happen.
```ruby
pdf.set_field(:first_name, 'Richard', generate_appearance: true)
pdf.set_field('first_name', 'Richard', generate_appearance: false)
```
* `def set_fields(fields, generate_appearance: nil)`
*Sets the values of multiple fields given a set of unique field names and values, with an optional `generate_appearance` directive.*
```ruby
pdf.set_fields(first_name: 'Richard', last_name: 'Rahl')
pdf.set_fields({first_name: 'Richard', last_name: 'Rahl'})
# result: changes the values of 'first_name' and 'last_name'
```
* `set_image`
Optionally, you can choose to override iText's `generateAppearance` flag to take better control of your fields' appearance, using `generate_appearance`. Passing `true` will force the field to generate its own appearance, while setting it to `false` would leave the appearance generation up to the PDF viewer application. Omitting the parameter would allow iText to decide what should happen.
```ruby
pdf.set_fields({first_name: 'Richard', last_name: 'Rahl'}, generate_appearance: true)
pdf.set_fields({first_name: 'Richard', last_name: 'Rahl'}, generate_appearance: false)
```
* `set_image(key, file_path)`
*Places an image file within the rectangular bounding box of the given form field.*
```ruby
pdf.set_image(:signature, 'signature.png')
pdf.set_image('signature', 'signature.png')
# result: the image 'signature.png' is shown in the foreground of the form field
```
* `set_image_base64`
* `set_image_base64(key, base64_image_data)`
*Places a base64 encoded image within the rectangular bounding box of the given form field.*
```ruby
pdf.set_image_base64('signature', 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==')
pdf.set_image_base64(:signature, 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==')
# result: the base64 encoded image is shown in the foreground of the form field
```
* `rename_field`
* `rename_field(old_key, new_key)`
*Renames a field given its unique field name and the new field name.*
```ruby
pdf.rename_field(:last_name, :surname)
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`
* `remove_field(key)`
*Removes a field from the document given its unique field name.*
```ruby
pdf.remove_field(:last_name)
pdf.remove_field('last_name')
# result: physically removes field 'last_name' from document
```
Expand All @@ -232,7 +280,7 @@ An instance of `FillablePDF` has the following methods at its disposal:
# output example: ["Rahl", "Richard"]
```
* `save`
* `save(flatten: false)`
*Overwrites the previously opened PDF document and flattens it if requested.*
```ruby
Expand All @@ -242,7 +290,7 @@ An instance of `FillablePDF` has the following methods at its disposal:
# result: document is saved with flattening
```
* `save_as`
* `save_as(file_path, flatten: false)`
*Saves the filled out PDF document in a given path and flattens it if requested.*
```ruby
Expand Down Expand Up @@ -344,8 +392,8 @@ end
puts
# setting form fields
pdf.set_fields(first_name: 'Richard', last_name: 'Rahl')
pdf.set_fields(football: 'Yes', baseball: 'Yes', basketball: 'Yes', nascar: 'Yes', hockey: 'Yes')
pdf.set_fields({first_name: 'Richard', last_name: 'Rahl'})
pdf.set_fields({football: 'Yes', baseball: 'Yes', basketball: 'Yes', nascar: 'Yes', hockey: 'Yes', rugby: 'Yes'}, generate_appearance: false)
pdf.set_field(:date, Time.now.strftime('%B %e, %Y'))
pdf.set_field(:newsletter, 'Off') # uncheck the checkbox
pdf.set_field(:language, 'dart') # select a radio button option
Expand All @@ -368,7 +416,7 @@ puts "Values: #{pdf.values}"
puts
# Checking field type
if pdf.field_type(:football) == Field::BUTTON
if pdf.field_type(:rugby) == Field::BUTTON
puts "Field 'football' is of type BUTTON"
else
puts "Field 'football' is not of type BUTTON"
Expand All @@ -383,8 +431,8 @@ puts "Renamed field 'last_name' to 'surname'"
puts
# Removing field
pdf.remove_field :nascar
puts "Removed field 'nascar'"
pdf.remove_field :marketing
puts "Removed field 'marketing'"
# saving the filled out PDF in another file
pdf.save_as('output.pdf')
Expand Down
7 changes: 5 additions & 2 deletions example/run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
puts

# setting form fields
pdf.set_fields(first_name: 'Richard', last_name: 'Rahl')
pdf.set_fields(football: 'Yes', baseball: 'Yes', basketball: 'Yes', nascar: 'Yes', hockey: 'Yes', rugby: 'Yes')
pdf.set_fields({first_name: 'Richard', last_name: 'Rahl'})
pdf.set_fields(
{football: 'Yes', baseball: 'Yes', basketball: 'Yes', nascar: 'Yes', hockey: 'Yes', rugby: 'Yes'},
generate_appearance: false
)
pdf.set_field(:date, Time.now.strftime('%B %e, %Y'))
pdf.set_field(:newsletter, 'Off') # uncheck the checkbox
pdf.set_field(:language, 'dart') # select a radio button option
Expand Down
Binary file added images/blank.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/checked.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/distinct.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 12 additions & 9 deletions lib/fillable-pdf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,12 +87,14 @@ def fields
#
# @param [String|Symbol] key the field name
# @param [String|Symbol] value the field value
# @param [NilClass|TrueClass|FalseClass] generate_appearance true to generate appearance, false to let the PDF viewer application generate form field appearance, nil (default) to let iText decide what's appropriate
#
def set_field(key, value)
# we set generate_appearance to false for buttons to ensure that the chosen
# appearance for checkboxes (i.e. check, circle, diamond) is not changed
generate_appearance = field_type(key) != Field::BUTTON
pdf_field(key).setValue(value.to_s, generate_appearance)
def set_field(key, value, generate_appearance: nil)
if generate_appearance.nil?
pdf_field(key).setValue(value.to_s)
else
pdf_field(key).setValue(value.to_s, generate_appearance)
end
end

##
Expand Down Expand Up @@ -157,9 +159,10 @@ def set_image_base64(key, base64_image_data)
# Sets the values of multiple fields given a set of unique field names and values.
#
# @param [Hash] fields the set of field names and values
# @param [NilClass|TrueClass|FalseClass] generate_appearance true to generate appearance, false to let the PDF viewer application generate form field appearance, nil (default) to let iText decide what's appropriate
#
def set_fields(fields)
fields.each { |key, value| set_field key, value }
def set_fields(fields, generate_appearance: nil)
fields.each { |key, value| set_field key, value, generate_appearance: generate_appearance }
end

##
Expand Down Expand Up @@ -220,7 +223,7 @@ def save(flatten: false)
# 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
# @param [TrueClass|FalseClass] flatten true if PDF should be flattened, false otherwise
#
def save_as(file_path, flatten: false)
if @file_path == file_path
Expand All @@ -245,7 +248,7 @@ def close
##
# Writes the contents of the modified fields to the previously opened PDF file.
#
# @param [Hash] flatten: true if PDF should be flattened, false otherwise
# @param [TrueClass|FalseClass] flatten: true if PDF should be flattened, false otherwise
#
def finalize(flatten: false)
@pdf_form.flattenFields if flatten
Expand Down
14 changes: 13 additions & 1 deletion test/pdf_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def test_that_an_error_is_thrown_for_non_existing_file
err = assert_raises IOError do
@pdf = FillablePDF.new 'test.pdf'
end

assert_match 'is not found', err.message
end

Expand Down Expand Up @@ -46,6 +47,7 @@ def test_that_a_field_type_can_be_accessed_by_name

def test_that_a_field_value_can_be_modified
@pdf.set_field(:first_name, 'Richard')

assert_equal 'Richard', @pdf.field(:first_name)
end

Expand All @@ -59,26 +61,32 @@ def test_that_a_base64_can_be_placed_in_photo_field

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')
@pdf.set_fields({first_name: 'Richard', last_name: 'Rahl'})

assert_equal 'Richard', @pdf.field(:first_name)
assert_equal 'Rahl', @pdf.field(:last_name)
end

def test_that_a_checkbox_can_be_checked_and_unchecked
@pdf.set_field(:nascar, 'Yes')

assert_equal 'Yes', @pdf.field(:nascar)
@pdf.set_field(:newsletter, 'Off')

assert_equal 'Off', @pdf.field(:newsletter)
end

def test_that_a_radio_button_can_be_checked_and_unchecked
@pdf.set_field(:language, 'ruby')

3.times { |i| assert_equal 'ruby', @pdf.field("language.#{i}".gsub('.0', '')) }
@pdf.set_field(:language, 'Off')

3.times { |i| assert_equal 'Off', @pdf.field("language.#{i}".gsub('.0', '')) }
end

Expand All @@ -89,6 +97,7 @@ def test_that_a_field_can_be_renamed
err = assert_raises RuntimeError do
@pdf.field(:last_name)
end

assert_match 'unknown key name', err.message
assert_equal 'Test', @pdf.field(:surname)
end
Expand All @@ -98,6 +107,7 @@ def test_that_a_field_can_be_removed
err = assert_raises RuntimeError do
@pdf.field(:first_name)
end

assert_match 'unknown key name', err.message
end

Expand All @@ -111,9 +121,11 @@ def test_that_field_values_can_be_accessed

def test_that_a_file_can_be_saved
@pdf.save_as(@tmp)

refute_nil FillablePDF.new(@tmp)
@pdf = FillablePDF.new(@tmp)
@pdf.save

refute_nil FillablePDF.new(@tmp)
end

Expand Down

0 comments on commit 7692f67

Please sign in to comment.