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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 29550c61e1d22f57a0df5eb9d2de4c01c49bed9f1ae36f96a7fd18889484d70d
4
- data.tar.gz: a3616692065ea659cc7eb582450def4645d90e6c4cea87d74e35dfb3e68650df
3
+ metadata.gz: 21838c4d6acfe2fee7d07688316c0b4fc32e7e5898dfcd0f77559424e3770802
4
+ data.tar.gz: 153fc518eee4b2fa76a91ce6228739d5d3cef4d5d4f1be05e3ee6a5f6fcf7ce7
5
5
  SHA512:
6
- metadata.gz: 9d40a617f23bd04c4f36853e79131d5d378573c37ca71c97aed293fc55c0c39dbc38b28a2d3c7cb56c7be11c0dc8a0d207cce30a75677076787fb9a7caaedb07
7
- data.tar.gz: b7c08116d5ea9721fb5b5a44be38b9ba7488b65e0b8020d148b5ae467b8b1c3319452871a7c61c1dbdca049c1ec55be41a68221fae1d943f72838c9d1c433b87
6
+ metadata.gz: 41d10ee4f4751d2a8213ed969f16c1c1bb4e80e87e2da9e4dce0a8af73df26bf80c148edbfdbfd5bfdb4876ac090da6783a5304891110802b43a0d6251c7881a
7
+ data.tar.gz: 14b5a5d075ab12359ecda3d93ec1502c2b9b114376e81fbb2946fa9ff7fa1fbac93e091b90df8599280114735fedd3d2d201f5d607bcf3a2c1bd130e388a76c0
@@ -3,7 +3,7 @@ name: Lint
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - main
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@v3
16
- - name: Set up Ruby 3.2
15
+ - uses: actions/checkout@v4
16
+ - name: Set up latest Ruby
17
17
  uses: ruby/setup-ruby@v1
18
18
  with:
19
- ruby-version: 3.2
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
@@ -3,25 +3,33 @@ name: Test
3
3
  on:
4
4
  push:
5
5
  branches:
6
- - main
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@v3
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
@@ -8,6 +8,7 @@
8
8
  /coverage/
9
9
  /doc/
10
10
  /Gemfile.lock
11
+ /gemfiles/
11
12
  /pkg/
12
13
  /spec/reports/
13
14
  /tmp/
data/.rubocop.yml CHANGED
@@ -1,4 +1,4 @@
1
- require:
1
+ plugins:
2
2
  - rubocop-md
3
3
  - rubocop-minitest
4
4
  - rubocop-performance
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
  [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](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 v8, v11
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
- * OSX: `/Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home`
63
- * Ubuntu/CentOS: `/usr/lib/jvm/java-1.8.0-openjdk`
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
- * `num_fields`
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.num_fields} fields."
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/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.add_runtime_dependency 'rjb', '~> 1.6'
25
- spec.requirements << 'JDK 8.x - 11.x'
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
@@ -1,3 +1,3 @@
1
1
  class FillablePDF
2
- VERSION = '0.9.6'
2
+ VERSION = '1.0.0'
3
3
  end
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 IOError, "File <#{file_path}> is not found" unless File.exist?(file_path)
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.getFormFields
28
+ @form_fields = @pdf_form.getAllFormFields
26
29
  rescue StandardError => e
27
- raise "#{e.message} (Input file may be corrupt, incompatible, read-only, write-protected, encrypted, or may not have any form fields)" # rubocop:disable Layout/LineLength
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
- num_fields.positive?
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 num_fields
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
- # @return the value of the field
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 "unknown key name `#{key}'"
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
- # @return the type of the field
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.toString
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 the hash of field keys and values
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 [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
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
- pdf_field(key).setValue(value.to_s)
114
+ field.setValue(value.to_s)
98
115
  else
99
- pdf_field(key).setValue(value.to_s, generate_appearance)
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
- raise IOError, "File <#{file_path}> is not found" unless File.exist?(file_path)
114
- field = pdf_field(key)
115
- widgets = field.getWidgets
116
- widget_dict = suppress_warnings { widgets.isEmpty ? field.getPdfObject : widgets.get(0).getPdfObject }
117
- orig_rect = widget_dict.getAsRectangle(ITEXT::PdfName.Rect)
118
- border_width = field.getBorderWidth
119
- bounding_rectangle = ITEXT::Rectangle.new(
120
- orig_rect.getWidth - (border_width * 2),
121
- orig_rect.getHeight - (border_width * 2)
122
- )
123
-
124
- pdf_form_x_object = ITEXT::PdfFormXObject.new(bounding_rectangle)
125
- canvas = ITEXT::Canvas.new(pdf_form_x_object, @pdf_doc)
126
- image = ITEXT::Image.new(ITEXT::ImageDataFactory.create(file_path.to_s))
127
- .setAutoScale(true)
128
- .setHorizontalAlignment(ITEXT::HorizontalAlignment.CENTER)
129
- container = ITEXT::Div.new
130
- .setMargin(border_width).add(image)
131
- .setVerticalAlignment(ITEXT::VerticalAlignment.MIDDLE)
132
- .setFillAvailableArea(true)
133
- canvas.add(container)
134
- canvas.close
135
-
136
- pdf_dict = ITEXT::PdfDictionary.new
137
- widget_dict.put(ITEXT::PdfName.AP, pdf_dict)
138
- pdf_dict.put(ITEXT::PdfName.N, pdf_form_x_object.getPdfObject)
139
- widget_dict.setModified
140
- rescue StandardError => e
141
- raise "#{e.message} (there may be something wrong with your image)"
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|Symbol] base64_image_data base64 encoded data image
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
- File.binwrite(tmp_file, Base64.decode64(base64_image_data))
156
- set_image(key, tmp_file)
157
- ensure
158
- FileUtils.rm tmp_file
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 [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
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
- fields.each { |key, value| set_field key, value, generate_appearance: generate_appearance }
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
- pdf_field(old_key).setFieldName(new_key.to_s)
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 [bool] flatten true if PDF should be flattened, false otherwise
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 [TrueClass|FalseClass] flatten true if PDF should be flattened, false otherwise
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
- else
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, false otherwise
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
- @pdf_doc.isClosed
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 [TrueClass|FalseClass] flatten: true if PDF should be flattened, false otherwise
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 "unknown key name `#{key}'" if field.nil?
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.9.6
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: 2023-07-12 00:00:00.000000000 Z
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-7.2.4.jar
48
- - ext/font-asian-7.2.4.jar
49
- - ext/forms-7.2.4.jar
50
- - ext/io-7.2.4.jar
51
- - ext/kernel-7.2.4.jar
52
- - ext/layout-7.2.4.jar
53
- - ext/slf4j-api-2.0.4.jar
54
- - ext/slf4j-simple-2.0.4.jar
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.x - 11.x
86
- rubygems_version: 3.4.16
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: []
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
Binary file
Binary file