hexapdf 0.40.0 → 0.41.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 +45 -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 +101 -0
- data/lib/hexapdf/cli/command.rb +11 -0
- data/lib/hexapdf/cli/form.rb +38 -9
- data/lib/hexapdf/cli/info.rb +4 -0
- data/lib/hexapdf/configuration.rb +10 -0
- 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/field.rb +14 -0
- data/lib/hexapdf/type/acro_form/form.rb +25 -8
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +498 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +78 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/annotations/widget.rb +1 -1
- 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/type/acro_form/test_appearance_generator.rb +31 -47
- data/test/hexapdf/type/acro_form/test_field.rb +11 -0
- data/test/hexapdf/type/acro_form/test_form.rb +33 -0
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +226 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +44 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae86345e0f2ed2dd27c9c58c550e7eaffb4d7c5d3ba388afb04318ee20491313
|
|
4
|
+
data.tar.gz: cfd9f8575ce9f4324c594c2617cc7e1bcf1d735276ac92a275f26acf74566bc9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d36715922fbbf5a93eeb5512ed0abf7ec78fbd099c49131500bd9f7db75c39248bb2bac12ad7e3df5c4a8a449351f63e0d83e0ed635f9a025e4bf25fdbe9f0e1
|
|
7
|
+
data.tar.gz: fc7a89694614826c8d151b7dab2c3f45ec335fc94efff873065c3dc68c845a691311b91b42c3b791eb71be80e1f8fd623838eb0cf1b94d0d1b758441df1b1a7a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
## 0.41.0 - 2024-05-05
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* Font loader [HexaPDF::FontLoader::VariantFromName] to ease specifying font
|
|
6
|
+
variants
|
|
7
|
+
* [HexaPDF::Type::AcroForm::JavaScriptActions] module to contain all JavaScript
|
|
8
|
+
actions that HexaPDF can handle
|
|
9
|
+
* Support for the `AFSimple_Calculate` Javascript method
|
|
10
|
+
* Support for Simplified Field Notation for defining Javascript calculations
|
|
11
|
+
* Configuration option 'encryption.on_decryption_error' to allow custom
|
|
12
|
+
decryption error handling
|
|
13
|
+
* CLI option `--fill-read-only-fields` to `hexapdf form` to specify whether
|
|
14
|
+
filling in read only fields is allowed
|
|
15
|
+
* [HexaPDF::Type::AcroForm::Field#form_field] to getting the field irrespective
|
|
16
|
+
of whether the object is already a field or a widget
|
|
17
|
+
* [HexaPDF::Type::AcroForm::TextField#set_format_action] for setting a
|
|
18
|
+
JavaScript action that formats the field's value
|
|
19
|
+
* [HexaPDF::Type::AcroForm::TextField#set_calculate_action] for setting a
|
|
20
|
+
JavaScript action that calculates the field's value
|
|
21
|
+
* [HexaPDF::Type::AcroForm#recalculate_fields] for recalculating fields
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
* CLI command `hexapdf form` to show more information in verbose mode
|
|
26
|
+
* CLI command 'hexapdf form' to show the field flags "read only" and "required"
|
|
27
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to remove the hidden flag from
|
|
28
|
+
widgets
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
* [HexaPDF::FontLoader::FromConfiguration] to accept arbitrary keyword arguments
|
|
33
|
+
* [HexaPDF::Font::CMap::Parser] to avoid instantiating invalid UTF-16BE chars
|
|
34
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to work for files where check
|
|
35
|
+
boxes don't have appearance subdictionaries
|
|
36
|
+
* [HexaPDF::Type::AcroForm::TextField#field_value=] to call the config option
|
|
37
|
+
'acro_form.on_invalid_value' when passing a non-String argument (except `nil`)
|
|
38
|
+
* [HexaPDF::Type::AcroForm::JavaScriptActions#apply_af_number_format] to
|
|
39
|
+
correctly convert strings using commas or points into numbers
|
|
40
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to use the field instead of the
|
|
41
|
+
widget object as the source for JavaScript format actions
|
|
42
|
+
* CLI command `hexapdf form --generate-template` to output fields without values
|
|
43
|
+
* `AFNumber_Format` JavaScript parsing to work without trailing semicolon
|
|
44
|
+
|
|
45
|
+
|
|
1
46
|
## 0.40.0 - 2024-03-23
|
|
2
47
|
|
|
3
48
|
### 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,101 @@
|
|
|
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("Calculate actions", at: [50, 570])
|
|
35
|
+
|
|
36
|
+
canvas.text("Source fields", at: [70, 540])
|
|
37
|
+
canvas.text("a:", at: [200, 540])
|
|
38
|
+
tx = form.create_text_field("a", font_size: 16)
|
|
39
|
+
tx.set_format_action(:number, decimals: 2)
|
|
40
|
+
widget = tx.create_widget(page, Rect: [220, 535, 280, 555])
|
|
41
|
+
tx.field_value = "10,50"
|
|
42
|
+
canvas.text("b:", at: [310, 540])
|
|
43
|
+
tx = form.create_text_field("b", font_size: 16)
|
|
44
|
+
tx.set_format_action(:number, decimals: 2)
|
|
45
|
+
widget = tx.create_widget(page, Rect: [330, 535, 390, 555])
|
|
46
|
+
tx.field_value = "20,60"
|
|
47
|
+
canvas.text("c:", at: [420, 540])
|
|
48
|
+
tx = form.create_text_field("c", font_size: 16)
|
|
49
|
+
tx.set_format_action(:number, decimals: 2)
|
|
50
|
+
widget = tx.create_widget(page, Rect: [440, 535, 500, 555])
|
|
51
|
+
tx.field_value = "30,70"
|
|
52
|
+
|
|
53
|
+
canvas.text("Predefined", at: [70, 510])
|
|
54
|
+
canvas.text("Sum", at: [90, 480])
|
|
55
|
+
tx = form.create_text_field("sum", font_size: 16)
|
|
56
|
+
tx.set_format_action(:number, decimals: 2)
|
|
57
|
+
tx.set_calculate_action(:sum, fields: ['a', 'b', 'c'])
|
|
58
|
+
tx.flag(:read_only)
|
|
59
|
+
widget = tx.create_widget(page, Rect: [310, 475, 500, 495])
|
|
60
|
+
canvas.text("Average", at: [90, 450])
|
|
61
|
+
tx = form.create_text_field("average", font_size: 16)
|
|
62
|
+
tx.set_format_action(:number, decimals: 2)
|
|
63
|
+
tx.set_calculate_action(:average, fields: ['a', 'b', 'c'])
|
|
64
|
+
tx.flag(:read_only)
|
|
65
|
+
widget = tx.create_widget(page, Rect: [310, 445, 500, 465])
|
|
66
|
+
canvas.text("Product", at: [90, 420])
|
|
67
|
+
tx = form.create_text_field("product", font_size: 16)
|
|
68
|
+
tx.set_format_action(:number, decimals: 2)
|
|
69
|
+
tx.set_calculate_action(:product, fields: ['a', 'b', 'c'])
|
|
70
|
+
tx.flag(:read_only)
|
|
71
|
+
widget = tx.create_widget(page, Rect: [310, 415, 500, 435])
|
|
72
|
+
canvas.text("Minimum", at: [90, 390])
|
|
73
|
+
tx = form.create_text_field("min", font_size: 16)
|
|
74
|
+
tx.set_format_action(:number, decimals: 2)
|
|
75
|
+
tx.set_calculate_action(:min, fields: ['a', 'b', 'c'])
|
|
76
|
+
tx.flag(:read_only)
|
|
77
|
+
widget = tx.create_widget(page, Rect: [310, 385, 500, 405])
|
|
78
|
+
canvas.text("Maximum", at: [90, 360])
|
|
79
|
+
tx = form.create_text_field("max", font_size: 16)
|
|
80
|
+
tx.set_format_action(:number, decimals: 2)
|
|
81
|
+
tx.set_calculate_action(:max, fields: ['a', 'b', 'c'])
|
|
82
|
+
tx.flag(:read_only)
|
|
83
|
+
widget = tx.create_widget(page, Rect: [310, 355, 500, 375])
|
|
84
|
+
|
|
85
|
+
canvas.text("Simplified Field Notation", at: [70, 330])
|
|
86
|
+
canvas.text("a + b + c", at: [90, 300])
|
|
87
|
+
tx = form.create_text_field("sfn1", font_size: 16)
|
|
88
|
+
tx.set_format_action(:number, decimals: 2)
|
|
89
|
+
tx.set_calculate_action(:sfn, fields: "a + b + c")
|
|
90
|
+
tx.flag(:read_only)
|
|
91
|
+
widget = tx.create_widget(page, Rect: [310, 295, 500, 315])
|
|
92
|
+
canvas.text("(a + b)*(c - a) / b + 3.14", at: [90, 270])
|
|
93
|
+
tx = form.create_text_field("sfn2", font_size: 16)
|
|
94
|
+
tx.set_format_action(:number, decimals: 2)
|
|
95
|
+
tx.set_calculate_action(:sfn, fields: "(a + b)*(c - a) / b + 3.14")
|
|
96
|
+
tx.flag(:read_only)
|
|
97
|
+
widget = tx.create_widget(page, Rect: [310, 265, 500, 285])
|
|
98
|
+
|
|
99
|
+
form.recalculate_fields
|
|
100
|
+
|
|
101
|
+
doc.write('acro_form_java_script.pdf', optimize: true)
|
data/lib/hexapdf/cli/command.rb
CHANGED
|
@@ -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
|
|
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
|
|
@@ -180,6 +196,7 @@ module HexaPDF
|
|
|
180
196
|
def fill_form(doc)
|
|
181
197
|
current_page_index = -1
|
|
182
198
|
each_field(doc) do |_page, page_index, field, _widget|
|
|
199
|
+
next if field.flagged?(:read_only) && !@fill_read_only_fields
|
|
183
200
|
if current_page_index != page_index
|
|
184
201
|
puts "Page #{page_index + 1}"
|
|
185
202
|
current_page_index = page_index
|
|
@@ -189,7 +206,8 @@ module HexaPDF
|
|
|
189
206
|
(field.alternate_field_name ? " (#{field.alternate_field_name})" : '')
|
|
190
207
|
concrete_field_type = field.concrete_field_type
|
|
191
208
|
|
|
192
|
-
|
|
209
|
+
flags = field_flags(field)
|
|
210
|
+
puts " #{field_name}" << (flags.empty? ? '' : " (#{flags.join(', ')})")
|
|
193
211
|
puts " └─ Current value: #{field.field_value.inspect}"
|
|
194
212
|
|
|
195
213
|
if field.field_type == :Ch
|
|
@@ -221,6 +239,11 @@ module HexaPDF
|
|
|
221
239
|
data.each do |name, value|
|
|
222
240
|
field = form.field_by_name(name)
|
|
223
241
|
raise Error, "Field '#{name}' not found in input PDF" unless field
|
|
242
|
+
if field.flagged?(:read_only) && !@fill_read_only_fields
|
|
243
|
+
puts "Ignoring field '#{name}' because it is read only and --fill-read-only-fields " \
|
|
244
|
+
"is no set"
|
|
245
|
+
next
|
|
246
|
+
end
|
|
224
247
|
apply_field_value(field, value)
|
|
225
248
|
end
|
|
226
249
|
end
|
|
@@ -275,8 +298,8 @@ module HexaPDF
|
|
|
275
298
|
end
|
|
276
299
|
|
|
277
300
|
# 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
|
|
301
|
+
# pages, it is only yielded on the first page if +with_seen+ is +false.
|
|
302
|
+
def each_field(doc, with_seen: false) # :yields: page, page_index, field
|
|
280
303
|
seen = {}
|
|
281
304
|
|
|
282
305
|
doc.pages.each_with_index do |page, page_index|
|
|
@@ -284,7 +307,7 @@ module HexaPDF
|
|
|
284
307
|
next unless annotation[:Subtype] == :Widget
|
|
285
308
|
field = annotation.form_field
|
|
286
309
|
next if field.concrete_field_type == :push_button
|
|
287
|
-
|
|
310
|
+
if with_seen || !seen[field.full_field_name]
|
|
288
311
|
yield(page, page_index, field, annotation)
|
|
289
312
|
seen[field.full_field_name] = true
|
|
290
313
|
end
|
|
@@ -292,6 +315,12 @@ module HexaPDF
|
|
|
292
315
|
end
|
|
293
316
|
end
|
|
294
317
|
|
|
318
|
+
# Returns an array with the flags "read only" and "required" if they are set.
|
|
319
|
+
def field_flags(field)
|
|
320
|
+
[field.flagged?(:read_only) ? "read only" : nil,
|
|
321
|
+
field.flagged?(:required) ? "required" : nil].compact
|
|
322
|
+
end
|
|
323
|
+
|
|
295
324
|
end
|
|
296
325
|
|
|
297
326
|
end
|
data/lib/hexapdf/cli/info.rb
CHANGED
|
@@ -192,6 +192,10 @@ module HexaPDF
|
|
|
192
192
|
puts "WARNING: Parse error at position #{pos}: #{msg}"
|
|
193
193
|
false
|
|
194
194
|
end
|
|
195
|
+
options[:config]['encryption.on_decryption_error'] = lambda do |obj, msg|
|
|
196
|
+
puts "WARNING: Decryption problem for object (#{obj.oid},#{obj.gen}): #{msg}"
|
|
197
|
+
false
|
|
198
|
+
end
|
|
195
199
|
options
|
|
196
200
|
else
|
|
197
201
|
super
|
|
@@ -255,6 +255,12 @@ module HexaPDF
|
|
|
255
255
|
# PDF defines a standard security handler that is implemented
|
|
256
256
|
# (HexaPDF::Encryption::StandardSecurityHandler) and assigned the :Standard name.
|
|
257
257
|
#
|
|
258
|
+
# encryption.on_decryption_error::
|
|
259
|
+
# Callback hook when HexaPDF encounters a decryption error that can potentially be ignored.
|
|
260
|
+
#
|
|
261
|
+
# The value needs to be an object that responds to \#call(obj, message) and returns +true+ if
|
|
262
|
+
# an error should be raised.
|
|
263
|
+
#
|
|
258
264
|
# encryption.sub_filter_map::
|
|
259
265
|
# A mapping from a PDF name (a Symbol) to a security handler class (see
|
|
260
266
|
# HexaPDF::Encryption::SecurityHandler). If the value is a String, it should contain the name
|
|
@@ -488,6 +494,9 @@ module HexaPDF
|
|
|
488
494
|
'encryption.filter_map' => {
|
|
489
495
|
Standard: 'HexaPDF::Encryption::StandardSecurityHandler',
|
|
490
496
|
},
|
|
497
|
+
'encryption.on_decryption_error' => proc do |_obj, _error|
|
|
498
|
+
false
|
|
499
|
+
end,
|
|
491
500
|
'encryption.sub_filter_map' => {},
|
|
492
501
|
'filter.map' => {
|
|
493
502
|
ASCIIHexDecode: 'HexaPDF::Filter::ASCIIHexDecode',
|
|
@@ -523,6 +532,7 @@ module HexaPDF
|
|
|
523
532
|
'HexaPDF::FontLoader::Standard14',
|
|
524
533
|
'HexaPDF::FontLoader::FromConfiguration',
|
|
525
534
|
'HexaPDF::FontLoader::FromFile',
|
|
535
|
+
'HexaPDF::FontLoader::VariantFromName',
|
|
526
536
|
],
|
|
527
537
|
'graphic_object.arc.max_curves' => 6,
|
|
528
538
|
'graphic_object.map' => {
|
|
@@ -2245,6 +2245,8 @@ module HexaPDF
|
|
|
2245
2245
|
# canvas.text("Times at size 10", at: [10, 150])
|
|
2246
2246
|
# canvas.font("Times", variant: :bold_italic, size: 15)
|
|
2247
2247
|
# canvas.text("Times bold+italic at size 15", at: [10, 100])
|
|
2248
|
+
# canvas.font("Times bold")
|
|
2249
|
+
# canvas.text("Times bold using the variant-from-name method", at: [10, 50])
|
|
2248
2250
|
#
|
|
2249
2251
|
# See: PDF2.0 s9.2.2, #font_size, #text
|
|
2250
2252
|
def font(name = nil, size: nil, **options)
|
|
@@ -92,6 +92,13 @@ module HexaPDF
|
|
|
92
92
|
#
|
|
93
93
|
# style.font = ['Helvetica', variant: :bold]
|
|
94
94
|
#
|
|
95
|
+
# Helvetica in bold could also be set the conventional way:
|
|
96
|
+
#
|
|
97
|
+
# style.font = 'Helvetica bold'
|
|
98
|
+
#
|
|
99
|
+
# However, using an array it is also possible to specify other options when setting a font,
|
|
100
|
+
# like the :subset option.
|
|
101
|
+
#
|
|
95
102
|
class Layout
|
|
96
103
|
|
|
97
104
|
# This class is used when a box can contain child boxes and the creation of such boxes should
|
|
@@ -539,7 +546,7 @@ module HexaPDF
|
|
|
539
546
|
# # assign the predefined style :cell_text to all texts
|
|
540
547
|
# args[] = {style: :cell_text}
|
|
541
548
|
# # row 0 has a grey background and bold text
|
|
542
|
-
# args[0] = {font:
|
|
549
|
+
# args[0] = {font: 'Helvetica bold', cell: {background_color: 'eee'}}
|
|
543
550
|
# # text in last column is right aligned
|
|
544
551
|
# args[0..-1, -1] = {text_align: :right}
|
|
545
552
|
# end
|
|
@@ -112,11 +112,15 @@ module HexaPDF
|
|
|
112
112
|
# It is assumed that the initialization vector is included in the first BLOCK_SIZE bytes
|
|
113
113
|
# of the data. After the decryption the PKCS#5 padding is removed.
|
|
114
114
|
#
|
|
115
|
+
# If a problem is encountered, an error message is yielded. If no block is given or if the
|
|
116
|
+
# supplied block returns +true+, an error is raised.
|
|
117
|
+
#
|
|
115
118
|
# See: PDF2.0 s7.6.3
|
|
116
|
-
def decrypt(key, data)
|
|
119
|
+
def decrypt(key, data) # :yields: error_message
|
|
117
120
|
return data if data.empty? # Handle invalid files with empty strings
|
|
118
121
|
if data.length % BLOCK_SIZE != 0 || data.length < BLOCK_SIZE
|
|
119
|
-
|
|
122
|
+
msg = "Invalid data for decryption, need 32 + 16*n bytes"
|
|
123
|
+
(!block_given? || yield(msg)) && raise(HexaPDF::EncryptionError, msg)
|
|
120
124
|
end
|
|
121
125
|
iv = data.slice!(0, BLOCK_SIZE)
|
|
122
126
|
# Handle invalid files with missing padding
|
|
@@ -126,8 +130,9 @@ module HexaPDF
|
|
|
126
130
|
# Returns a Fiber object that decrypts the data from the given source fiber with the
|
|
127
131
|
# +key+.
|
|
128
132
|
#
|
|
129
|
-
# Padding
|
|
130
|
-
|
|
133
|
+
# Padding, the initialization vector and an optionally given block are handled like in
|
|
134
|
+
# #decrypt.
|
|
135
|
+
def decryption_fiber(key, source) # :yields: error_message
|
|
131
136
|
Fiber.new do
|
|
132
137
|
data = ''.b
|
|
133
138
|
while data.length < BLOCK_SIZE && source.alive? && (new_data = source.resume)
|
|
@@ -145,8 +150,10 @@ module HexaPDF
|
|
|
145
150
|
end
|
|
146
151
|
|
|
147
152
|
if data.length % BLOCK_SIZE != 0
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
msg = "Invalid data for decryption, need 32 + 16*n bytes"
|
|
154
|
+
(!block_given? || yield(msg)) && raise(HexaPDF::EncryptionError, msg)
|
|
155
|
+
end
|
|
156
|
+
if data.empty?
|
|
150
157
|
data # Handle invalid files with missing padding
|
|
151
158
|
else
|
|
152
159
|
unpad(algorithm.process(data))
|
|
@@ -153,17 +153,18 @@ module HexaPDF
|
|
|
153
153
|
|
|
154
154
|
# Creates a new encrypted stream data object by utilizing the given stream data object +obj+
|
|
155
155
|
# as template. The arguments +key+ and +algorithm+ are used for decrypting purposes.
|
|
156
|
-
def initialize(obj, key, algorithm)
|
|
156
|
+
def initialize(obj, key, algorithm, &error_block)
|
|
157
157
|
obj.instance_variables.each {|v| instance_variable_set(v, obj.instance_variable_get(v)) }
|
|
158
158
|
@key = key
|
|
159
159
|
@algorithm = algorithm
|
|
160
|
+
@error_block = error_block
|
|
160
161
|
end
|
|
161
162
|
|
|
162
163
|
alias undecrypted_fiber fiber
|
|
163
164
|
|
|
164
165
|
# Returns a fiber like HexaPDF::StreamData#fiber, but one wrapped in a decrypting fiber.
|
|
165
166
|
def fiber(*args)
|
|
166
|
-
@algorithm.decryption_fiber(@key, super(*args))
|
|
167
|
+
@algorithm.decryption_fiber(@key, super(*args), &@error_block)
|
|
167
168
|
end
|
|
168
169
|
|
|
169
170
|
end
|
|
@@ -268,17 +269,18 @@ module HexaPDF
|
|
|
268
269
|
def decrypt(obj)
|
|
269
270
|
return obj if @is_encrypt_dict[obj] || obj.type == :XRef
|
|
270
271
|
|
|
272
|
+
error_proc = proc {|msg| document.config['encryption.on_decryption_error'].call(obj, msg) }
|
|
271
273
|
key = object_key(obj.oid, obj.gen, string_algorithm)
|
|
272
274
|
each_string_in_object(obj.value) do |str|
|
|
273
275
|
next if str.empty? || (obj.type == :Sig && obj[:Contents].equal?(str))
|
|
274
|
-
str.replace(string_algorithm.decrypt(key, str))
|
|
276
|
+
str.replace(string_algorithm.decrypt(key, str, &error_proc))
|
|
275
277
|
end
|
|
276
278
|
|
|
277
279
|
if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream.filter[0] != :Crypt
|
|
278
280
|
unless string_algorithm == stream_algorithm
|
|
279
281
|
key = object_key(obj.oid, obj.gen, stream_algorithm)
|
|
280
282
|
end
|
|
281
|
-
obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm)
|
|
283
|
+
obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm, &error_proc)
|
|
282
284
|
end
|
|
283
285
|
|
|
284
286
|
obj
|
|
@@ -163,11 +163,7 @@ module HexaPDF
|
|
|
163
163
|
dest = tokenizer.next_object
|
|
164
164
|
|
|
165
165
|
if dest.kind_of?(String)
|
|
166
|
-
|
|
167
|
-
code1.upto(code2) do |code|
|
|
168
|
-
cmap.add_unicode_mapping(code, +'' << codepoint)
|
|
169
|
-
codepoint += 1
|
|
170
|
-
end
|
|
166
|
+
cmap.add_unicode_range_mapping(code1, code2, dest.unpack("n*"))
|
|
171
167
|
elsif dest.kind_of?(Array)
|
|
172
168
|
code1.upto(code2) do |code|
|
|
173
169
|
str = dest[code - code1].encode!(::Encoding::UTF_8, ::Encoding::UTF_16BE)
|