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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/examples/019-acro_form.rb +12 -23
  4. data/examples/027-composer_optional_content.rb +1 -1
  5. data/examples/030-pdfa.rb +6 -6
  6. data/examples/031-acro_form_java_script.rb +101 -0
  7. data/lib/hexapdf/cli/command.rb +11 -0
  8. data/lib/hexapdf/cli/form.rb +38 -9
  9. data/lib/hexapdf/cli/info.rb +4 -0
  10. data/lib/hexapdf/configuration.rb +10 -0
  11. data/lib/hexapdf/content/canvas.rb +2 -0
  12. data/lib/hexapdf/document/layout.rb +8 -1
  13. data/lib/hexapdf/encryption/aes.rb +13 -6
  14. data/lib/hexapdf/encryption/security_handler.rb +6 -4
  15. data/lib/hexapdf/font/cmap/parser.rb +1 -5
  16. data/lib/hexapdf/font/cmap.rb +22 -3
  17. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  18. data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
  19. data/lib/hexapdf/font_loader.rb +1 -0
  20. data/lib/hexapdf/layout/style.rb +5 -4
  21. data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
  22. data/lib/hexapdf/type/acro_form/field.rb +14 -0
  23. data/lib/hexapdf/type/acro_form/form.rb +25 -8
  24. data/lib/hexapdf/type/acro_form/java_script_actions.rb +498 -0
  25. data/lib/hexapdf/type/acro_form/text_field.rb +78 -0
  26. data/lib/hexapdf/type/acro_form.rb +1 -0
  27. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  28. data/lib/hexapdf/version.rb +1 -1
  29. data/test/hexapdf/encryption/test_aes.rb +18 -8
  30. data/test/hexapdf/encryption/test_security_handler.rb +17 -0
  31. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  32. data/test/hexapdf/font/cmap/test_parser.rb +5 -3
  33. data/test/hexapdf/font/test_cmap.rb +8 -0
  34. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  35. data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
  36. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
  37. data/test/hexapdf/type/acro_form/test_field.rb +11 -0
  38. data/test/hexapdf/type/acro_form/test_form.rb +33 -0
  39. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +226 -0
  40. data/test/hexapdf/type/acro_form/test_text_field.rb +44 -0
  41. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dab266d2115bdd9a7c9caf6a7512b56da77c14b8fe080d631d270c89a67e49f
4
- data.tar.gz: c05acdf542d3b65e2c763f9aa84e21de4ad4f6800a48a4b4ca6873832089a0e2
3
+ metadata.gz: ae86345e0f2ed2dd27c9c58c550e7eaffb4d7c5d3ba388afb04318ee20491313
4
+ data.tar.gz: cfd9f8575ce9f4324c594c2617cc7e1bcf1d735276ac92a275f26acf74566bc9
5
5
  SHA512:
6
- metadata.gz: 0e63528c1c604b06a8f07a53852ebfc2f5172a9aee43320942e4088f8aaf68953acc6f9e6017c4215591a8bb7a428b61fa20bd6e4ea180af39c62058de5b04db
7
- data.tar.gz: 8b648570a98fcd5f1688c97ead5c05a0da6918cba49b3905bd70f526a65ddf376618e5d533c6bb04823117fc892839a600d0ba63d006699205e1ef399e0b64f5
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
@@ -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("Number format", at: [70, 420])
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, 325, 500, 405])
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, 300])
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, 295, 500, 315])
60
+ widget = tx.create_widget(page, Rect: [200, 325, 500, 345])
72
61
 
73
- canvas.text("File select", at: [70, 270])
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, 265, 500, 285])
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, 240])
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, 220, 500, 255])
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, 170])
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, 150, 500, 185])
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, 120])
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, 50, 500, 135])
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: ['Helvetica', variant: :bold], font_size: 24)
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: ['Lato', variant: :bold], font_size: 20, margin: [50, 0, 20])
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: ['Lato', variant: :bold],
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: ['Lato', variant: :bold]},
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: ['Lato', variant: :bold]}
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: ["Lato", variant: :bold]}
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: ["Lato", variant: :bold]}
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)
@@ -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
 
@@ -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
- Array(field.field_value).each do |val|
130
- puts "#{name}: #{val.to_s.gsub(/(\r|\r\n|\n)/, '\1 ')}"
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
- puts " #{field_name}"
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
- puts " #{field_name}"
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
- unless seen[field.full_field_name]
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
@@ -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: ['Helvetica', variant: :bold], cell: {background_color: 'eee'}}
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
- raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
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 and the initialization vector are handled like in #decrypt.
130
- def decryption_fiber(key, source)
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
- raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
149
- elsif data.empty?
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
- codepoint = dest.force_encoding(::Encoding::UTF_16BE).ord
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)