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
@@ -0,0 +1,327 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/acro_form/java_script_actions'
6
+
7
+ describe HexaPDF::Type::AcroForm::JavaScriptActions do
8
+ before do
9
+ @klass = HexaPDF::Type::AcroForm::JavaScriptActions
10
+ @action = {S: :JavaScript}
11
+ end
12
+
13
+ describe "format" do
14
+ it "returns the original value if the format action can't be processed" do
15
+ @action[:JS] = 'Unknown();'
16
+ @klass.apply_format("10", @action)
17
+ end
18
+
19
+ describe "AFNumber_Format" do
20
+ before do
21
+ @value = '1234567.898765'
22
+ @action[:JS] = ''
23
+ end
24
+
25
+ it "returns a correct JavaScript string" do
26
+ assert_equal('AFNumber_Format(2, 0, 0, 0, "", true);',
27
+ @klass.af_number_format_action)
28
+ assert_equal('AFNumber_Format(1, 1, 1, 0, "E", false);',
29
+ @klass.af_number_format_action(decimals: 1, separator_style: :point_no_thousands,
30
+ negative_style: :red, currency_string: "E",
31
+ prepend_currency: false))
32
+ end
33
+
34
+ it "raise an error for invalid arguments" do
35
+ assert_raises(ArgumentError) { @klass.af_number_format_action(separator_style: :unknown) }
36
+ assert_raises(ArgumentError) { @klass.af_number_format_action(negative_style: :unknown) }
37
+ end
38
+
39
+ def assert_format(arg_string, result_value, result_color)
40
+ @action[:JS] = "AFNumber_Format(#{arg_string});"
41
+ value, text_color = @klass.apply_format(@value, @action)
42
+ assert_equal(result_value, value)
43
+ result_color ? assert_equal(result_color, text_color) : assert_nil(text_color)
44
+ end
45
+
46
+ it "works with both commas and points as decimal separator" do
47
+ @value = '1234567.898'
48
+ assert_format('2, 2, 0, 0, "", false', "1.234.567,90", "black")
49
+ @value = '1234567,898'
50
+ assert_format('2, 2, 0, 0, "", false', "1.234.567,90", "black")
51
+ @value = '123,4567,898'
52
+ assert_format('2, 2, 0, 0, "", false', "123,46", "black")
53
+ end
54
+
55
+ it "respects the set number of decimals" do
56
+ assert_format('0, 2, 0, 0, "E", false', "1.234.568E", "black")
57
+ assert_format('2, 2, 0, 0, "E", false', "1.234.567,90E", "black")
58
+ end
59
+
60
+ it "respects the digit separator style" do
61
+ ["1,234,567.90", "1234567.90", "1.234.567,90", "1234567,90"].each_with_index do |result, style|
62
+ assert_format("2, #{style}, 0, 0, \"\", false", result, "black")
63
+ end
64
+ end
65
+
66
+ it "respects the negative value styling" do
67
+ @value = '-1234567.898'
68
+ [["-E1234567,90", "black"], ["E1234567,90", "red"], ["(E1234567,90)", "black"],
69
+ ["(E1234567,90)", "red"]].each_with_index do |result, style|
70
+ assert_format("2, 3, #{style}, 0, \"E\", true", result[0], result[1])
71
+ end
72
+ end
73
+
74
+ it "respects the specified currency string and position" do
75
+ assert_format('2, 3, 0, 0, " E", false', "1234567,90 E", "black")
76
+ assert_format('2, 3, 0, 0, "E ", true', "E 1234567,90", "black")
77
+ end
78
+
79
+ it "allows omitting the trailing semicolon" do
80
+ @action[:JS] = "AFNumber_Format(2,2,0,0,\"\",false )"
81
+ value, = @klass.apply_format('1234.567', @action)
82
+ assert_equal('1.234,57', value)
83
+ end
84
+
85
+ it "does nothing to the value if the JavaScript method could not be determined " do
86
+ assert_format('2, 3, 0, 0, " E", false, a', "1234567.898765", nil)
87
+ end
88
+ end
89
+
90
+ describe "AFPercent_Format" do
91
+ before do
92
+ @value = '123.456789'
93
+ @action[:JS] = ''
94
+ end
95
+
96
+ it "returns a correct JavaScript string" do
97
+ assert_equal('AFPercent_Format(2, 0);',
98
+ @klass.af_percent_format_action)
99
+ assert_equal('AFPercent_Format(1, 1);',
100
+ @klass.af_percent_format_action(decimals: 1, separator_style: :point_no_thousands))
101
+ end
102
+
103
+ it "raise an error for invalid arguments" do
104
+ assert_raises(ArgumentError) { @klass.af_percent_format_action(separator_style: :unknown) }
105
+ end
106
+
107
+ def assert_format(arg_string, result_value)
108
+ @action[:JS] = "AFPercent_Format(#{arg_string});"
109
+ value, text_color = @klass.apply_format(@value, @action)
110
+ assert_equal(result_value, value)
111
+ assert_nil(text_color)
112
+ end
113
+
114
+ it "works with both commas and points as decimal separator" do
115
+ @value = '123.456789'
116
+ assert_format('2, 2', "12.345,68%")
117
+ @value = '123,456789'
118
+ assert_format('2, 2', "12.345,68%")
119
+ @value = '123,4567,89'
120
+ assert_format('2, 2', "12.345,67%")
121
+ end
122
+
123
+ it "respects the set number of decimals" do
124
+ assert_format('0, 2', "12.346%")
125
+ assert_format('2, 2', "12.345,68%")
126
+ end
127
+
128
+ it "respects the digit separator style" do
129
+ ["12,345.68%", "12345.68%", "12.345,68%", "12345,68%"].each_with_index do |result, style|
130
+ assert_format("2, #{style}", result)
131
+ end
132
+ end
133
+
134
+ it "allows omitting the trailing semicolon" do
135
+ @action[:JS] = "AFPercent_Format(2,2 )"
136
+ value, = @klass.apply_format('1.234', @action)
137
+ assert_equal('123,40%', value)
138
+ end
139
+
140
+ it "does nothing to the value if the JavaScript method could not be determined " do
141
+ assert_format('2, "df"', "123.456789")
142
+ end
143
+ end
144
+
145
+ describe "AFTime_Format" do
146
+ before do
147
+ @value = '15:25:37'
148
+ @action[:JS] = ''
149
+ end
150
+
151
+ it "returns a correct JavaScript string" do
152
+ assert_equal('AFTime_Format(0);',
153
+ @klass.af_time_format_action)
154
+ assert_equal('AFTime_Format(1);',
155
+ @klass.af_time_format_action(format: :hh12_mm))
156
+ end
157
+
158
+ it "raise an error for invalid arguments" do
159
+ assert_raises(ArgumentError) { @klass.af_time_format_action(format: :unknown) }
160
+ end
161
+
162
+ def assert_format(arg_string, result_value)
163
+ @action[:JS] = "AFTime_Format(#{arg_string});"
164
+ value, text_color = @klass.apply_format(@value, @action)
165
+ assert_equal(result_value, value)
166
+ assert_nil(text_color)
167
+ end
168
+
169
+ it "respects the time format" do
170
+ ["15:25", "3:25 PM", "15:25:37", "3:25:37 PM"].each_with_index do |result, style|
171
+ assert_format(style, result)
172
+ end
173
+ end
174
+
175
+ it "allows omitting the trailing semicolon" do
176
+ @action[:JS] = "AFTime_Format(2 )"
177
+ value, = @klass.apply_format('15:34', @action)
178
+ assert_equal('15:34:00', value)
179
+ end
180
+
181
+ it "does nothing to the value if the JavaScript method could not be determined " do
182
+ assert_format('1, "df"', "15:25:37")
183
+ end
184
+ end
185
+ end
186
+
187
+ describe "calculate" do
188
+ before do
189
+ @doc = HexaPDF::Document.new
190
+ @form = @doc.acro_form(create: true)
191
+ @form.create_text_field('text')
192
+ @field1 = @form.create_text_field('text.1')
193
+ @field1.field_value = "10"
194
+ @field2 = @form.create_text_field('text.2')
195
+ @field2.field_value = "20"
196
+ @field3 = @form.create_text_field('text.3')
197
+ @field3.field_value = "30"
198
+ end
199
+
200
+ it "returns nil if the calculate action is not a JavaScript action" do
201
+ @action[:S] = :GoTo
202
+ assert_nil(@klass.calculate(@form, @action))
203
+ end
204
+
205
+ it "returns nil if the calculate action contains unknown JavaScript" do
206
+ @action[:JS] = 'Unknown();'
207
+ assert_nil(@klass.calculate(@form, @action))
208
+ end
209
+
210
+ describe "predefined calculations" do
211
+ it "returns a correct JavaScript string" do
212
+ assert_equal('AFSimple_Calculate("SUM", ["text.1","text.2"]);',
213
+ @klass.af_simple_calculate_action(:sum, ['text.1', @field2]))
214
+ end
215
+
216
+ def assert_calculation(function, fields, value)
217
+ fields = fields.map {|field| "\"#{field.full_field_name}\"" }.join(", ")
218
+ @action[:JS] = "AFSimple_Calculate(\"#{function}\", new Array(#{fields}));"
219
+ assert_equal(value, @klass.calculate(@form, @action))
220
+ end
221
+
222
+ it "can sum fields" do
223
+ assert_calculation('SUM', [@field1, @field2, @field3], "60")
224
+ end
225
+
226
+ it "can average fields" do
227
+ assert_calculation('AVG', [@field1, @field2, @field3], "20")
228
+ end
229
+
230
+ it "can multiply fields" do
231
+ assert_calculation('PRD', [@field1, @field2, @field3], "6000")
232
+ end
233
+
234
+ it "can find the minimum field value" do
235
+ assert_calculation('MIN', [@field1, @field2, @field3], "10")
236
+ end
237
+
238
+ it "can find the maximum field value" do
239
+ assert_calculation('MAX', [@field1, @field2, @field3], "30")
240
+ end
241
+
242
+ it "works with floats" do
243
+ @field1.field_value = "10,54"
244
+ assert_calculation('SUM', [@field1, @field2], "30.54")
245
+ end
246
+
247
+ it "returns nil if a field cannot be resolved" do
248
+ @action[:JS] = 'AFSimple_Calculate("SUM", ["unknown"]);'
249
+ assert_nil(@klass.calculate(@form, @action))
250
+ end
251
+
252
+ it "allows omitting the trailing semicolon" do
253
+ @action[:JS] = 'AFSimple_Calculate("SUM", ["text.1"] )'
254
+ assert_equal("10", @klass.calculate(@form, @action))
255
+ end
256
+ end
257
+
258
+ describe "simplified field notation calculations" do
259
+ it "returns a correct JavaScript string" do
260
+ sfn = '(text.1 + text.2) * text.3 - text.1 / text.1 + 0 + 5.43 + 7,24'
261
+ assert_equal("/** BVCALC #{sfn} EVCALC **/ " \
262
+ 'event.value = (AFMakeNumber(getField("text.1").value) + ' \
263
+ 'AFMakeNumber(getField("text.2").value)) * ' \
264
+ 'AFMakeNumber(getField("text.3").value) - ' \
265
+ 'AFMakeNumber(getField("text.1").value) / ' \
266
+ 'AFMakeNumber(getField("text.1").value) ' \
267
+ '+ 0.0 + 5.43 + 7.24',
268
+ @klass.simplified_field_notation_action(@form, sfn))
269
+ end
270
+
271
+ it "fails if the SFN string is invalid when generating a JavaScript action string" do
272
+ assert_raises(ArgumentError) { @klass.simplified_field_notation_action(@form, '(test') }
273
+ end
274
+
275
+ def assert_calculation(sfn, value)
276
+ @action[:JS] = "/** BVCALC #{sfn} EVCALC **/"
277
+ result = @klass.calculate(@form, @action)
278
+ value ? assert_equal(value, result) : assert_nil(result)
279
+ end
280
+
281
+ it "works for additions" do
282
+ assert_calculation('text.1 + text.2 + text.1', "40")
283
+ end
284
+
285
+ it "works for substraction" do
286
+ assert_calculation('text.2-text\.1', "10")
287
+ end
288
+
289
+ it "works for multiplication" do
290
+ assert_calculation('text.2\* text\.1 * text\.3', "6000")
291
+ end
292
+
293
+ it "works for division" do
294
+ assert_calculation('text.2 /text\.1', "2")
295
+ end
296
+
297
+ it "works with parentheses" do
298
+ assert_calculation('(text.2 + (text.1*text.3))', "320")
299
+ end
300
+
301
+ it "works with numbers" do
302
+ assert_calculation('text.1 + 10.54 - 1,54 + 3', "22")
303
+ end
304
+
305
+ it "works in a more complex case" do
306
+ assert_calculation('(text.1 + text.2)/(text.3) * text.1', "10")
307
+ end
308
+
309
+ it "works with floats" do
310
+ @field1.field_value = "10,54"
311
+ assert_calculation('text.1 + text.2', "30.54")
312
+ end
313
+
314
+ it "fails if a referenced field is not a terminal field" do
315
+ assert_calculation('text + text.2', nil)
316
+ end
317
+
318
+ it "fails if a referenced field does not exist" do
319
+ assert_calculation('unknown + text.2', nil)
320
+ end
321
+
322
+ it "fails if parentheses don't match" do
323
+ assert_calculation('(text.1 + text.2', nil)
324
+ end
325
+ end
326
+ end
327
+ end
@@ -114,6 +114,12 @@ describe HexaPDF::Type::AcroForm::TextField do
114
114
  assert(widget[:AP][:N])
115
115
  end
116
116
 
117
+ it "calls acro_form.on_invalid_value if the provided value is not a string" do
118
+ @doc.config['acro_form.on_invalid_value'] = proc {|_field, value| value.to_s }
119
+ @field.field_value = 10
120
+ assert_equal("10", @field.field_value)
121
+ end
122
+
117
123
  it "fails if the :password flag is set" do
118
124
  @field.flag(:password)
119
125
  assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
@@ -197,6 +203,62 @@ describe HexaPDF::Type::AcroForm::TextField do
197
203
  end
198
204
  end
199
205
 
206
+ describe "set_format_action" do
207
+ it "applies the number format" do
208
+ @doc.acro_form(create: true)
209
+ @field.set_format_action(:number, decimals: 0)
210
+ assert(@field.key?(:AA))
211
+ assert(@field[:AA].key?(:F))
212
+ assert_equal('AFNumber_Format(0, 0, 0, 0, "", true);', @field[:AA][:F][:JS])
213
+ end
214
+
215
+ it "applies the percent format" do
216
+ @doc.acro_form(create: true)
217
+ @field.set_format_action(:percent, decimals: 0)
218
+ assert(@field.key?(:AA))
219
+ assert(@field[:AA].key?(:F))
220
+ assert_equal('AFPercent_Format(0, 0);', @field[:AA][:F][:JS])
221
+ end
222
+
223
+ it "applies the time format" do
224
+ @doc.acro_form(create: true)
225
+ @field.set_format_action(:time, format: :hh_mm_ss)
226
+ assert(@field.key?(:AA))
227
+ assert(@field[:AA].key?(:F))
228
+ assert_equal('AFTime_Format(2);', @field[:AA][:F][:JS])
229
+ end
230
+
231
+ it "fails if an unknown format action is specified" do
232
+ assert_raises(ArgumentError) { @field.set_format_action(:unknown) }
233
+ end
234
+ end
235
+
236
+ describe "set_calculate_action" do
237
+ before do
238
+ @form = @doc.acro_form(create: true)
239
+ @form.create_text_field('text1')
240
+ @form.create_text_field('text2')
241
+ end
242
+
243
+ it "sets the calculate action using AFSimple_Calculate" do
244
+ @field.set_calculate_action(:sum, fields: ['text1', @form.field_by_name('text2')])
245
+ assert(@field.key?(:AA))
246
+ assert(@field[:AA].key?(:C))
247
+ assert_equal('AFSimple_Calculate("SUM", ["text1","text2"]);', @field[:AA][:C][:JS])
248
+ assert_equal([@field], @form[:CO].value)
249
+ end
250
+
251
+ it "sets the simplified field notation calculate action" do
252
+ @field.set_calculate_action(:sfn, fields: "text1")
253
+ assert_equal('/** BVCALC text1 EVCALC **/ event.value = AFMakeNumber(getField("text1").value)',
254
+ @field[:AA][:C][:JS])
255
+ end
256
+
257
+ it "fails if an unknown calculate action is specified" do
258
+ assert_raises(ArgumentError) { @field.set_calculate_action(:unknown) }
259
+ end
260
+ end
261
+
200
262
  describe "validation" do
201
263
  it "checks the value of the /FT field" do
202
264
  @field.delete(:FT)
@@ -146,6 +146,11 @@ describe HexaPDF::Type::Resources do
146
146
  @res.font(:test)
147
147
  end
148
148
  end
149
+
150
+ it "wraps the resulting object in the appropriate font class" do
151
+ @res[:Font] = {test: {Type: :Font, Subtype: :Type1}}
152
+ assert_kind_of(HexaPDF::Type::FontType1, @res.font(:test))
153
+ end
149
154
  end
150
155
 
151
156
  describe "add_font" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.0
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-23 00:00:00.000000000 Z
11
+ date: 2024-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -306,6 +306,7 @@ files:
306
306
  - examples/028-frame_mask_mode.rb
307
307
  - examples/029-composer_fallback_fonts.rb
308
308
  - examples/030-pdfa.rb
309
+ - examples/031-acro_form_java_script.rb
309
310
  - examples/emoji-smile.png
310
311
  - examples/emoji-wink.png
311
312
  - examples/machupicchu.jpg
@@ -324,6 +325,7 @@ files:
324
325
  - lib/hexapdf/cli/modify.rb
325
326
  - lib/hexapdf/cli/optimize.rb
326
327
  - lib/hexapdf/cli/split.rb
328
+ - lib/hexapdf/cli/usage.rb
327
329
  - lib/hexapdf/cli/watermark.rb
328
330
  - lib/hexapdf/composer.rb
329
331
  - lib/hexapdf/configuration.rb
@@ -429,6 +431,7 @@ files:
429
431
  - lib/hexapdf/font_loader/from_configuration.rb
430
432
  - lib/hexapdf/font_loader/from_file.rb
431
433
  - lib/hexapdf/font_loader/standard14.rb
434
+ - lib/hexapdf/font_loader/variant_from_name.rb
432
435
  - lib/hexapdf/image_loader.rb
433
436
  - lib/hexapdf/image_loader/jpeg.rb
434
437
  - lib/hexapdf/image_loader/pdf.rb
@@ -477,6 +480,7 @@ files:
477
480
  - lib/hexapdf/type/acro_form/choice_field.rb
478
481
  - lib/hexapdf/type/acro_form/field.rb
479
482
  - lib/hexapdf/type/acro_form/form.rb
483
+ - lib/hexapdf/type/acro_form/java_script_actions.rb
480
484
  - lib/hexapdf/type/acro_form/signature_field.rb
481
485
  - lib/hexapdf/type/acro_form/text_field.rb
482
486
  - lib/hexapdf/type/acro_form/variable_text_field.rb
@@ -698,6 +702,7 @@ files:
698
702
  - test/hexapdf/font_loader/test_from_configuration.rb
699
703
  - test/hexapdf/font_loader/test_from_file.rb
700
704
  - test/hexapdf/font_loader/test_standard14.rb
705
+ - test/hexapdf/font_loader/test_variant_from_name.rb
701
706
  - test/hexapdf/image_loader/test_jpeg.rb
702
707
  - test/hexapdf/image_loader/test_pdf.rb
703
708
  - test/hexapdf/image_loader/test_png.rb
@@ -748,6 +753,7 @@ files:
748
753
  - test/hexapdf/type/acro_form/test_choice_field.rb
749
754
  - test/hexapdf/type/acro_form/test_field.rb
750
755
  - test/hexapdf/type/acro_form/test_form.rb
756
+ - test/hexapdf/type/acro_form/test_java_script_actions.rb
751
757
  - test/hexapdf/type/acro_form/test_signature_field.rb
752
758
  - test/hexapdf/type/acro_form/test_text_field.rb
753
759
  - test/hexapdf/type/acro_form/test_variable_text_field.rb