fillable-pdf 1.1.2 → 1.1.4

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: 6759e7cfdd5a0a97c20a9e98926b6c8e40d5a31b6543407e87cfd574806564d7
4
- data.tar.gz: 59cda9afff539dc1bfb3ef194e4cb6a83c5208d8287dbfc8ed35a15bd5788829
3
+ metadata.gz: 9ef1adfa571c40a68f117e48e73917ef837207c8c2888a449672f2fa07664873
4
+ data.tar.gz: fb5c2c786c58b1635c37534381ae5ac106aaa0694e7839ec73acdaa331c863d5
5
5
  SHA512:
6
- metadata.gz: c6f97c5a48a92f0606bd6121d99b2ce35f1fe7fffe5983ef50332a66b397242f628d306e1cc29d604c15f223747c0e5c5c7229d55e6da7cf0a0712911060c4d7
7
- data.tar.gz: 5a8a6633a6f63b9a931ed3e7964bdb05a3ca0902583fe4e31ce1c3ee7af1c8973e39e2aebfee9a35afe2cec8fc1fef4cd8ea379dd26d5ecbaefef8b16502e177
6
+ metadata.gz: 86ad2b2c634818d159e2f7b710d2d38c5e726eecf9c809e8b19466cc2f842f8af9633b0900004b0f98031741a2680f5aff2bbf40f4448161d0590a2141630192
7
+ data.tar.gz: e940b9d5908efb0fae7b7975b0ec7878b78876fccc07c5a08c9a7e1575fa38e4829ae0a41290a791ac6e8d0d21dd97024e14c65700baf3e1f01d14b0ba7f02c7
data/README.md CHANGED
@@ -6,14 +6,12 @@
6
6
  [![JDK](https://img.shields.io/badge/JDK-8%20to%2026-007396?logo=openjdk&logoColor=white)](https://github.com/vkononov/fillable-pdf/blob/main/.github/workflows/test.yml)
7
7
  [![Test Matrix](https://img.shields.io/github/actions/workflow/status/vkononov/fillable-pdf/test.yml?branch=main&label=Test%20Matrix&logo=github)](https://github.com/vkononov/fillable-pdf/actions/workflows/test.yml)
8
8
  [![Lint](https://img.shields.io/github/actions/workflow/status/vkononov/fillable-pdf/lint.yml?branch=main&label=Lint&logo=github)](https://github.com/vkononov/fillable-pdf/actions/workflows/lint.yml)
9
- [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
9
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/MIT)
10
10
 
11
11
  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.
12
12
 
13
13
  Supports **Ruby 2.4 → latest** and **JDK 8 → latest**, with the full Ruby/Java matrix tested daily in CI.
14
14
 
15
- [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/vkononov)
16
-
17
15
  ## Known Issues
18
16
 
19
17
  1. Phusion Passenger's [smart spawning](https://www.phusionpassenger.com/library/indepth/ruby/spawn_methods/#the-smart-spawning-method) is not supported. Please see [Deployment with Phusion Passenger + Nginx](#deployment-with-phusion-passenger--nginx) for more information.
@@ -231,7 +229,7 @@ An instance of `FillablePDF` has the following methods at its disposal:
231
229
  pdf.set_field('first_name', 'Richard', generate_appearance: false)
232
230
  ```
233
231
 
234
- * `def set_fields(fields, generate_appearance: nil)`
232
+ * `set_fields(fields, generate_appearance: nil)`
235
233
  *Sets the values of multiple fields given a set of unique field names and values, with an optional `generate_appearance` directive.*
236
234
 
237
235
  ```ruby
@@ -319,7 +317,7 @@ An instance of `FillablePDF` has the following methods at its disposal:
319
317
  # result: document is saved in a given path with flattening
320
318
  ```
321
319
 
322
- **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.
320
+ **NOTE:** Saving the file automatically closes the input file, so you would need to reinitialize the `FillablePDF` class before making any more changes or saving another copy.
323
321
 
324
322
  * `save_as!(file_path, flatten: false)`
325
323
  *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).*
@@ -454,15 +452,18 @@ Naturally, there are many downsides (in terms of efficiency, scalability, securi
454
452
 
455
453
  ## Example
456
454
 
457
- The following [example.rb](example/run.rb) with [input.pdf](example/input.pdf) is located in the [example](example) directory. It uses all of the methods that are described above and generates the output files [output.pdf](example/output.pdf) and [output.flat.pdf](example/output.flat.pdf).
455
+ The following [run.rb](example/run.rb) with [input.pdf](example/input.pdf) is located in the [example](example) directory. It uses most of the methods that are described above and generates the output files [output.pdf](example/output.pdf) and [output.flat.pdf](example/output.flat.pdf).
458
456
 
459
457
  ```ruby
460
458
  require_relative '../lib/fillable-pdf'
461
459
 
460
+ # Get the directory where this script is located
461
+ EXAMPLE_DIR = __dir__
462
+
462
463
  BASE64_PHOTO = 'iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==' # rubocop:disable Layout/LineLength
463
464
 
464
465
  # opening a fillable PDF
465
- pdf = FillablePDF.new('input.pdf')
466
+ pdf = FillablePDF.new(File.join(EXAMPLE_DIR, 'input.pdf'))
466
467
 
467
468
  # total number of fields
468
469
  if pdf.any_fields?
@@ -475,12 +476,15 @@ puts
475
476
 
476
477
  # setting form fields
477
478
  pdf.set_fields({first_name: 'Richard', last_name: 'Rahl'})
478
- pdf.set_fields({football: 'Yes', baseball: 'Yes', basketball: 'Yes', nascar: 'Yes', hockey: 'Yes', rugby: 'Yes'}, generate_appearance: false)
479
+ pdf.set_fields(
480
+ {football: 'Yes', baseball: 'Yes', basketball: 'Yes', nascar: 'Yes', hockey: 'Yes', rugby: 'Yes'},
481
+ generate_appearance: false
482
+ )
479
483
  pdf.set_field(:date, Time.now.strftime('%B %e, %Y'))
480
484
  pdf.set_field(:newsletter, 'Off') # uncheck the checkbox
481
485
  pdf.set_field(:language, 'dart') # select a radio button option
482
486
  pdf.set_image_base64(:photo, BASE64_PHOTO)
483
- pdf.set_image(:signature, 'signature.png')
487
+ pdf.set_image(:signature, File.join(EXAMPLE_DIR, 'signature.png'))
484
488
 
485
489
  # list of fields
486
490
  puts "Fields hash: #{pdf.fields}"
@@ -517,11 +521,11 @@ pdf.remove_field :marketing
517
521
  puts "Removed field 'marketing'"
518
522
 
519
523
  # saving the filled out PDF in another file
520
- pdf.save_as('output.pdf')
524
+ pdf.save_as(File.join(EXAMPLE_DIR, 'output.pdf'))
521
525
 
522
526
  # saving another copy of the filled out PDF in another file and making it non-editable
523
- pdf = FillablePDF.new('output.pdf')
524
- pdf.save_as 'output.flat.pdf', flatten: true
527
+ pdf = FillablePDF.new(File.join(EXAMPLE_DIR, 'output.pdf'))
528
+ pdf.save_as File.join(EXAMPLE_DIR, 'output.flat.pdf'), flatten: true
525
529
 
526
530
  # closing the document
527
531
  pdf.close
@@ -530,19 +534,19 @@ pdf.close
530
534
  The example above produces the following output and also generates the output file [output.pdf](example/output.pdf).
531
535
 
532
536
  ```text
533
- The form has a total of 16 fields.
537
+ The form has a total of 14 fields.
534
538
 
535
- Fields hash: {:last_name=>"Rahl", :first_name=>"Richard", :football=>"Yes", :baseball=>"Yes", :basketball=>"Yes", :hockey=>"Yes", :date=>"November 16, 2021", :newsletter=>"Off", :nascar=>"Yes", :language=>"dart", :"language.1"=>"dart", :"language.2"=>"dart", :"language.3"=>"dart", :"language.4"=>"dart", :signature=>"", :photo=>""}
539
+ Fields hash: {last_name: "Rahl", first_name: "Richard", football: "Yes", baseball: "Yes", basketball: "Yes", date: "August 12, 2026", newsletter: "Off", nascar: "Yes", language: "dart", signature: "", photo: "", rugby: "Yes", hockey: "Yes", marketing: ""}
536
540
 
537
- Keys: [:last_name, :first_name, :football, :baseball, :basketball, :hockey, :date, :newsletter, :nascar, :language, :"language.1", :"language.2", :"language.3", :"language.4", :signature, :photo]
541
+ Keys: [:last_name, :first_name, :football, :baseball, :basketball, :date, :newsletter, :nascar, :language, :signature, :photo, :rugby, :hockey, :marketing]
538
542
 
539
- Values: ["Rahl", "Richard", "Yes", "Yes", "Yes", "Yes", "November 16, 2021", "Off", "Yes", "dart", "dart", "dart", "dart", "dart", "", ""]
543
+ Values: ["Rahl", "Richard", "Yes", "Yes", "Yes", "August 12, 2026", "Off", "Yes", "dart", "", "", "Yes", "Yes", ""]
540
544
 
541
545
  Field 'football' is of type BUTTON
542
546
 
543
547
  Renamed field 'last_name' to 'surname'
544
548
 
545
- Removed field 'nascar'
549
+ Removed field 'marketing'
546
550
  ```
547
551
 
548
552
  ## Contributing
@@ -556,6 +560,6 @@ Removed field 'nascar'
556
560
 
557
561
  ## License
558
562
 
559
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
563
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/MIT).
560
564
 
561
- However, you must also adhere to the [iText License](https://github.com/itext/itext7) when using this gem in your project.
565
+ However, you must also adhere to the [iText License](https://github.com/itext/itext-java) when using this gem in your project.
@@ -1,3 +1,3 @@
1
1
  class FillablePDF
2
- VERSION = '1.1.2'
2
+ VERSION = '1.1.4'
3
3
  end
data/lib/fillable-pdf.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'fillable-pdf/errors'
5
5
  require 'base64'
6
6
  require 'securerandom'
7
7
  require 'tmpdir'
8
+ require 'tempfile'
8
9
 
9
10
  class FillablePDF # rubocop:disable Metrics/ClassLength
10
11
  include SuppressWarnings
@@ -128,7 +129,7 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
128
129
  # @param [String|Symbol] key the field name
129
130
  # @param [String|Symbol] file_path the name of the image file or image path
130
131
  # @return [self] returns self for method chaining
131
- # @raise [FileOperationError] if the image file is not found
132
+ # @raise [FileOperationError] if the image file is not found or the field has no visible area
132
133
  # @raise [FieldNotFoundError] if the field does not exist
133
134
  #
134
135
  def set_image(key, file_path) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
@@ -136,13 +137,7 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
136
137
  raise FileOperationError, "File <#{file_path}> is not found" unless File.exist?(file_path)
137
138
 
138
139
  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
140
+ widget_dict, orig_rect, border_width = image_placement(key)
146
141
 
147
142
  bounding_rectangle = ITEXT::Rectangle.new(
148
143
  orig_rect.getWidth - (border_width * 2),
@@ -165,6 +160,8 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
165
160
  widget_dict.put(ITEXT::PdfName.AP, pdf_dict)
166
161
  pdf_dict.put(ITEXT::PdfName.N, pdf_form_x_object.getPdfObject)
167
162
  widget_dict.setModified
163
+ rescue FileOperationError
164
+ raise
168
165
  rescue StandardError => e
169
166
  raise FileOperationError, "Failed to set image for field '#{key}': #{e.message}"
170
167
  end
@@ -299,15 +296,15 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
299
296
  #
300
297
  def save(flatten: false)
301
298
  ensure_document_open
302
- tmp_file = "#{Dir.tmpdir}/#{SecureRandom.uuid}"
303
- save_as(tmp_file, flatten: flatten)
304
- FileUtils.mv tmp_file, @file_path
299
+ write_document(@file_path, flatten: flatten)
305
300
  self
301
+ rescue StandardError => e
302
+ raise FileOperationError, "Failed to save file `#{@file_path}`: #{e.message}"
306
303
  end
307
304
 
308
305
  ##
309
306
  # 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.
307
+ # The destination is replaced only after the document has been finalized.
311
308
  #
312
309
  # @param [String] file_path the name of the PDF file or file path
313
310
  # @param [Boolean] flatten true if PDF should be flattened, false otherwise
@@ -316,12 +313,7 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
316
313
  #
317
314
  def save_as(file_path, flatten: false)
318
315
  ensure_document_open
319
- if @file_path == file_path
320
- save(flatten: flatten)
321
- return self
322
- end
323
-
324
- File.open(file_path, 'wb') { |f| f.write(finalize(flatten: flatten)) && f.close }
316
+ write_document(file_path, flatten: flatten)
325
317
  self
326
318
  rescue StandardError => e
327
319
  raise FileOperationError, "Failed to save file `#{file_path}`: #{e.message}"
@@ -339,9 +331,12 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
339
331
  #
340
332
  def save_as!(file_path, flatten: false)
341
333
  ensure_document_open
342
- raise InvalidArgumentError, 'Cannot save_as! to the same file path. Use save() instead.' if @file_path == file_path
334
+ if same_source_file?(file_path)
335
+ raise InvalidArgumentError,
336
+ 'Cannot save_as! to the same file path. Use save() instead.'
337
+ end
343
338
 
344
- File.open(file_path, 'wb') { |f| f.write(finalize(flatten: flatten)) && f.close }
339
+ write_document(file_path, flatten: flatten)
345
340
  self
346
341
  rescue InvalidArgumentError
347
342
  raise
@@ -373,6 +368,29 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
373
368
 
374
369
  private
375
370
 
371
+ def same_source_file?(file_path)
372
+ File.expand_path(@file_path.to_s) == File.expand_path(file_path.to_s) ||
373
+ (File.exist?(file_path) && File.identical?(@file_path, file_path))
374
+ end
375
+
376
+ # Keep the existing destination intact until finalization and writing succeed.
377
+ # Resolve symlinks so saving through one preserves the link itself.
378
+ def write_document(file_path, flatten:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
379
+ destination = File.expand_path(file_path.to_s)
380
+ destination = File.realpath(destination) if File.symlink?(destination)
381
+ mode = File.stat(destination).mode & 0o777 if File.exist?(destination)
382
+ temporary = Tempfile.new(['.fillable-pdf-', '.pdf'], File.dirname(destination))
383
+ temporary.binmode
384
+ temporary.write(finalize(flatten: flatten))
385
+ temporary.flush
386
+ temporary.fsync
387
+ temporary.chmod(mode) if mode
388
+ temporary.close
389
+ File.rename(temporary.path, destination)
390
+ ensure
391
+ temporary&.close!
392
+ end
393
+
376
394
  ##
377
395
  # Writes the contents of the modified fields to the previously opened PDF file.
378
396
  #
@@ -393,6 +411,20 @@ class FillablePDF # rubocop:disable Metrics/ClassLength
393
411
  field
394
412
  end
395
413
 
414
+ # Dictionary, rectangle and border width of the field's first widget, falling
415
+ # back to the field itself when it has no widget annotations.
416
+ def image_placement(key)
417
+ field = pdf_field(key)
418
+ widgets = field.getWidgets
419
+ widget = widgets.isEmpty ? nil : widgets.get(0)
420
+ widget_dict = suppress_warnings { (widget || field).getPdfObject }
421
+ rect = widget_dict.getAsRectangle(ITEXT::PdfName.Rect)
422
+ raise FileOperationError, "Field '#{key}' has no visible area to place an image in" if rect.nil?
423
+
424
+ border_style = widget&.getBorderStyle
425
+ [widget_dict, rect, border_style.nil? ? 0 : border_style.getWidth]
426
+ end
427
+
396
428
  def validate_input(key, value)
397
429
  validate_field_name(key)
398
430
  raise InvalidArgumentError, 'Field value cannot be nil' if value.nil?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fillable-pdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vadim Kononov
@@ -51,9 +51,9 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '1.6'
54
- description: FillablePDF is an extremely simple and lightweight utility that bridges
55
- iText and Ruby in order to fill out fillable PDF forms or extract field values from
56
- previously filled out PDF forms.
54
+ description: Ruby wrapper for iText to work with AcroForm PDFs. Fill and read text
55
+ fields, checkboxes and radio buttons, add images to fields, rename or remove fields,
56
+ and flatten forms.
57
57
  email:
58
58
  - vadim@konoson.com
59
59
  executables: []
@@ -96,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
96
  version: '0'
97
97
  requirements:
98
98
  - JDK >= 8
99
- rubygems_version: 4.0.10
99
+ rubygems_version: 4.0.20
100
100
  specification_version: 4
101
- summary: Fill out or extract field values from simple fillable PDF forms using iText.
101
+ summary: Fill, read, flatten and edit PDF form fields in Ruby with the iText engine
102
102
  test_files: []