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 +4 -4
- data/README.md +23 -19
- data/lib/fillable-pdf/version.rb +1 -1
- data/lib/fillable-pdf.rb +52 -20
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9ef1adfa571c40a68f117e48e73917ef837207c8c2888a449672f2fa07664873
|
|
4
|
+
data.tar.gz: fb5c2c786c58b1635c37534381ae5ac106aaa0694e7839ec73acdaa331c863d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86ad2b2c634818d159e2f7b710d2d38c5e726eecf9c809e8b19466cc2f842f8af9633b0900004b0f98031741a2680f5aff2bbf40f4448161d0590a2141630192
|
|
7
|
+
data.tar.gz: e940b9d5908efb0fae7b7975b0ec7878b78876fccc07c5a08c9a7e1575fa38e4829ae0a41290a791ac6e8d0d21dd97024e14c65700baf3e1f01d14b0ba7f02c7
|
data/README.md
CHANGED
|
@@ -6,14 +6,12 @@
|
|
|
6
6
|
[](https://github.com/vkononov/fillable-pdf/blob/main/.github/workflows/test.yml)
|
|
7
7
|
[](https://github.com/vkononov/fillable-pdf/actions/workflows/test.yml)
|
|
8
8
|
[](https://github.com/vkononov/fillable-pdf/actions/workflows/lint.yml)
|
|
9
|
-
[](https://opensource.org/
|
|
9
|
+
[](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
|
-
[](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
|
-
* `
|
|
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 `
|
|
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 [
|
|
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(
|
|
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
|
|
537
|
+
The form has a total of 14 fields.
|
|
534
538
|
|
|
535
|
-
Fields hash: {:
|
|
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, :
|
|
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", "
|
|
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 '
|
|
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](
|
|
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/
|
|
565
|
+
However, you must also adhere to the [iText License](https://github.com/itext/itext-java) when using this gem in your project.
|
data/lib/fillable-pdf/version.rb
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
99
|
+
rubygems_version: 4.0.20
|
|
100
100
|
specification_version: 4
|
|
101
|
-
summary: Fill
|
|
101
|
+
summary: Fill, read, flatten and edit PDF form fields in Ruby with the iText engine
|
|
102
102
|
test_files: []
|