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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -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 +113 -0
  7. data/lib/hexapdf/cli/command.rb +25 -11
  8. data/lib/hexapdf/cli/files.rb +31 -7
  9. data/lib/hexapdf/cli/form.rb +46 -38
  10. data/lib/hexapdf/cli/info.rb +4 -0
  11. data/lib/hexapdf/cli/inspect.rb +1 -1
  12. data/lib/hexapdf/cli/usage.rb +215 -0
  13. data/lib/hexapdf/cli.rb +2 -0
  14. data/lib/hexapdf/configuration.rb +11 -1
  15. data/lib/hexapdf/content/canvas.rb +2 -0
  16. data/lib/hexapdf/document/layout.rb +8 -1
  17. data/lib/hexapdf/encryption/aes.rb +13 -6
  18. data/lib/hexapdf/encryption/security_handler.rb +6 -4
  19. data/lib/hexapdf/font/cmap/parser.rb +1 -5
  20. data/lib/hexapdf/font/cmap.rb +22 -3
  21. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  22. data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
  23. data/lib/hexapdf/font_loader.rb +1 -0
  24. data/lib/hexapdf/layout/style.rb +5 -4
  25. data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
  26. data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
  27. data/lib/hexapdf/type/acro_form/field.rb +14 -0
  28. data/lib/hexapdf/type/acro_form/form.rb +70 -8
  29. data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
  30. data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
  31. data/lib/hexapdf/type/acro_form.rb +1 -0
  32. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  33. data/lib/hexapdf/type/resources.rb +2 -1
  34. data/lib/hexapdf/utils.rb +19 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/test/hexapdf/encryption/test_aes.rb +18 -8
  37. data/test/hexapdf/encryption/test_security_handler.rb +17 -0
  38. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  39. data/test/hexapdf/font/cmap/test_parser.rb +5 -3
  40. data/test/hexapdf/font/test_cmap.rb +8 -0
  41. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  42. data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
  43. data/test/hexapdf/test_utils.rb +16 -0
  44. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
  45. data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
  46. data/test/hexapdf/type/acro_form/test_field.rb +11 -0
  47. data/test/hexapdf/type/acro_form/test_form.rb +80 -0
  48. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
  49. data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
  50. data/test/hexapdf/type/test_resources.rb +5 -0
  51. metadata +8 -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: dd69db2c04dc802784438f9038c9a6ab32dac0806edca43b6c59406bba9d7ea6
4
+ data.tar.gz: a51a510a6a98b547b2b11fb07804ca9039a666adf95b52f65db9b070c3ffe9c3
5
5
  SHA512:
6
- metadata.gz: 0e63528c1c604b06a8f07a53852ebfc2f5172a9aee43320942e4088f8aaf68953acc6f9e6017c4215591a8bb7a428b61fa20bd6e4ea180af39c62058de5b04db
7
- data.tar.gz: 8b648570a98fcd5f1688c97ead5c05a0da6918cba49b3905bd70f526a65ddf376618e5d533c6bb04823117fc892839a600d0ba63d006699205e1ef399e0b64f5
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
@@ -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,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)
@@ -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-2023 Thomas Leitner; licensed under the AGPLv3\n\n" \
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 raises an error if it does and
160
- # HexaPDF::CLI#force is not set.
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
- raise Error, "Output file '#{filename}' already exists, not overwriting. Use --force to " \
164
- "force writing"
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
- str = IO.console.noecho {|io| io.gets.chomp }
402
- puts
403
- str
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
@@ -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 or extract embedded files from a PDF file")
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 not given, the available files are listed with their names and
52
- indices. The --extract option can then be used to extract one or more files.
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
- with_document(pdf, password: @password) do |doc|
76
- if @indices.empty?
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)
@@ -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
@@ -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
- puts " #{field_name}"
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
- apply_field_value(field, value)
228
+ form.fill(field.full_field_name => value)
210
229
  rescue HexaPDF::Error => e
211
- puts " ⚠ #{e.message}"
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.each do |name, value|
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
- apply_field_value(field, value)
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
- unless seen[field.full_field_name]
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