hexapdf 0.40.0 → 0.42.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/CHANGELOG.md +71 -0
- data/examples/019-acro_form.rb +12 -23
- data/examples/027-composer_optional_content.rb +1 -1
- data/examples/030-pdfa.rb +6 -6
- data/examples/031-acro_form_java_script.rb +113 -0
- data/lib/hexapdf/cli/command.rb +25 -11
- data/lib/hexapdf/cli/files.rb +31 -7
- data/lib/hexapdf/cli/form.rb +46 -38
- data/lib/hexapdf/cli/info.rb +4 -0
- data/lib/hexapdf/cli/inspect.rb +1 -1
- data/lib/hexapdf/cli/usage.rb +215 -0
- data/lib/hexapdf/cli.rb +2 -0
- data/lib/hexapdf/configuration.rb +11 -1
- data/lib/hexapdf/content/canvas.rb +2 -0
- data/lib/hexapdf/document/layout.rb +8 -1
- data/lib/hexapdf/encryption/aes.rb +13 -6
- data/lib/hexapdf/encryption/security_handler.rb +6 -4
- data/lib/hexapdf/font/cmap/parser.rb +1 -5
- data/lib/hexapdf/font/cmap.rb +22 -3
- data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
- data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
- data/lib/hexapdf/font_loader.rb +1 -0
- data/lib/hexapdf/layout/style.rb +5 -4
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
- data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
- data/lib/hexapdf/type/acro_form/field.rb +14 -0
- data/lib/hexapdf/type/acro_form/form.rb +70 -8
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/annotations/widget.rb +1 -1
- data/lib/hexapdf/type/resources.rb +2 -1
- data/lib/hexapdf/utils.rb +19 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +18 -8
- data/test/hexapdf/encryption/test_security_handler.rb +17 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
- data/test/hexapdf/font/cmap/test_parser.rb +5 -3
- data/test/hexapdf/font/test_cmap.rb +8 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
- data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
- data/test/hexapdf/test_utils.rb +16 -0
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
- data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_field.rb +11 -0
- data/test/hexapdf/type/acro_form/test_form.rb +80 -0
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
- data/test/hexapdf/type/test_resources.rb +5 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd69db2c04dc802784438f9038c9a6ab32dac0806edca43b6c59406bba9d7ea6
|
4
|
+
data.tar.gz: a51a510a6a98b547b2b11fb07804ca9039a666adf95b52f65db9b070c3ffe9c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 714cba0b758960066498413b545dc7cf2806e1e483f99b017958450bfe29fd12aac1eef40f670d189f7cd7dab5784dd83e7b6f95533b97cf71c883dd9f307485
|
7
|
+
data.tar.gz: c93ceed7393b57fa5c6ca83141a8307b6015a1fa6907886b52c57bf72384d4d23ac8356adede0dabc35578cad5e1cef074f75c94e01e5c9532cadd0a688bbd57
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,74 @@
|
|
1
|
+
## 0.42.0 - 2024-05-12
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* Support for the `AFPercent_Format` JavaScript method
|
6
|
+
* Support for the `AFTime_Format` JavaScript method
|
7
|
+
* [HexaPDF::Type::AcroForm::Form#fill] for easily filling out form fields
|
8
|
+
* CLI command `hexapdf usage` for showing space usage information
|
9
|
+
* Support for attaching files via `hexapdf files` CLI command
|
10
|
+
* Refinement on [HexaPDF::Utils] to support conversion of Numeric values to
|
11
|
+
points (e.g. `5.mm`, `5.cm`, `5.inch`)
|
12
|
+
|
13
|
+
### Changed
|
14
|
+
|
15
|
+
* [HexaPDF::Type::AcroForm::ButtonField#field_value=] to always allow using
|
16
|
+
`true` for check boxes
|
17
|
+
* CLI commands to prompt whether an existing output file should be overwritten
|
18
|
+
|
19
|
+
### Fixed
|
20
|
+
|
21
|
+
* [HexaPDF::Type::Resources#font] to always return a correctly wrapped font
|
22
|
+
object
|
23
|
+
* [HexaPDF::Type::AcroForm::TextField#field_value=] to actually use the value
|
24
|
+
returned by the call to the config option 'acro_form.on_invalid_value'
|
25
|
+
|
26
|
+
|
27
|
+
## 0.41.0 - 2024-05-05
|
28
|
+
|
29
|
+
### Added
|
30
|
+
|
31
|
+
* Font loader [HexaPDF::FontLoader::VariantFromName] to ease specifying font
|
32
|
+
variants
|
33
|
+
* [HexaPDF::Type::AcroForm::JavaScriptActions] module to contain all JavaScript
|
34
|
+
actions that HexaPDF can handle
|
35
|
+
* Support for the `AFSimple_Calculate` Javascript method
|
36
|
+
* Support for Simplified Field Notation for defining Javascript calculations
|
37
|
+
* Configuration option 'encryption.on_decryption_error' to allow custom
|
38
|
+
decryption error handling
|
39
|
+
* CLI option `--fill-read-only-fields` to `hexapdf form` to specify whether
|
40
|
+
filling in read only fields is allowed
|
41
|
+
* [HexaPDF::Type::AcroForm::Field#form_field] to getting the field irrespective
|
42
|
+
of whether the object is already a field or a widget
|
43
|
+
* [HexaPDF::Type::AcroForm::TextField#set_format_action] for setting a
|
44
|
+
JavaScript action that formats the field's value
|
45
|
+
* [HexaPDF::Type::AcroForm::TextField#set_calculate_action] for setting a
|
46
|
+
JavaScript action that calculates the field's value
|
47
|
+
* [HexaPDF::Type::AcroForm#recalculate_fields] for recalculating fields
|
48
|
+
|
49
|
+
### Changed
|
50
|
+
|
51
|
+
* CLI command `hexapdf form` to show more information in verbose mode
|
52
|
+
* CLI command 'hexapdf form' to show the field flags "read only" and "required"
|
53
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to remove the hidden flag from
|
54
|
+
widgets
|
55
|
+
|
56
|
+
### Fixed
|
57
|
+
|
58
|
+
* [HexaPDF::FontLoader::FromConfiguration] to accept arbitrary keyword arguments
|
59
|
+
* [HexaPDF::Font::CMap::Parser] to avoid instantiating invalid UTF-16BE chars
|
60
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to work for files where check
|
61
|
+
boxes don't have appearance subdictionaries
|
62
|
+
* [HexaPDF::Type::AcroForm::TextField#field_value=] to call the config option
|
63
|
+
'acro_form.on_invalid_value' when passing a non-String argument (except `nil`)
|
64
|
+
* [HexaPDF::Type::AcroForm::JavaScriptActions#apply_af_number_format] to
|
65
|
+
correctly convert strings using commas or points into numbers
|
66
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to use the field instead of the
|
67
|
+
widget object as the source for JavaScript format actions
|
68
|
+
* CLI command `hexapdf form --generate-template` to output fields without values
|
69
|
+
* `AFNumber_Format` JavaScript parsing to work without trailing semicolon
|
70
|
+
|
71
|
+
|
1
72
|
## 0.40.0 - 2024-03-23
|
2
73
|
|
3
74
|
### Changed
|
data/examples/019-acro_form.rb
CHANGED
@@ -6,9 +6,6 @@
|
|
6
6
|
# This example show-cases how to create the various form field types and their
|
7
7
|
# possible standard appearances.
|
8
8
|
#
|
9
|
-
# Note the 'number format' text field which uses a JavaScript function for
|
10
|
-
# formatting a number.
|
11
|
-
#
|
12
9
|
# Usage:
|
13
10
|
# : `ruby acro_form.rb`
|
14
11
|
#
|
@@ -52,46 +49,38 @@ tx = form.create_text_field("Single Line", font_size: 16)
|
|
52
49
|
widget = tx.create_widget(page, Rect: [200, 445, 500, 465])
|
53
50
|
tx.field_value = "A sample test string!"
|
54
51
|
|
55
|
-
canvas.text("
|
56
|
-
tx = form.create_text_field("Number format", font_size: 16)
|
57
|
-
widget = tx.create_widget(page, Rect: [200, 415, 500, 435])
|
58
|
-
widget[:AA] = {
|
59
|
-
F: {S: :JavaScript, JS: 'AFNumber_Format(2, 2, 0, 0, "EUR ", true);'},
|
60
|
-
}
|
61
|
-
tx.field_value = "123456,789"
|
62
|
-
|
63
|
-
canvas.text("Multiline", at: [70, 390])
|
52
|
+
canvas.text("Multiline", at: [70, 420])
|
64
53
|
tx = form.create_multiline_text_field("Multiline", font_size: 0, align: :right)
|
65
|
-
widget = tx.create_widget(page, Rect: [200,
|
54
|
+
widget = tx.create_widget(page, Rect: [200, 355, 500, 435])
|
66
55
|
widget.border_style(color: 0, width: 1)
|
67
56
|
tx.field_value = "A sample test string! " * 30 + "\nNew line\n\nAnother line"
|
68
57
|
|
69
|
-
canvas.text("Password", at: [70,
|
58
|
+
canvas.text("Password", at: [70, 330])
|
70
59
|
tx = form.create_password_field("Password", font_size: 16)
|
71
|
-
widget = tx.create_widget(page, Rect: [200,
|
60
|
+
widget = tx.create_widget(page, Rect: [200, 325, 500, 345])
|
72
61
|
|
73
|
-
canvas.text("File select", at: [70,
|
62
|
+
canvas.text("File select", at: [70, 300])
|
74
63
|
tx = form.create_file_select_field("File Select", font_size: 16)
|
75
|
-
widget = tx.create_widget(page, Rect: [200,
|
64
|
+
widget = tx.create_widget(page, Rect: [200, 295, 500, 315])
|
76
65
|
tx.field_value = "path/to/file.pdf"
|
77
66
|
|
78
|
-
canvas.text("Comb", at: [70,
|
67
|
+
canvas.text("Comb", at: [70, 270])
|
79
68
|
tx = form.create_comb_text_field("Comb field", max_chars: 10, font_size: 16, align: :center)
|
80
|
-
widget = tx.create_widget(page, Rect: [200,
|
69
|
+
widget = tx.create_widget(page, Rect: [200, 250, 500, 285])
|
81
70
|
widget.border_style(color: [30, 128, 0], width: 1)
|
82
71
|
tx.field_value = 'Hello'
|
83
72
|
|
84
|
-
canvas.text("Combo Box", at: [50,
|
73
|
+
canvas.text("Combo Box", at: [50, 200])
|
85
74
|
cb = form.create_combo_box("Combo Box", font_size: 12, editable: true,
|
86
75
|
option_items: ['Value 1', 'Another value', 'Choose me!'])
|
87
|
-
widget = cb.create_widget(page, Rect: [200,
|
76
|
+
widget = cb.create_widget(page, Rect: [200, 180, 500, 215])
|
88
77
|
widget.border_style(width: 1)
|
89
78
|
cb.field_value = 'Another value'
|
90
79
|
|
91
|
-
canvas.text("List Box", at: [50,
|
80
|
+
canvas.text("List Box", at: [50, 150])
|
92
81
|
lb = form.create_list_box("List Box", font_size: 15, align: :center, multi_select: true,
|
93
82
|
option_items: 1.upto(7).map {|i| "Value #{i}" })
|
94
|
-
widget = lb.create_widget(page, Rect: [200,
|
83
|
+
widget = lb.create_widget(page, Rect: [200, 80, 500, 165])
|
95
84
|
widget.border_style(width: 1)
|
96
85
|
lb.list_box_top_index = 1
|
97
86
|
lb.field_value = ['Value 6', 'Value 2']
|
@@ -28,7 +28,7 @@ HexaPDF::Composer.create('composer_optional_content.pdf') do |composer|
|
|
28
28
|
a3m = composer.document.optional_content.create_ocmd([a3, all], policy: :any_on)
|
29
29
|
|
30
30
|
composer.text('The Great Ruby Quiz', text_align: :center, margin: [0, 0, 24],
|
31
|
-
font:
|
31
|
+
font: 'Helvetica bold', font_size: 24)
|
32
32
|
|
33
33
|
composer.list(marker_type: :decimal, item_spacing: 32, style: :question) do |listing|
|
34
34
|
listing.multiple do |item|
|
data/examples/030-pdfa.rb
CHANGED
@@ -30,18 +30,18 @@ HexaPDF::Composer.create('pdfa.pdf') do |composer|
|
|
30
30
|
composer.style(:base, font: 'Lato', font_size: 10, line_spacing: 1.3)
|
31
31
|
composer.style(:top, font_size: 8)
|
32
32
|
composer.style(:top_box, padding: [100, 0, 0], margin: [0, 0, 10], border: {width: [0, 0, 1]})
|
33
|
-
composer.style(:header, font:
|
33
|
+
composer.style(:header, font: 'Lato bold', font_size: 20, margin: [50, 0, 20])
|
34
34
|
composer.style(:line_items, border: {width: 1, color: "eee"}, margin: [20, 0])
|
35
35
|
composer.style(:line_item_cell, font_size: 8)
|
36
36
|
composer.style(:footer, border: {width: [1, 0, 0], color: "darkgrey"},
|
37
37
|
padding: [5, 0, 0], valign: :bottom)
|
38
|
-
composer.style(:footer_heading, font:
|
38
|
+
composer.style(:footer_heading, font: 'Lato bold',
|
39
39
|
font_size: 8, padding: [0, 0, 8])
|
40
40
|
composer.style(:footer_text, font_size: 8, fill_color: "darkgrey")
|
41
41
|
|
42
42
|
# Top part
|
43
43
|
composer.box(:container, style: :top_box) do |container|
|
44
|
-
container.formatted_text([{text: company[:name], font:
|
44
|
+
container.formatted_text([{text: company[:name], font: 'Lato bold'},
|
45
45
|
" - " + company[:address].join(' - ')], style: :top)
|
46
46
|
end
|
47
47
|
composer.text("Mega Client\nSmall Lane 5\n67890 Noonestown", mask_mode: :box)
|
@@ -50,7 +50,7 @@ HexaPDF::Composer.create('pdfa.pdf') do |composer|
|
|
50
50
|
["Service date:", "2024-02-01"]]
|
51
51
|
composer.table(cells, column_widths: [150, 80], style: {align: :right}) do |args|
|
52
52
|
args[] = {cell: {border: {width: 0}, padding: 2}, text_align: :right}
|
53
|
-
args[0..-1, 0] = {font:
|
53
|
+
args[0..-1, 0] = {font: 'Lato bold'}
|
54
54
|
end
|
55
55
|
|
56
56
|
# Middle part
|
@@ -65,9 +65,9 @@ HexaPDF::Composer.create('pdfa.pdf') do |composer|
|
|
65
65
|
cells << [nil, nil, nil, "€ #{250 * max * (max + 1) / 2},00"]
|
66
66
|
composer.table(cells, column_widths: [250, 80], style: :line_items) do |args|
|
67
67
|
args[] = {cell: {border: {width: 0}, padding: 8}, style: :line_item_cell}
|
68
|
-
args[0] = {cell: {background_color: "eee"}, font:
|
68
|
+
args[0] = {cell: {background_color: "eee"}, font: "Lato bold"}
|
69
69
|
args[-1] = {cell: {background_color: "eee", border: {width: [2, 0, 0]}},
|
70
|
-
font:
|
70
|
+
font: "Lato bold"}
|
71
71
|
args[0..-1, 1..-1] = {text_align: :right}
|
72
72
|
end
|
73
73
|
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# # PDF Forms - JavaScript Actions
|
2
|
+
#
|
3
|
+
# Interactive PDF forms can contain JavaScript to enhance the form. For example,
|
4
|
+
# it is possible to use JavaScript to format numbers or to calculate a field
|
5
|
+
# value based on the value of other fields.
|
6
|
+
#
|
7
|
+
# While HexaPDF doesn't support all kinds of JavaScript actions, it supports
|
8
|
+
# select field format actions as well as the two most common calculate actions.
|
9
|
+
#
|
10
|
+
# Usage:
|
11
|
+
# : `ruby acro_form.rb`
|
12
|
+
#
|
13
|
+
|
14
|
+
require 'hexapdf'
|
15
|
+
|
16
|
+
doc = HexaPDF::Document.new
|
17
|
+
page = doc.pages.add
|
18
|
+
canvas = page.canvas
|
19
|
+
|
20
|
+
canvas.font("Helvetica", size: 36)
|
21
|
+
canvas.text("AcroForm JavaScript Actions ", at: [50, 750])
|
22
|
+
form = doc.acro_form(create: true)
|
23
|
+
|
24
|
+
canvas.font_size(16)
|
25
|
+
|
26
|
+
canvas.text("Value format actions", at: [50, 650])
|
27
|
+
|
28
|
+
canvas.text("Number format", at: [70, 620])
|
29
|
+
tx = form.create_text_field("Number_Format", font_size: 16)
|
30
|
+
tx.set_format_action(:number, decimals: 2, separator_style: :comma)
|
31
|
+
widget = tx.create_widget(page, Rect: [200, 615, 500, 635])
|
32
|
+
tx.field_value = "1234567.898"
|
33
|
+
|
34
|
+
canvas.text("Percent format", at: [70, 590])
|
35
|
+
tx = form.create_text_field("Percent_Format", font_size: 16)
|
36
|
+
tx.set_format_action(:percent, decimals: 2, separator_style: :comma)
|
37
|
+
widget = tx.create_widget(page, Rect: [200, 585, 500, 605])
|
38
|
+
tx.field_value = "12,45678"
|
39
|
+
|
40
|
+
canvas.text("Time format", at: [70, 560])
|
41
|
+
tx = form.create_text_field("Time_Format", font_size: 16)
|
42
|
+
tx.set_format_action(:time, format: :hh_mm_ss)
|
43
|
+
widget = tx.create_widget(page, Rect: [200, 555, 500, 575])
|
44
|
+
tx.field_value = "3:15:20 pm"
|
45
|
+
|
46
|
+
canvas.text("Calculate actions", at: [50, 510])
|
47
|
+
|
48
|
+
canvas.text("Source fields", at: [70, 480])
|
49
|
+
canvas.text("a:", at: [200, 480])
|
50
|
+
tx = form.create_text_field("a", font_size: 16)
|
51
|
+
tx.set_format_action(:number, decimals: 2)
|
52
|
+
widget = tx.create_widget(page, Rect: [220, 475, 280, 495])
|
53
|
+
tx.field_value = "10,50"
|
54
|
+
canvas.text("b:", at: [310, 480])
|
55
|
+
tx = form.create_text_field("b", font_size: 16)
|
56
|
+
tx.set_format_action(:number, decimals: 2)
|
57
|
+
widget = tx.create_widget(page, Rect: [330, 475, 390, 495])
|
58
|
+
tx.field_value = "20,60"
|
59
|
+
canvas.text("c:", at: [420, 480])
|
60
|
+
tx = form.create_text_field("c", font_size: 16)
|
61
|
+
tx.set_format_action(:number, decimals: 2)
|
62
|
+
widget = tx.create_widget(page, Rect: [440, 475, 500, 495])
|
63
|
+
tx.field_value = "30,70"
|
64
|
+
|
65
|
+
canvas.text("Predefined", at: [70, 450])
|
66
|
+
canvas.text("Sum", at: [90, 420])
|
67
|
+
tx = form.create_text_field("sum", font_size: 16)
|
68
|
+
tx.set_format_action(:number, decimals: 2)
|
69
|
+
tx.set_calculate_action(:sum, fields: ['a', 'b', 'c'])
|
70
|
+
tx.flag(:read_only)
|
71
|
+
widget = tx.create_widget(page, Rect: [310, 415, 500, 435])
|
72
|
+
canvas.text("Average", at: [90, 390])
|
73
|
+
tx = form.create_text_field("average", font_size: 16)
|
74
|
+
tx.set_format_action(:number, decimals: 2)
|
75
|
+
tx.set_calculate_action(:average, fields: ['a', 'b', 'c'])
|
76
|
+
tx.flag(:read_only)
|
77
|
+
widget = tx.create_widget(page, Rect: [310, 385, 500, 405])
|
78
|
+
canvas.text("Product", at: [90, 360])
|
79
|
+
tx = form.create_text_field("product", font_size: 16)
|
80
|
+
tx.set_format_action(:number, decimals: 2)
|
81
|
+
tx.set_calculate_action(:product, fields: ['a', 'b', 'c'])
|
82
|
+
tx.flag(:read_only)
|
83
|
+
widget = tx.create_widget(page, Rect: [310, 355, 500, 375])
|
84
|
+
canvas.text("Minimum", at: [90, 330])
|
85
|
+
tx = form.create_text_field("min", font_size: 16)
|
86
|
+
tx.set_format_action(:number, decimals: 2)
|
87
|
+
tx.set_calculate_action(:min, fields: ['a', 'b', 'c'])
|
88
|
+
tx.flag(:read_only)
|
89
|
+
widget = tx.create_widget(page, Rect: [310, 325, 500, 345])
|
90
|
+
canvas.text("Maximum", at: [90, 300])
|
91
|
+
tx = form.create_text_field("max", font_size: 16)
|
92
|
+
tx.set_format_action(:number, decimals: 2)
|
93
|
+
tx.set_calculate_action(:max, fields: ['a', 'b', 'c'])
|
94
|
+
tx.flag(:read_only)
|
95
|
+
widget = tx.create_widget(page, Rect: [310, 295, 500, 315])
|
96
|
+
|
97
|
+
canvas.text("Simplified Field Notation", at: [70, 270])
|
98
|
+
canvas.text("a + b + c", at: [90, 240])
|
99
|
+
tx = form.create_text_field("sfn1", font_size: 16)
|
100
|
+
tx.set_format_action(:number, decimals: 2)
|
101
|
+
tx.set_calculate_action(:sfn, fields: "a + b + c")
|
102
|
+
tx.flag(:read_only)
|
103
|
+
widget = tx.create_widget(page, Rect: [310, 235, 500, 255])
|
104
|
+
canvas.text("(a + b)*(c - a) / b + 3.14", at: [90, 210])
|
105
|
+
tx = form.create_text_field("sfn2", font_size: 16)
|
106
|
+
tx.set_format_action(:number, decimals: 2)
|
107
|
+
tx.set_calculate_action(:sfn, fields: "(a + b)*(c - a) / b + 3.14")
|
108
|
+
tx.flag(:read_only)
|
109
|
+
widget = tx.create_widget(page, Rect: [310, 205, 500, 225])
|
110
|
+
|
111
|
+
form.recalculate_fields
|
112
|
+
|
113
|
+
doc.write('acro_form_java_script.pdf', optimize: true)
|
data/lib/hexapdf/cli/command.rb
CHANGED
@@ -53,7 +53,7 @@ module HexaPDF
|
|
53
53
|
module Extensions #:nodoc:
|
54
54
|
def help_banner #:nodoc:
|
55
55
|
"hexapdf #{HexaPDF::VERSION} - Versatile PDF Manipulation Tool\n" \
|
56
|
-
"Copyright (c) 2014-
|
56
|
+
"Copyright (c) 2014-2024 Thomas Leitner; licensed under the AGPLv3\n\n" \
|
57
57
|
"#{format(usage, indent: 7)}\n\n"
|
58
58
|
end
|
59
59
|
|
@@ -134,6 +134,17 @@ module HexaPDF
|
|
134
134
|
false
|
135
135
|
end
|
136
136
|
end
|
137
|
+
hash[:config]['encryption.on_decryption_error'] =
|
138
|
+
if command_parser.strict
|
139
|
+
proc { true }
|
140
|
+
else
|
141
|
+
proc do |obj, msg|
|
142
|
+
if command_parser.verbosity_info?
|
143
|
+
$stderr.puts "Ignored decryption problem for object (#{ob.oid},#{obj.gen}): #{msg}"
|
144
|
+
end
|
145
|
+
false
|
146
|
+
end
|
147
|
+
end
|
137
148
|
hash
|
138
149
|
end
|
139
150
|
|
@@ -156,12 +167,12 @@ module HexaPDF
|
|
156
167
|
end
|
157
168
|
end
|
158
169
|
|
159
|
-
# Checks whether the given output file exists and
|
160
|
-
# HexaPDF::CLI#force is
|
170
|
+
# Checks whether the given output file exists and ask whether to overwrite the output file if
|
171
|
+
# it does. If HexaPDF::CLI#force is set, a possibly existing output file is always overwritten.
|
161
172
|
def maybe_raise_on_existing_file(filename)
|
162
173
|
if !command_parser.force && File.exist?(filename)
|
163
|
-
|
164
|
-
|
174
|
+
response = read_from_console("Output file '#{filename}' already exists - overwrite? (y/n)")
|
175
|
+
exit(1) unless response =~ /y/i
|
165
176
|
end
|
166
177
|
end
|
167
178
|
|
@@ -366,9 +377,9 @@ module HexaPDF
|
|
366
377
|
# console.
|
367
378
|
def read_password(prompt = "Password")
|
368
379
|
if $stdin.tty?
|
369
|
-
read_from_console(prompt)
|
380
|
+
read_from_console(prompt, noecho: true)
|
370
381
|
else
|
371
|
-
($stdin.gets || read_from_console(prompt)).chomp
|
382
|
+
($stdin.gets || read_from_console(prompt, noecho: true)).chomp
|
372
383
|
end
|
373
384
|
end
|
374
385
|
|
@@ -396,11 +407,14 @@ module HexaPDF
|
|
396
407
|
private
|
397
408
|
|
398
409
|
# Displays the given prompt, reads from the console without echo and returns the read string.
|
399
|
-
def read_from_console(prompt)
|
410
|
+
def read_from_console(prompt, noecho: false)
|
400
411
|
IO.console.write("#{prompt}: ")
|
401
|
-
|
402
|
-
|
403
|
-
|
412
|
+
if noecho
|
413
|
+
IO.console.noecho {|io| io.gets.chomp }
|
414
|
+
puts
|
415
|
+
else
|
416
|
+
IO.console.gets.chomp
|
417
|
+
end
|
404
418
|
end
|
405
419
|
|
406
420
|
end
|
data/lib/hexapdf/cli/files.rb
CHANGED
@@ -39,19 +39,29 @@ require 'hexapdf/cli/command'
|
|
39
39
|
module HexaPDF
|
40
40
|
module CLI
|
41
41
|
|
42
|
-
# Lists or extracts embedded files from a PDF file.
|
42
|
+
# Lists or extracts embedded files from a PDF file or attaches them.
|
43
43
|
#
|
44
44
|
# See: HexaPDF::Type::EmbeddedFile
|
45
45
|
class Files < Command
|
46
46
|
|
47
47
|
def initialize #:nodoc:
|
48
48
|
super('files', takes_commands: false)
|
49
|
-
short_desc("List
|
49
|
+
short_desc("List and extract embedded files from a PDF or attach files")
|
50
50
|
long_desc(<<~EOF)
|
51
|
-
If the option --extract is
|
52
|
-
indices. The --extract option can then be
|
51
|
+
If neither the option --attach nor the option --extract is given, the available
|
52
|
+
files are listed with their names and indices. The --extract option can then be
|
53
|
+
used to extract one or more files. Or the --attach option can be used to attach
|
54
|
+
files to the PDF.
|
53
55
|
EOF
|
54
56
|
|
57
|
+
options.on("--attach FILE", "-a FILE", String,
|
58
|
+
"The file that should be attached. Can be used multiple times.") do |file|
|
59
|
+
@attach_files << [file, nil]
|
60
|
+
end
|
61
|
+
options.on("--description DESC", "-d DESC", String,
|
62
|
+
"Adds a description to the last file to be attached.") do |description|
|
63
|
+
@attach_files[-1][1] = description
|
64
|
+
end
|
55
65
|
options.on("--extract [a,b,c,...]", "-e [a,b,c,...]", Array,
|
56
66
|
"The indices of the files that should be extracted. Use 0 or no argument to " \
|
57
67
|
"extract all files.") do |indices|
|
@@ -66,15 +76,24 @@ module HexaPDF
|
|
66
76
|
@password = (pwd == '-' ? read_password : pwd)
|
67
77
|
end
|
68
78
|
|
79
|
+
@attach_files = []
|
69
80
|
@indices = []
|
70
81
|
@password = nil
|
71
82
|
@search = false
|
72
83
|
end
|
73
84
|
|
74
|
-
def execute(pdf) #:nodoc:
|
75
|
-
|
76
|
-
|
85
|
+
def execute(pdf, output = nil) #:nodoc:
|
86
|
+
if @indices.empty? && !@attach_files.empty?
|
87
|
+
raise Error, "Missing output file" unless output
|
88
|
+
maybe_raise_on_existing_file(output)
|
89
|
+
end
|
90
|
+
with_document(pdf, password: @password, out_file: output) do |doc|
|
91
|
+
if @indices.empty? && @attach_files.empty?
|
77
92
|
list_files(doc)
|
93
|
+
elsif !@indices.empty? && !@attach_files.empty?
|
94
|
+
raise Error, "Use either --attach or --extract but not both"
|
95
|
+
elsif !@attach_files.empty?
|
96
|
+
attach_files(doc)
|
78
97
|
else
|
79
98
|
extract_files(doc)
|
80
99
|
end
|
@@ -116,6 +135,11 @@ module HexaPDF
|
|
116
135
|
end
|
117
136
|
end
|
118
137
|
|
138
|
+
# Attaches the files given on the CLI to the document.
|
139
|
+
def attach_files(doc)
|
140
|
+
@attach_files.each {|file, desc| doc.files.add(file, description: desc) }
|
141
|
+
end
|
142
|
+
|
119
143
|
# Iterates over all embedded files.
|
120
144
|
def each_file(doc, &block) # :yields: obj, index
|
121
145
|
doc.files.each(search: @search).select(&:embedded_file?).each_with_index(&block)
|
data/lib/hexapdf/cli/form.rb
CHANGED
@@ -76,6 +76,10 @@ module HexaPDF
|
|
76
76
|
options.on('--flatten', 'Flatten the form fields') do
|
77
77
|
@flatten = true
|
78
78
|
end
|
79
|
+
options.on("--[no-]fill-read-only-fields", "Allow filling in fields that are " \
|
80
|
+
"marked as read only. Default: false") do |read_only|
|
81
|
+
@fill_read_only_fields = read_only
|
82
|
+
end
|
79
83
|
options.on("--[no-]viewer-override", "Let the PDF viewer override the visual " \
|
80
84
|
"appearance. Default: use setting from input PDF") do |need_appearances|
|
81
85
|
@need_appearances = need_appearances
|
@@ -90,6 +94,7 @@ module HexaPDF
|
|
90
94
|
@flatten = false
|
91
95
|
@generate_template = false
|
92
96
|
@template = nil
|
97
|
+
@fill_read_only_fields = false
|
93
98
|
@need_appearances = nil
|
94
99
|
@incremental = true
|
95
100
|
end
|
@@ -115,6 +120,7 @@ module HexaPDF
|
|
115
120
|
else
|
116
121
|
fill_form(doc)
|
117
122
|
end
|
123
|
+
doc.acro_form.recalculate_fields
|
118
124
|
end
|
119
125
|
if @flatten && !doc.acro_form.flatten.empty?
|
120
126
|
$stderr.puts "Warning: Not all form fields could be flattened"
|
@@ -126,8 +132,12 @@ module HexaPDF
|
|
126
132
|
each_field(doc) do |_, _, field, _|
|
127
133
|
next if unsupported_fields.include?(field.concrete_field_type)
|
128
134
|
name = field.full_field_name.gsub(':', "\\:")
|
129
|
-
|
130
|
-
|
135
|
+
if field.field_value
|
136
|
+
Array(field.field_value).each do |val|
|
137
|
+
puts "#{name}: #{val.to_s.gsub(/(\r|\r\n|\n)/, '\1 ')}"
|
138
|
+
end
|
139
|
+
else
|
140
|
+
puts "#{name}: "
|
131
141
|
end
|
132
142
|
end
|
133
143
|
else
|
@@ -141,7 +151,7 @@ module HexaPDF
|
|
141
151
|
# Lists all terminal form fields.
|
142
152
|
def list_form_fields(doc)
|
143
153
|
current_page_index = -1
|
144
|
-
each_field(doc) do |_page, page_index, field, widget|
|
154
|
+
each_field(doc, with_seen: true) do |_page, page_index, field, widget|
|
145
155
|
if current_page_index != page_index
|
146
156
|
puts "Page #{page_index + 1}"
|
147
157
|
current_page_index = page_index
|
@@ -151,6 +161,7 @@ module HexaPDF
|
|
151
161
|
(field.alternate_field_name ? " (#{field.alternate_field_name})" : '')
|
152
162
|
concrete_field_type = field.concrete_field_type
|
153
163
|
nice_field_type = concrete_field_type.to_s.split('_').map(&:capitalize).join(' ')
|
164
|
+
size = "(#{widget[:Rect].width.round(3)}x#{widget[:Rect].height.round(3)})"
|
154
165
|
position = "(#{widget[:Rect].left}, #{widget[:Rect].bottom})"
|
155
166
|
field_value = if !field.field_value || concrete_field_type != :signature_field
|
156
167
|
field.field_value.inspect
|
@@ -161,9 +172,10 @@ module HexaPDF
|
|
161
172
|
temp
|
162
173
|
end
|
163
174
|
|
164
|
-
|
175
|
+
flags = field_flags(field)
|
176
|
+
puts " #{field_name}" << (flags.empty? ? '' : " (#{flags.join(', ')})")
|
165
177
|
if command_parser.verbosity_info?
|
166
|
-
printf(" └─ %-22s | %-20s\n", nice_field_type, position)
|
178
|
+
printf(" └─ %-22s | %-20s\n", nice_field_type, "#{size} #{position}")
|
167
179
|
end
|
168
180
|
puts " └─ #{field_value}"
|
169
181
|
if command_parser.verbosity_info?
|
@@ -172,6 +184,10 @@ module HexaPDF
|
|
172
184
|
elsif concrete_field_type == :radio_button || concrete_field_type == :check_box
|
173
185
|
puts " └─ Options: #{([:Off] + field.allowed_values).map(&:to_s).join(', ')}"
|
174
186
|
end
|
187
|
+
puts " └─ Widget OID: #{widget.oid},#{widget.gen}"
|
188
|
+
if field != widget
|
189
|
+
puts " └─ Field OID: #{field.oid},#{field.gen}"
|
190
|
+
end
|
175
191
|
end
|
176
192
|
end
|
177
193
|
end
|
@@ -179,7 +195,9 @@ module HexaPDF
|
|
179
195
|
# Fills out the form by interactively asking the user for field values.
|
180
196
|
def fill_form(doc)
|
181
197
|
current_page_index = -1
|
198
|
+
form = doc.acro_form
|
182
199
|
each_field(doc) do |_page, page_index, field, _widget|
|
200
|
+
next if field.flagged?(:read_only) && !@fill_read_only_fields
|
183
201
|
if current_page_index != page_index
|
184
202
|
puts "Page #{page_index + 1}"
|
185
203
|
current_page_index = page_index
|
@@ -189,7 +207,8 @@ module HexaPDF
|
|
189
207
|
(field.alternate_field_name ? " (#{field.alternate_field_name})" : '')
|
190
208
|
concrete_field_type = field.concrete_field_type
|
191
209
|
|
192
|
-
|
210
|
+
flags = field_flags(field)
|
211
|
+
puts " #{field_name}" << (flags.empty? ? '' : " (#{flags.join(', ')})")
|
193
212
|
puts " └─ Current value: #{field.field_value.inspect}"
|
194
213
|
|
195
214
|
if field.field_type == :Ch
|
@@ -206,9 +225,9 @@ module HexaPDF
|
|
206
225
|
print " └─ New value: "
|
207
226
|
value = $stdin.readline.chomp
|
208
227
|
next if value.empty?
|
209
|
-
|
228
|
+
form.fill(field.full_field_name => value)
|
210
229
|
rescue HexaPDF::Error => e
|
211
|
-
puts " ⚠
|
230
|
+
puts " ⚠ Error while setting '#{field.full_field_name}': #{e.message}"
|
212
231
|
retry
|
213
232
|
end
|
214
233
|
end
|
@@ -216,13 +235,20 @@ module HexaPDF
|
|
216
235
|
|
217
236
|
# Fills out the form using the data from the provided template file.
|
218
237
|
def fill_form_with_template(doc)
|
219
|
-
data = parse_template
|
220
238
|
form = doc.acro_form
|
221
|
-
data
|
239
|
+
data = parse_template
|
240
|
+
data.reject! do |name, _value|
|
222
241
|
field = form.field_by_name(name)
|
223
242
|
raise Error, "Field '#{name}' not found in input PDF" unless field
|
224
|
-
|
243
|
+
if field.flagged?(:read_only) && !@fill_read_only_fields
|
244
|
+
puts "Ignoring field '#{name}' because it is read only and --fill-read-only-fields " \
|
245
|
+
"is not set"
|
246
|
+
true
|
247
|
+
else
|
248
|
+
false
|
249
|
+
end
|
225
250
|
end
|
251
|
+
form.fill(data)
|
226
252
|
end
|
227
253
|
|
228
254
|
# Parses the data from the given template file.
|
@@ -250,33 +276,9 @@ module HexaPDF
|
|
250
276
|
data
|
251
277
|
end
|
252
278
|
|
253
|
-
# Applies the given value to the field.
|
254
|
-
def apply_field_value(field, value)
|
255
|
-
case field.concrete_field_type
|
256
|
-
when :single_line_text_field, :multiline_text_field, :comb_text_field, :file_select_field,
|
257
|
-
:combo_box, :list_box, :editable_combo_box
|
258
|
-
field.field_value = value
|
259
|
-
when :check_box
|
260
|
-
field.field_value = case value
|
261
|
-
when /y(es)?|t(rue)?/
|
262
|
-
true
|
263
|
-
when /n(o)?|f(alse)?/
|
264
|
-
false
|
265
|
-
else
|
266
|
-
value
|
267
|
-
end
|
268
|
-
when :radio_button
|
269
|
-
field.field_value = value.to_sym
|
270
|
-
else
|
271
|
-
raise Error, "Field type #{field.concrete_field_type} not yet supported"
|
272
|
-
end
|
273
|
-
rescue StandardError
|
274
|
-
raise Error, "Error while setting '#{field.full_field_name}': #{$!.message}"
|
275
|
-
end
|
276
|
-
|
277
279
|
# Iterates over all non-push button fields in page order. If a field appears on multiple
|
278
|
-
# pages, it is only yielded on the first page.
|
279
|
-
def each_field(doc) # :yields: page, page_index, field
|
280
|
+
# pages, it is only yielded on the first page if +with_seen+ is +false.
|
281
|
+
def each_field(doc, with_seen: false) # :yields: page, page_index, field
|
280
282
|
seen = {}
|
281
283
|
|
282
284
|
doc.pages.each_with_index do |page, page_index|
|
@@ -284,7 +286,7 @@ module HexaPDF
|
|
284
286
|
next unless annotation[:Subtype] == :Widget
|
285
287
|
field = annotation.form_field
|
286
288
|
next if field.concrete_field_type == :push_button
|
287
|
-
|
289
|
+
if with_seen || !seen[field.full_field_name]
|
288
290
|
yield(page, page_index, field, annotation)
|
289
291
|
seen[field.full_field_name] = true
|
290
292
|
end
|
@@ -292,6 +294,12 @@ module HexaPDF
|
|
292
294
|
end
|
293
295
|
end
|
294
296
|
|
297
|
+
# Returns an array with the flags "read only" and "required" if they are set.
|
298
|
+
def field_flags(field)
|
299
|
+
[field.flagged?(:read_only) ? "read only" : nil,
|
300
|
+
field.flagged?(:required) ? "required" : nil].compact
|
301
|
+
end
|
302
|
+
|
295
303
|
end
|
296
304
|
|
297
305
|
end
|