fillable-pdf 0.9.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +5 -7
- data/.github/workflows/test.yml +14 -6
- data/.gitignore +1 -0
- data/.rubocop.yml +1 -1
- data/Appraisals +29 -0
- data/Gemfile +4 -0
- data/README.md +31 -7
- data/ext/commons-9.4.0.jar +0 -0
- data/ext/{font-asian-7.2.4.jar → font-asian-9.4.0.jar} +0 -0
- data/ext/forms-9.4.0.jar +0 -0
- data/ext/io-9.4.0.jar +0 -0
- data/ext/kernel-9.4.0.jar +0 -0
- data/ext/layout-9.4.0.jar +0 -0
- data/ext/slf4j-api-2.0.17.jar +0 -0
- data/ext/slf4j-simple-2.0.17.jar +0 -0
- data/fillable-pdf.gemspec +3 -2
- data/lib/fillable-pdf/errors.rb +13 -0
- data/lib/fillable-pdf/version.rb +1 -1
- data/lib/fillable-pdf.rb +215 -70
- metadata +28 -15
- data/ext/commons-7.2.4.jar +0 -0
- data/ext/forms-7.2.4.jar +0 -0
- data/ext/io-7.2.4.jar +0 -0
- data/ext/kernel-7.2.4.jar +0 -0
- data/ext/layout-7.2.4.jar +0 -0
- data/ext/slf4j-api-2.0.4.jar +0 -0
- data/ext/slf4j-simple-2.0.4.jar +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21838c4d6acfe2fee7d07688316c0b4fc32e7e5898dfcd0f77559424e3770802
|
|
4
|
+
data.tar.gz: 153fc518eee4b2fa76a91ce6228739d5d3cef4d5d4f1be05e3ee6a5f6fcf7ce7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41d10ee4f4751d2a8213ed969f16c1c1bb4e80e87e2da9e4dce0a8af73df26bf80c148edbfdbfd5bfdb4876ac090da6783a5304891110802b43a0d6251c7881a
|
|
7
|
+
data.tar.gz: 14b5a5d075ab12359ecda3d93ec1502c2b9b114376e81fbb2946fa9ff7fa1fbac93e091b90df8599280114735fedd3d2d201f5d607bcf3a2c1bd130e388a76c0
|
data/.github/workflows/lint.yml
CHANGED
|
@@ -3,7 +3,7 @@ name: Lint
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
|
-
-
|
|
6
|
+
- master
|
|
7
7
|
pull_request:
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
@@ -12,13 +12,11 @@ jobs:
|
|
|
12
12
|
name: Rubocop
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
16
|
-
- name: Set up Ruby
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- name: Set up latest Ruby
|
|
17
17
|
uses: ruby/setup-ruby@v1
|
|
18
18
|
with:
|
|
19
|
-
ruby-version:
|
|
19
|
+
ruby-version: 'ruby'
|
|
20
20
|
bundler-cache: true
|
|
21
|
-
- name: Install dependencies
|
|
22
|
-
run: bundle install
|
|
23
21
|
- name: Run Rubocop
|
|
24
|
-
run: bin/lint --no-fix
|
|
22
|
+
run: bin/lint --no-fix
|
data/.github/workflows/test.yml
CHANGED
|
@@ -3,25 +3,33 @@ name: Test
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
5
|
branches:
|
|
6
|
-
-
|
|
6
|
+
- master
|
|
7
7
|
pull_request:
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
test:
|
|
11
11
|
runs-on: ubuntu-latest
|
|
12
|
-
name: Ruby ${{ matrix.ruby }}
|
|
12
|
+
name: Ruby ${{ matrix.ruby }} / Java ${{ matrix.java }}
|
|
13
13
|
strategy:
|
|
14
|
+
fail-fast: false
|
|
14
15
|
matrix:
|
|
15
|
-
ruby: [ '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4' ]
|
|
16
|
+
ruby: [ '3.4', '3.3', '3.2', '3.1', '3.0', '2.7', '2.6', '2.5', '2.4' ]
|
|
17
|
+
java: [ '8', '11', '17', '21', '23' ]
|
|
16
18
|
|
|
17
19
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Set up Java ${{ matrix.java }}
|
|
22
|
+
uses: actions/setup-java@v4
|
|
23
|
+
with:
|
|
24
|
+
distribution: 'temurin'
|
|
25
|
+
java-version: ${{ matrix.java }}
|
|
19
26
|
- name: Set up Ruby ${{ matrix.ruby }}
|
|
20
27
|
uses: ruby/setup-ruby@v1
|
|
21
28
|
with:
|
|
22
29
|
ruby-version: ${{ matrix.ruby }}
|
|
23
30
|
bundler-cache: true
|
|
31
|
+
cache-version: java-${{ matrix.java }}
|
|
24
32
|
- name: Install dependencies
|
|
25
|
-
run: bundle install
|
|
33
|
+
run: bundle install && bundle exec appraisal install
|
|
26
34
|
- name: Run tests
|
|
27
|
-
run: bundle exec rake test
|
|
35
|
+
run: bundle exec appraisal rake test
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
data/Appraisals
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
appraise 'rjb-1.6-base64-0.1' do
|
|
2
|
+
gem 'rjb', '~> 1.6.0'
|
|
3
|
+
gem 'base64', '~> 0.1.0'
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
appraise 'rjb-1.6-base64-0.2' do
|
|
7
|
+
gem 'rjb', '~> 1.6.0'
|
|
8
|
+
gem 'base64', '~> 0.2.0'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
appraise 'rjb-1.6-base64-0.3' do
|
|
12
|
+
gem 'rjb', '~> 1.6.0'
|
|
13
|
+
gem 'base64', '~> 0.3.0'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
appraise 'rjb-1.7-base64-0.1' do
|
|
17
|
+
gem 'rjb', '~> 1.7.0'
|
|
18
|
+
gem 'base64', '~> 0.1.0'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
appraise 'rjb-1.7-base64-0.2' do
|
|
22
|
+
gem 'rjb', '~> 1.7.0'
|
|
23
|
+
gem 'base64', '~> 0.2.0'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
appraise 'rjb-1.7-base64-0.3' do
|
|
27
|
+
gem 'rjb', '~> 1.7.0'
|
|
28
|
+
gem 'base64', '~> 0.3.0'
|
|
29
|
+
end
|
data/Gemfile
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
source 'https://rubygems.org'
|
|
2
2
|
|
|
3
|
+
# Specify your gem's dependencies in cloudflare-turnstile-rails.gemspec
|
|
3
4
|
gemspec
|
|
4
5
|
|
|
6
|
+
# A Ruby library for testing your library against different versions of dependencies
|
|
7
|
+
gem 'appraisal'
|
|
8
|
+
|
|
5
9
|
group :development do
|
|
6
10
|
gem 'rake'
|
|
7
11
|
|
data/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
FillablePDF is an extremely simple and lightweight utility that bridges iText and Ruby in order to fill out fillable PDF forms or extract field values from previously filled out PDF forms.
|
|
9
9
|
|
|
10
|
+
Supports `Ruby >= 2.4.0` with `JDK >= 8`
|
|
11
|
+
|
|
10
12
|
[](https://www.buymeacoffee.com/vkononov)
|
|
11
13
|
|
|
12
14
|
## Known Issues
|
|
@@ -55,12 +57,13 @@ If your checkboxes are showing incorrectly, it's likely because iText is overwri
|
|
|
55
57
|
|
|
56
58
|
## Installation
|
|
57
59
|
|
|
58
|
-
**Prerequisites:** Java SE Development Kit
|
|
60
|
+
**Prerequisites:** Java SE Development Kit (JDK)
|
|
59
61
|
|
|
60
62
|
- Ensure that your `JAVA_HOME` variable is set before installing this gem (see examples below).
|
|
61
63
|
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
+
* macOS: `/Library/Java/JavaVirtualMachines/jdk-<version>.jdk/Contents/Home`
|
|
65
|
+
* Linux: `/usr/lib/jvm/java-<version>-openjdk` or `/usr/lib/jvm/temurin-<version>`
|
|
66
|
+
* Windows: `C:\Program Files\Java\jdk-<version>`
|
|
64
67
|
|
|
65
68
|
Add this line to your application's Gemfile:
|
|
66
69
|
|
|
@@ -150,12 +153,12 @@ An instance of `FillablePDF` has the following methods at its disposal:
|
|
|
150
153
|
# output example: true
|
|
151
154
|
```
|
|
152
155
|
|
|
153
|
-
* `
|
|
156
|
+
* `field_count`
|
|
154
157
|
*Returns the total number of fillable form fields.*
|
|
155
158
|
|
|
156
159
|
```ruby
|
|
160
|
+
pdf.field_count
|
|
157
161
|
# output example: 10
|
|
158
|
-
pdf.num_fields
|
|
159
162
|
```
|
|
160
163
|
|
|
161
164
|
* `field(key)`
|
|
@@ -292,7 +295,7 @@ An instance of `FillablePDF` has the following methods at its disposal:
|
|
|
292
295
|
```
|
|
293
296
|
|
|
294
297
|
* `save_as(file_path, flatten: false)`
|
|
295
|
-
*Saves the filled out PDF document in a given path and flattens it if requested.*
|
|
298
|
+
*Saves the filled out PDF document in a given path and flattens it if requested. If the path matches the current file, calls save() instead.*
|
|
296
299
|
|
|
297
300
|
```ruby
|
|
298
301
|
pdf.save_as('output.pdf')
|
|
@@ -303,6 +306,16 @@ An instance of `FillablePDF` has the following methods at its disposal:
|
|
|
303
306
|
|
|
304
307
|
**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.
|
|
305
308
|
|
|
309
|
+
* `save_as!(file_path, flatten: false)`
|
|
310
|
+
*Saves the filled out PDF document in a given path and flattens it if requested. Raises an error if the path matches the current file (use save() instead).*
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
pdf.save_as!('output.pdf')
|
|
314
|
+
# result: document is saved in a new path
|
|
315
|
+
pdf.save_as!(pdf.path)
|
|
316
|
+
# raises InvalidArgumentError
|
|
317
|
+
```
|
|
318
|
+
|
|
306
319
|
* `close`
|
|
307
320
|
*Closes the PDF document discarding all unsaved changes.*
|
|
308
321
|
|
|
@@ -311,6 +324,17 @@ An instance of `FillablePDF` has the following methods at its disposal:
|
|
|
311
324
|
# result: document is closed
|
|
312
325
|
```
|
|
313
326
|
|
|
327
|
+
* `closed?`
|
|
328
|
+
*Checks if the PDF document is closed.*
|
|
329
|
+
|
|
330
|
+
```ruby
|
|
331
|
+
pdf.closed?
|
|
332
|
+
# output example: false
|
|
333
|
+
pdf.close
|
|
334
|
+
pdf.closed?
|
|
335
|
+
# output example: true
|
|
336
|
+
```
|
|
337
|
+
|
|
314
338
|
|
|
315
339
|
## Deployment with Heroku
|
|
316
340
|
|
|
@@ -385,7 +409,7 @@ pdf = FillablePDF.new('input.pdf')
|
|
|
385
409
|
|
|
386
410
|
# total number of fields
|
|
387
411
|
if pdf.any_fields?
|
|
388
|
-
puts "The form has a total of #{pdf.
|
|
412
|
+
puts "The form has a total of #{pdf.field_count} fields."
|
|
389
413
|
else
|
|
390
414
|
puts 'The form is not fillable.'
|
|
391
415
|
end
|
|
Binary file
|
|
Binary file
|
data/ext/forms-9.4.0.jar
ADDED
|
Binary file
|
data/ext/io-9.4.0.jar
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/fillable-pdf.gemspec
CHANGED
|
@@ -21,8 +21,9 @@ Gem::Specification.new do |spec|
|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
|
22
22
|
spec.require_paths = %w[ext lib]
|
|
23
23
|
|
|
24
|
-
spec.
|
|
25
|
-
spec.
|
|
24
|
+
spec.add_dependency 'base64', '~> 0.1'
|
|
25
|
+
spec.add_dependency 'rjb', '~> 1.6'
|
|
26
|
+
spec.requirements << 'JDK >= 8'
|
|
26
27
|
|
|
27
28
|
spec.metadata = {
|
|
28
29
|
'rubygems_mfa_required' => 'true'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class FillablePDF
|
|
2
|
+
# Base error class for all FillablePDF errors
|
|
3
|
+
class Error < StandardError; end
|
|
4
|
+
|
|
5
|
+
# Raised when a field is not found in the PDF form
|
|
6
|
+
class FieldNotFoundError < Error; end
|
|
7
|
+
|
|
8
|
+
# Raised when invalid arguments are provided to a method
|
|
9
|
+
class InvalidArgumentError < Error; end
|
|
10
|
+
|
|
11
|
+
# Raised when a PDF file operation fails
|
|
12
|
+
class FileOperationError < Error; end
|
|
13
|
+
end
|
data/lib/fillable-pdf/version.rb
CHANGED
data/lib/fillable-pdf.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative 'fillable-pdf/itext'
|
|
2
2
|
require_relative 'fillable-pdf/suppress_warnings'
|
|
3
3
|
require_relative 'fillable-pdf/field'
|
|
4
|
+
require_relative 'fillable-pdf/errors'
|
|
4
5
|
require 'base64'
|
|
5
6
|
require 'securerandom'
|
|
6
7
|
require 'tmpdir'
|
|
@@ -12,68 +13,77 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
|
|
|
12
13
|
# Opens a given fillable-pdf PDF file and prepares it for modification.
|
|
13
14
|
#
|
|
14
15
|
# @param [String|Symbol] file_path the name of the PDF file or file path
|
|
16
|
+
# @raise [FileOperationError] if the file is not found or cannot be opened
|
|
15
17
|
#
|
|
16
|
-
def initialize(file_path)
|
|
17
|
-
raise
|
|
18
|
+
def initialize(file_path) # rubocop:disable Metrics/MethodLength
|
|
19
|
+
raise FileOperationError, "File <#{file_path}> is not found" unless File.exist?(file_path)
|
|
18
20
|
@file_path = file_path
|
|
21
|
+
@closed = false
|
|
19
22
|
begin
|
|
20
23
|
@byte_stream = ITEXT::ByteArrayOutputStream.new
|
|
21
24
|
@pdf_reader = ITEXT::PdfReader.new @file_path.to_s
|
|
22
25
|
@pdf_writer = ITEXT::PdfWriter.new @byte_stream
|
|
23
26
|
@pdf_doc = ITEXT::PdfDocument.new @pdf_reader, @pdf_writer
|
|
24
27
|
@pdf_form = ITEXT::PdfAcroForm.getAcroForm(@pdf_doc, true)
|
|
25
|
-
@form_fields = @pdf_form.
|
|
28
|
+
@form_fields = @pdf_form.getAllFormFields
|
|
26
29
|
rescue StandardError => e
|
|
27
|
-
|
|
30
|
+
handle_pdf_open_error(e)
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
##
|
|
32
35
|
# Determines whether the form has any fields.
|
|
33
36
|
#
|
|
34
|
-
# @return true if form has fields, false otherwise
|
|
37
|
+
# @return [Boolean] true if form has fields, false otherwise
|
|
35
38
|
#
|
|
36
39
|
def any_fields?
|
|
37
|
-
|
|
40
|
+
field_count.positive?
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
##
|
|
41
44
|
# Returns the total number of fillable form fields.
|
|
42
45
|
#
|
|
43
|
-
# @return the number of fields
|
|
46
|
+
# @return [Integer] the number of fields
|
|
44
47
|
#
|
|
45
|
-
def
|
|
48
|
+
def field_count
|
|
46
49
|
@form_fields.size
|
|
47
50
|
end
|
|
48
51
|
|
|
52
|
+
##
|
|
53
|
+
# @deprecated Use {#field_count} instead
|
|
54
|
+
def num_fields
|
|
55
|
+
warn '[DEPRECATION] `num_fields` is deprecated. Use `field_count` instead.'
|
|
56
|
+
field_count
|
|
57
|
+
end
|
|
58
|
+
|
|
49
59
|
##
|
|
50
60
|
# Retrieves the value of a field given its unique field name.
|
|
51
61
|
#
|
|
52
62
|
# @param [String|Symbol] key the field name
|
|
53
|
-
#
|
|
54
|
-
# @
|
|
63
|
+
# @return [String] the value of the field
|
|
64
|
+
# @raise [FieldNotFoundError] if the field does not exist
|
|
55
65
|
#
|
|
56
66
|
def field(key)
|
|
57
67
|
pdf_field(key).getValueAsString
|
|
58
68
|
rescue NoMethodError
|
|
59
|
-
raise "
|
|
69
|
+
raise FieldNotFoundError, "Unknown key name `#{key}'"
|
|
60
70
|
end
|
|
61
71
|
|
|
62
72
|
##
|
|
63
73
|
# Retrieves the string type of a field given its unique field name.
|
|
64
74
|
#
|
|
65
75
|
# @param [String|Symbol] key the field name
|
|
66
|
-
#
|
|
67
|
-
# @
|
|
76
|
+
# @return [String, nil] the type of the field (e.g., '/Btn', '/Tx', '/Ch', '/Sig')
|
|
77
|
+
# @raise [FieldNotFoundError] if the field does not exist
|
|
68
78
|
#
|
|
69
79
|
def field_type(key)
|
|
70
|
-
pdf_field(key).getFormType
|
|
80
|
+
pdf_field(key).getFormType&.toString
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
##
|
|
74
84
|
# Retrieves a hash of all fields and their values.
|
|
75
85
|
#
|
|
76
|
-
# @return
|
|
86
|
+
# @return [Hash{Symbol => String}] hash of field keys (as symbols) and values
|
|
77
87
|
#
|
|
78
88
|
def fields
|
|
79
89
|
iterator = @form_fields.keySet.iterator
|
|
@@ -90,14 +100,23 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
|
|
|
90
100
|
#
|
|
91
101
|
# @param [String|Symbol] key the field name
|
|
92
102
|
# @param [String|Symbol] value the field value
|
|
93
|
-
# @param [
|
|
103
|
+
# @param [Boolean, nil] 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
|
|
104
|
+
# @return [self] returns self for method chaining
|
|
105
|
+
# @raise [InvalidArgumentError] if key or value are invalid
|
|
106
|
+
# @raise [FieldNotFoundError] if the field does not exist
|
|
94
107
|
#
|
|
95
108
|
def set_field(key, value, generate_appearance: nil)
|
|
109
|
+
ensure_document_open
|
|
110
|
+
validate_input(key, value)
|
|
111
|
+
field = pdf_field(key)
|
|
112
|
+
|
|
96
113
|
if generate_appearance.nil?
|
|
97
|
-
|
|
114
|
+
field.setValue(value.to_s)
|
|
98
115
|
else
|
|
99
|
-
|
|
116
|
+
field.setValue(value.to_s, generate_appearance)
|
|
100
117
|
end
|
|
118
|
+
|
|
119
|
+
self
|
|
101
120
|
end
|
|
102
121
|
|
|
103
122
|
##
|
|
@@ -108,37 +127,49 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
|
|
|
108
127
|
#
|
|
109
128
|
# @param [String|Symbol] key the field name
|
|
110
129
|
# @param [String|Symbol] file_path the name of the image file or image path
|
|
130
|
+
# @return [self] returns self for method chaining
|
|
131
|
+
# @raise [FileOperationError] if the image file is not found
|
|
132
|
+
# @raise [FieldNotFoundError] if the field does not exist
|
|
111
133
|
#
|
|
112
134
|
def set_image(key, file_path) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
orig_rect
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
ensure_document_open
|
|
136
|
+
raise FileOperationError, "File <#{file_path}> is not found" unless File.exist?(file_path)
|
|
137
|
+
|
|
138
|
+
begin
|
|
139
|
+
field = pdf_field(key)
|
|
140
|
+
widgets = field.getWidgets
|
|
141
|
+
widget_dict = suppress_warnings { widgets.isEmpty ? field.getPdfObject : widgets.get(0).getPdfObject }
|
|
142
|
+
orig_rect = widget_dict.getAsRectangle(ITEXT::PdfName.Rect)
|
|
143
|
+
|
|
144
|
+
border_style = field.getWidgets.get(0).getBorderStyle
|
|
145
|
+
border_width = border_style.nil? ? 0 : border_style.getWidth
|
|
146
|
+
|
|
147
|
+
bounding_rectangle = ITEXT::Rectangle.new(
|
|
148
|
+
orig_rect.getWidth - (border_width * 2),
|
|
149
|
+
orig_rect.getHeight - (border_width * 2)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
pdf_form_x_object = ITEXT::PdfFormXObject.new(bounding_rectangle)
|
|
153
|
+
canvas = ITEXT::Canvas.new(pdf_form_x_object, @pdf_doc)
|
|
154
|
+
image = ITEXT::Image.new(ITEXT::ImageDataFactory.create(file_path.to_s))
|
|
155
|
+
.setAutoScale(true)
|
|
156
|
+
.setHorizontalAlignment(ITEXT::HorizontalAlignment.CENTER)
|
|
157
|
+
container = ITEXT::Div.new
|
|
158
|
+
.setMargin(border_width).add(image)
|
|
159
|
+
.setVerticalAlignment(ITEXT::VerticalAlignment.MIDDLE)
|
|
160
|
+
.setFillAvailableArea(true)
|
|
161
|
+
canvas.add(container)
|
|
162
|
+
canvas.close
|
|
163
|
+
|
|
164
|
+
pdf_dict = ITEXT::PdfDictionary.new
|
|
165
|
+
widget_dict.put(ITEXT::PdfName.AP, pdf_dict)
|
|
166
|
+
pdf_dict.put(ITEXT::PdfName.N, pdf_form_x_object.getPdfObject)
|
|
167
|
+
widget_dict.setModified
|
|
168
|
+
rescue StandardError => e
|
|
169
|
+
raise FileOperationError, "Failed to set image for field '#{key}': #{e.message}"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
self
|
|
142
173
|
end
|
|
143
174
|
|
|
144
175
|
##
|
|
@@ -148,49 +179,97 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
|
|
|
148
179
|
# content will be removed, which means you cannot have both text and image.
|
|
149
180
|
#
|
|
150
181
|
# @param [String|Symbol] key the field name
|
|
151
|
-
# @param [String
|
|
182
|
+
# @param [String] base64_image_data base64 encoded image data
|
|
183
|
+
# @return [self] returns self for method chaining
|
|
184
|
+
# @raise [InvalidArgumentError] if the base64 data is invalid
|
|
185
|
+
# @raise [FieldNotFoundError] if the field does not exist
|
|
152
186
|
#
|
|
153
187
|
def set_image_base64(key, base64_image_data)
|
|
188
|
+
ensure_document_open
|
|
154
189
|
tmp_file = "#{Dir.tmpdir}/#{SecureRandom.uuid}"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
190
|
+
begin
|
|
191
|
+
decoded_data = Base64.strict_decode64(base64_image_data)
|
|
192
|
+
File.binwrite(tmp_file, decoded_data)
|
|
193
|
+
set_image(key, tmp_file)
|
|
194
|
+
rescue ArgumentError => e
|
|
195
|
+
raise InvalidArgumentError, "Invalid base64 data: #{e.message}"
|
|
196
|
+
ensure
|
|
197
|
+
FileUtils.rm_f(tmp_file)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
self
|
|
159
201
|
end
|
|
160
202
|
|
|
161
203
|
##
|
|
162
204
|
# Sets the values of multiple fields given a set of unique field names and values.
|
|
163
205
|
#
|
|
164
|
-
# @param [Hash] fields the set of field names and values
|
|
165
|
-
# @param [
|
|
206
|
+
# @param [Hash{String, Symbol => String}] fields the set of field names and values
|
|
207
|
+
# @param [Boolean, nil] 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
|
|
208
|
+
# @return [self] returns self for method chaining
|
|
209
|
+
# @raise [InvalidArgumentError] if any key or value is invalid
|
|
210
|
+
# @raise [FieldNotFoundError] if any field does not exist
|
|
166
211
|
#
|
|
167
212
|
def set_fields(fields, generate_appearance: nil)
|
|
168
|
-
|
|
213
|
+
ensure_document_open
|
|
214
|
+
fields.each { |key, value| set_field(key, value, generate_appearance: generate_appearance) }
|
|
215
|
+
self
|
|
169
216
|
end
|
|
170
217
|
|
|
171
218
|
##
|
|
172
219
|
# Renames a field given its unique field name and the new field name.
|
|
173
220
|
#
|
|
174
|
-
# @param [String|Symbol] old_key the field name
|
|
175
|
-
# @param [String|Symbol] new_key the field name
|
|
221
|
+
# @param [String|Symbol] old_key the current field name
|
|
222
|
+
# @param [String|Symbol] new_key the new field name
|
|
223
|
+
# @return [self] returns self for method chaining
|
|
224
|
+
# @raise [FieldNotFoundError] if the field does not exist
|
|
225
|
+
# @raise [InvalidArgumentError] if the new field name already exists
|
|
176
226
|
#
|
|
177
|
-
def rename_field(old_key, new_key)
|
|
178
|
-
|
|
227
|
+
def rename_field(old_key, new_key) # rubocop:disable Metrics/MethodLength
|
|
228
|
+
ensure_document_open
|
|
229
|
+
validate_field_name(old_key)
|
|
230
|
+
validate_field_name(new_key)
|
|
231
|
+
|
|
232
|
+
old_key = old_key.to_s
|
|
233
|
+
new_key = new_key.to_s
|
|
234
|
+
|
|
235
|
+
raise FieldNotFoundError, "Field `#{old_key}` not found" unless @form_fields.containsKey(old_key)
|
|
236
|
+
raise InvalidArgumentError, "Field name `#{new_key}` already exists" if @form_fields.containsKey(new_key)
|
|
237
|
+
|
|
238
|
+
field = pdf_field(old_key)
|
|
239
|
+
field.setFieldName(new_key)
|
|
240
|
+
|
|
241
|
+
@form_fields.remove(old_key)
|
|
242
|
+
@form_fields.put(new_key, field)
|
|
243
|
+
|
|
244
|
+
self
|
|
245
|
+
rescue FieldNotFoundError, InvalidArgumentError
|
|
246
|
+
raise
|
|
247
|
+
rescue StandardError => e
|
|
248
|
+
raise FileOperationError, "Unable to rename field `#{old_key}` to `#{new_key}`: #{e.message}"
|
|
179
249
|
end
|
|
180
250
|
|
|
181
251
|
##
|
|
182
252
|
# Removes a field from the document given its unique field name.
|
|
183
253
|
#
|
|
184
254
|
# @param [String|Symbol] key the field name
|
|
255
|
+
# @return [self] returns self for method chaining
|
|
256
|
+
# @raise [FieldNotFoundError] if the field does not exist
|
|
185
257
|
#
|
|
186
258
|
def remove_field(key)
|
|
259
|
+
ensure_document_open
|
|
260
|
+
validate_field_name(key)
|
|
261
|
+
raise FieldNotFoundError, "Unknown key name `#{key}'" unless @form_fields.containsKey(key.to_s)
|
|
262
|
+
|
|
187
263
|
@pdf_form.removeField(key.to_s)
|
|
264
|
+
@form_fields.remove(key.to_s)
|
|
265
|
+
|
|
266
|
+
self
|
|
188
267
|
end
|
|
189
268
|
|
|
190
269
|
##
|
|
191
270
|
# Returns a list of all field keys used in the document.
|
|
192
271
|
#
|
|
193
|
-
# @return array of field names
|
|
272
|
+
# @return [Array<Symbol>] array of field names as symbols
|
|
194
273
|
#
|
|
195
274
|
def names
|
|
196
275
|
iterator = @form_fields.keySet.iterator
|
|
@@ -202,7 +281,7 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
|
|
|
202
281
|
##
|
|
203
282
|
# Returns a list of all field values used in the document.
|
|
204
283
|
#
|
|
205
|
-
# @return array of field values
|
|
284
|
+
# @return [Array<String>] array of field values
|
|
206
285
|
#
|
|
207
286
|
def values
|
|
208
287
|
iterator = @form_fields.keySet.iterator
|
|
@@ -214,36 +293,82 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
|
|
|
214
293
|
##
|
|
215
294
|
# Overwrites the previously opened PDF document and flattens it if requested.
|
|
216
295
|
#
|
|
217
|
-
# @param [
|
|
296
|
+
# @param [Boolean] flatten true if PDF should be flattened, false otherwise
|
|
297
|
+
# @return [self] returns self for method chaining
|
|
298
|
+
# @raise [FileOperationError] if the save operation fails
|
|
218
299
|
#
|
|
219
300
|
def save(flatten: false)
|
|
301
|
+
ensure_document_open
|
|
220
302
|
tmp_file = "#{Dir.tmpdir}/#{SecureRandom.uuid}"
|
|
221
303
|
save_as(tmp_file, flatten: flatten)
|
|
222
304
|
FileUtils.mv tmp_file, @file_path
|
|
305
|
+
self
|
|
223
306
|
end
|
|
224
307
|
|
|
225
308
|
##
|
|
226
309
|
# Saves the filled out PDF document in a given path and flattens it if requested.
|
|
310
|
+
# If the path matches the current file path, it will call save() instead.
|
|
227
311
|
#
|
|
228
312
|
# @param [String] file_path the name of the PDF file or file path
|
|
229
|
-
# @param [
|
|
313
|
+
# @param [Boolean] flatten true if PDF should be flattened, false otherwise
|
|
314
|
+
# @return [self] returns self for method chaining
|
|
315
|
+
# @raise [FileOperationError] if the save operation fails
|
|
230
316
|
#
|
|
231
317
|
def save_as(file_path, flatten: false)
|
|
318
|
+
ensure_document_open
|
|
232
319
|
if @file_path == file_path
|
|
233
320
|
save(flatten: flatten)
|
|
234
|
-
|
|
235
|
-
File.open(file_path, 'wb') { |f| f.write(finalize(flatten: flatten)) && f.close }
|
|
321
|
+
return self
|
|
236
322
|
end
|
|
323
|
+
|
|
324
|
+
File.open(file_path, 'wb') { |f| f.write(finalize(flatten: flatten)) && f.close }
|
|
325
|
+
self
|
|
326
|
+
rescue StandardError => e
|
|
327
|
+
raise FileOperationError, "Failed to save file `#{file_path}`: #{e.message}"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
##
|
|
331
|
+
# Saves the filled out PDF document in a given path and flattens it if requested.
|
|
332
|
+
# Raises an error if the path matches the current file path (use save() instead).
|
|
333
|
+
#
|
|
334
|
+
# @param [String] file_path the name of the PDF file or file path
|
|
335
|
+
# @param [Boolean] flatten true if PDF should be flattened, false otherwise
|
|
336
|
+
# @return [self] returns self for method chaining
|
|
337
|
+
# @raise [InvalidArgumentError] if file_path matches the current file path
|
|
338
|
+
# @raise [FileOperationError] if the save operation fails
|
|
339
|
+
#
|
|
340
|
+
def save_as!(file_path, flatten: false)
|
|
341
|
+
ensure_document_open
|
|
342
|
+
raise InvalidArgumentError, 'Cannot save_as! to the same file path. Use save() instead.' if @file_path == file_path
|
|
343
|
+
|
|
344
|
+
File.open(file_path, 'wb') { |f| f.write(finalize(flatten: flatten)) && f.close }
|
|
345
|
+
self
|
|
346
|
+
rescue InvalidArgumentError
|
|
347
|
+
raise
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
raise FileOperationError, "Failed to save file `#{file_path}`: #{e.message}"
|
|
237
350
|
end
|
|
238
351
|
|
|
239
352
|
##
|
|
240
353
|
# Closes the PDF document discarding all unsaved changes.
|
|
241
354
|
#
|
|
242
|
-
# @return [Boolean] true if document is closed
|
|
355
|
+
# @return [Boolean] true if document is closed
|
|
243
356
|
#
|
|
244
|
-
def close
|
|
357
|
+
def close # rubocop:disable Naming/PredicateMethod
|
|
358
|
+
return true if closed?
|
|
359
|
+
|
|
245
360
|
@pdf_doc.close
|
|
246
|
-
@
|
|
361
|
+
@closed = true
|
|
362
|
+
true
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
##
|
|
366
|
+
# Checks if the PDF document is closed.
|
|
367
|
+
#
|
|
368
|
+
# @return [Boolean] true if document is closed, false otherwise
|
|
369
|
+
#
|
|
370
|
+
def closed?
|
|
371
|
+
@closed ||= false
|
|
247
372
|
end
|
|
248
373
|
|
|
249
374
|
private
|
|
@@ -251,17 +376,37 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
|
|
|
251
376
|
##
|
|
252
377
|
# Writes the contents of the modified fields to the previously opened PDF file.
|
|
253
378
|
#
|
|
254
|
-
# @param [
|
|
379
|
+
# @param [Boolean] flatten true if PDF should be flattened, false otherwise
|
|
380
|
+
# @return [Java::byte[]] byte array of the PDF document
|
|
255
381
|
#
|
|
256
382
|
def finalize(flatten: false)
|
|
257
383
|
@pdf_form.flattenFields if flatten
|
|
258
384
|
close
|
|
259
385
|
@byte_stream.toByteArray
|
|
386
|
+
rescue StandardError => e
|
|
387
|
+
raise FileOperationError, "Failed to finalize document: #{e.message}"
|
|
260
388
|
end
|
|
261
389
|
|
|
262
390
|
def pdf_field(key)
|
|
263
391
|
field = @form_fields.get(key.to_s)
|
|
264
|
-
raise "
|
|
392
|
+
raise FieldNotFoundError, "Unknown key name `#{key}'" if field.nil?
|
|
265
393
|
field
|
|
266
394
|
end
|
|
395
|
+
|
|
396
|
+
def validate_input(key, value)
|
|
397
|
+
validate_field_name(key)
|
|
398
|
+
raise InvalidArgumentError, 'Field value cannot be nil' if value.nil?
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def validate_field_name(key)
|
|
402
|
+
raise InvalidArgumentError, 'Field name must be a string or symbol' unless key.is_a?(String) || key.is_a?(Symbol)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def ensure_document_open
|
|
406
|
+
raise FileOperationError, 'Cannot perform operation on a closed PDF document' if closed?
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def handle_pdf_open_error(err)
|
|
410
|
+
raise FileOperationError, "#{err.message} (Input file may be corrupt, incompatible, read-only, write-protected, encrypted, or may not have any form fields)" # rubocop:disable Layout/LineLength
|
|
411
|
+
end
|
|
267
412
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fillable-pdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vadim Kononov
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
13
26
|
- !ruby/object:Gem::Dependency
|
|
14
27
|
name: rjb
|
|
15
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +50,7 @@ files:
|
|
|
37
50
|
- ".github/workflows/test.yml"
|
|
38
51
|
- ".gitignore"
|
|
39
52
|
- ".rubocop.yml"
|
|
53
|
+
- Appraisals
|
|
40
54
|
- Gemfile
|
|
41
55
|
- LICENSE.md
|
|
42
56
|
- README.md
|
|
@@ -44,19 +58,20 @@ files:
|
|
|
44
58
|
- bin/console
|
|
45
59
|
- bin/lint
|
|
46
60
|
- bin/setup
|
|
47
|
-
- ext/commons-
|
|
48
|
-
- ext/font-asian-
|
|
49
|
-
- ext/forms-
|
|
50
|
-
- ext/io-
|
|
51
|
-
- ext/kernel-
|
|
52
|
-
- ext/layout-
|
|
53
|
-
- ext/slf4j-api-2.0.
|
|
54
|
-
- ext/slf4j-simple-2.0.
|
|
61
|
+
- ext/commons-9.4.0.jar
|
|
62
|
+
- ext/font-asian-9.4.0.jar
|
|
63
|
+
- ext/forms-9.4.0.jar
|
|
64
|
+
- ext/io-9.4.0.jar
|
|
65
|
+
- ext/kernel-9.4.0.jar
|
|
66
|
+
- ext/layout-9.4.0.jar
|
|
67
|
+
- ext/slf4j-api-2.0.17.jar
|
|
68
|
+
- ext/slf4j-simple-2.0.17.jar
|
|
55
69
|
- fillable-pdf.gemspec
|
|
56
70
|
- images/blank.png
|
|
57
71
|
- images/checked.png
|
|
58
72
|
- images/distinct.png
|
|
59
73
|
- lib/fillable-pdf.rb
|
|
74
|
+
- lib/fillable-pdf/errors.rb
|
|
60
75
|
- lib/fillable-pdf/field.rb
|
|
61
76
|
- lib/fillable-pdf/itext.rb
|
|
62
77
|
- lib/fillable-pdf/suppress_warnings.rb
|
|
@@ -66,7 +81,6 @@ licenses:
|
|
|
66
81
|
- MIT
|
|
67
82
|
metadata:
|
|
68
83
|
rubygems_mfa_required: 'true'
|
|
69
|
-
post_install_message:
|
|
70
84
|
rdoc_options: []
|
|
71
85
|
require_paths:
|
|
72
86
|
- ext
|
|
@@ -82,9 +96,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
82
96
|
- !ruby/object:Gem::Version
|
|
83
97
|
version: '0'
|
|
84
98
|
requirements:
|
|
85
|
-
- JDK 8
|
|
86
|
-
rubygems_version: 3.
|
|
87
|
-
signing_key:
|
|
99
|
+
- JDK >= 8
|
|
100
|
+
rubygems_version: 3.6.9
|
|
88
101
|
specification_version: 4
|
|
89
102
|
summary: Fill out or extract field values from simple fillable PDF forms using iText.
|
|
90
103
|
test_files: []
|
data/ext/commons-7.2.4.jar
DELETED
|
Binary file
|
data/ext/forms-7.2.4.jar
DELETED
|
Binary file
|
data/ext/io-7.2.4.jar
DELETED
|
Binary file
|
data/ext/kernel-7.2.4.jar
DELETED
|
Binary file
|
data/ext/layout-7.2.4.jar
DELETED
|
Binary file
|
data/ext/slf4j-api-2.0.4.jar
DELETED
|
Binary file
|
data/ext/slf4j-simple-2.0.4.jar
DELETED
|
Binary file
|