hexapdf 0.40.0 → 0.41.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -332,11 +332,28 @@ describe HexaPDF::Encryption::SecurityHandler do
332
332
  end
333
333
 
334
334
  it "enhances a thrown EncryptionError by setting the PDF object" do
335
+ @document.config['encryption.on_decryption_error'] = proc { true }
335
336
  @handler.set_up_encryption(key_length: 256)
336
337
  error = assert_raises(HexaPDF::EncryptionError) { @handler.decrypt(@obj) }
337
338
  assert_match(/Object \(1,0\):/, error.message)
338
339
  end
339
340
 
341
+ it "uses the encryption.on_decryption_error configuration option" do
342
+ @handler.set_up_encryption(key_length: 256)
343
+ @handler.decrypt(@obj)
344
+ assert_equal('', @obj[:Key])
345
+
346
+ @obj[:Key] = @encrypted
347
+ called = false
348
+ @document.config['encryption.on_decryption_error'] = proc do |obj, msg|
349
+ assert_same(@obj, obj)
350
+ assert_match(/32 \+ 16/, msg)
351
+ called = true
352
+ end
353
+ assert_raises(HexaPDF::EncryptionError) { @handler.decrypt(@obj) }
354
+ assert(called)
355
+ end
356
+
340
357
  it "fails if V < 5 and the object number changes" do
341
358
  @obj.oid = 55
342
359
  @handler.decrypt(@obj)
@@ -258,7 +258,7 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
258
258
  end
259
259
 
260
260
  it "fails if the field cannot be decrypted" do
261
- @dict[:Perms].succ!
261
+ @dict[:Perms].setbyte(-1, (@dict[:Perms].getbyte(-1) + 1) % 256)
262
262
  exp = assert_raises(HexaPDF::EncryptionError) { @handler.set_up_decryption(@dict) }
263
263
  assert_match(/cannot be decrypted/, exp.message)
264
264
  end
@@ -33,10 +33,11 @@ describe HexaPDF::Font::CMap::Parser do
33
33
  <8145> <8145> 8123
34
34
  <8146> <8148> 9000
35
35
  endcidrange
36
- 2 beginbfrange
36
+ 4 beginbfrange
37
37
  <0000> <005E> <0020>
38
38
  <1379> <137B> <90FE>
39
39
  <005F> <0061> [ <00660066> <00660069> <00660066006C> ]
40
+ <E040> <E041> <D840DC3D>
40
41
  endbfrange
41
42
  1 beginbfchar
42
43
  <3A51> <D840DC3E>
@@ -82,8 +83,9 @@ describe HexaPDF::Font::CMap::Parser do
82
83
  assert_equal("ff", cmap.to_unicode(0x5F))
83
84
  assert_equal("fi", cmap.to_unicode(0x60))
84
85
  assert_equal("ffl", cmap.to_unicode(0x61))
85
- assert_equal("\xD8\x40\xDC\x3E".encode("UTF-8", "UTF-16BE"),
86
- cmap.to_unicode(0x3A51))
86
+ symbol = "\xD8\x40\xDC\x3E".encode("UTF-8", "UTF-16BE")
87
+ assert_equal(symbol, cmap.to_unicode(0xE041))
88
+ assert_equal(symbol, cmap.to_unicode(0x3A51))
87
89
  assert_nil(cmap.to_unicode(0xFF))
88
90
  end
89
91
 
@@ -97,6 +97,14 @@ describe HexaPDF::Font::CMap do
97
97
  assert_equal("ABC", @cmap.to_unicode(20))
98
98
  end
99
99
 
100
+ it "allows adding a code range to unicode mapping and retrieving the values" do
101
+ @cmap.add_unicode_range_mapping(20, 30, [65])
102
+ @cmap.add_unicode_range_mapping(40, 41, [0xD840, 0xDC3D])
103
+ assert_equal("A", @cmap.to_unicode(20))
104
+ assert_equal("K", @cmap.to_unicode(30))
105
+ assert_equal("𠀾", @cmap.to_unicode(41))
106
+ end
107
+
100
108
  it "returns nil for unknown mappings" do
101
109
  assert_nil(@cmap.to_unicode(20))
102
110
  end
@@ -37,6 +37,10 @@ describe HexaPDF::FontLoader::FromConfiguration do
37
37
  assert_nil(@klass.call(@doc, "Unknown"))
38
38
  end
39
39
 
40
+ it "allows arbitrary keywords arguments" do
41
+ assert_nil(@klass.call(@doc, "Unknown", something: :other))
42
+ end
43
+
40
44
  it "returns a hash with all configured fonts" do
41
45
  assert_equal({'font' => [:none], 'font1' => [:none]}, @klass.available_fonts(@doc))
42
46
  end
@@ -0,0 +1,34 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/font_loader'
5
+ require 'hexapdf/document'
6
+
7
+ describe HexaPDF::FontLoader::VariantFromName do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @obj = HexaPDF::FontLoader::VariantFromName
11
+ end
12
+
13
+ it "loads the font if the name contains a valid variant" do
14
+ wrapper = @obj.call(@doc, "Helvetica bold")
15
+ assert_equal("Helvetica-Bold", wrapper.wrapped_font.font_name)
16
+ wrapper = @obj.call(@doc, "Helvetica italic")
17
+ assert_equal("Helvetica-Oblique", wrapper.wrapped_font.font_name)
18
+ wrapper = @obj.call(@doc, "Helvetica bold_italic")
19
+ assert_equal("Helvetica-BoldOblique", wrapper.wrapped_font.font_name)
20
+ end
21
+
22
+ it "returns nil if the font name contains an unknown variant" do
23
+ assert_nil(@obj.call(@doc, "Helvetica oblique"))
24
+ end
25
+
26
+ it "ignores a supplied variant keyword argument" do
27
+ wrapper = @obj.call(@doc, "Helvetica bold", variant: :italic)
28
+ assert_equal("Helvetica-Bold", wrapper.wrapped_font.font_name)
29
+ end
30
+
31
+ it "returns nil for unknown fonts" do
32
+ assert_nil(@obj.call(@doc, "Unknown"))
33
+ end
34
+ end
@@ -235,6 +235,12 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
235
235
  assert(@widget.flagged?(:print))
236
236
  end
237
237
 
238
+ it "removes the hidden flag on the widgets" do
239
+ @widget.flag(:hidden)
240
+ @generator.create_appearances
241
+ refute(@widget.flagged?(:hidden))
242
+ end
243
+
238
244
  it "adjusts the /Rect if width is zero" do
239
245
  @generator.create_appearances
240
246
  assert_equal(12, @widget[:Rect].width)
@@ -272,6 +278,19 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
272
278
  end
273
279
  end
274
280
 
281
+ it "creates the needed appearance dictionary for the normal appearance" do
282
+ @widget[:AP][:N] = 5
283
+ @generator.create_appearances
284
+ assert_equal(:XObject, @widget[:AP][:N][:Off].type)
285
+ assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
286
+
287
+ @field[:V] = :Other
288
+ @widget[:AP][:N] = @doc.wrap({Type: :XObject, Subtype: :Form})
289
+ @generator.create_appearances
290
+ assert_equal(:XObject, @widget[:AP][:N][:Off].type)
291
+ assert_equal(:XObject, @widget[:AP][:N][:Other].type)
292
+ end
293
+
275
294
  it "creates the needed appearance streams" do
276
295
  @widget[:AP][:N].delete(:Off)
277
296
  @generator.create_appearances
@@ -309,7 +328,7 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
309
328
  [:restore_graphics_state]])
310
329
  end
311
330
 
312
- it "fails if the appearance dictionaries are not set up" do
331
+ it "fails if the appearance dictionary doesn't contain a name for the on state" do
313
332
  @widget[:AP][:N].delete(:Yes)
314
333
  assert_raises(HexaPDF::Error) { @generator.create_appearances }
315
334
  end
@@ -371,6 +390,12 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
371
390
  assert(@widget.flagged?(:print))
372
391
  end
373
392
 
393
+ it "removes the hidden flag on the widgets" do
394
+ @widget.flag(:hidden)
395
+ @generator.create_appearances
396
+ refute(@widget.flagged?(:hidden))
397
+ end
398
+
374
399
  describe "it adjusts the :Rect when necessary" do
375
400
  before do
376
401
  @widget.border_style(width: 3)
@@ -549,52 +574,11 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
549
574
  end
550
575
  end
551
576
 
552
- describe "Javascript action AFNumber_Format" do
553
- before do
554
- @field[:V] = '1234567.898765'
555
- @action = {S: :JavaScript, JS: ''}
556
- @field[:AA] = {F: @action}
557
- end
558
-
559
- def assert_format(arg_string, result, range)
560
- @action[:JS] = "AFNumber_Format(#{arg_string});"
561
- @generator.create_appearances
562
- assert_operators(@widget[:AP][:N].stream, result, range: range)
563
- end
564
-
565
- it "respects the set number of decimals" do
566
- assert_format('0, 2, 0, 0, "E", false',
567
- [:show_text, ["1.234.568E"]], 9)
568
- assert_format('2, 2, 0, 0, "E", false',
569
- [:show_text, ["1.234.567,90E"]], 9)
570
- end
571
-
572
- it "respects the digit separator style" do
573
- ["1,234,567.90", "1234567.90", "1.234.567,90", "1234567,90"].each_with_index do |result, style|
574
- assert_format("2, #{style}, 0, 0, \"\", false", [:show_text, [result]], 9)
575
- end
576
- end
577
-
578
- it "respects the negative value styling" do
579
- @field[:V] = '-1234567.898'
580
- @widget[:Rect].height = 11.25
581
- ["-E1234567,90", "E1234567,90", "(E1234567,90)", "(E1234567,90)"].each_with_index do |result, style|
582
- assert_format("2, 3, #{style}, 0, \"E\", true",
583
- [[:set_device_rgb_non_stroking_color, [style % 2, 0.0, 0.0]],
584
- [:begin_text],
585
- [:set_text_matrix, [1, 0, 0, 1, 2, 3.183272]],
586
- [:show_text, [result]]], 6..9)
587
- end
588
- end
589
-
590
- it "respects the specified currency string and position" do
591
- assert_format('2, 3, 0, 0, " E", false', [:show_text, ["1234567,90 E"]], 9)
592
- assert_format('2, 3, 0, 0, "E ", true', [:show_text, ["E 1234567,90"]], 9)
593
- end
594
-
595
- it "does nothing to the value if the Javascript method could not be determined " do
596
- assert_format('2, 3, 0, 0, " E", false, a', [:show_text, ["1234567.898765"]], 8)
597
- end
577
+ it "applies JavaScript format actions" do
578
+ @field[:V] = '1234567.898765'
579
+ @field[:AA] = {F: {S: :JavaScript, JS: 'AFNumber_Format(0, 2, 0, 0, "E", false);'}}
580
+ @generator.create_appearances
581
+ assert_operators(@widget[:AP][:N].stream, [:show_text, ["1.234.568E"]], range: 9)
598
582
  end
599
583
 
600
584
  it "creates the /N appearance stream according to the set string" do
@@ -34,6 +34,13 @@ describe HexaPDF::Type::AcroForm::Field do
34
34
  assert_equal(:Ch, @field[:FT])
35
35
  end
36
36
 
37
+ it "wraps fields inside the correct subclass" do
38
+ field = HexaPDF::Type::AcroForm::Field.wrap(@doc, {FT: :Tx})
39
+ assert_kind_of(HexaPDF::Type::AcroForm::TextField, field)
40
+ field = HexaPDF::Type::AcroForm::Field.wrap(@doc, {})
41
+ assert_kind_of(HexaPDF::Type::AcroForm::Field, field)
42
+ end
43
+
37
44
  it "has convenience methods for accessing the field flags" do
38
45
  assert_equal([], @field.flags)
39
46
  refute(@field.flagged?(:required))
@@ -100,6 +107,10 @@ describe HexaPDF::Type::AcroForm::Field do
100
107
  refute(@field.terminal_field?)
101
108
  end
102
109
 
110
+ it "returns itself when asked for the form field" do
111
+ assert_same(@field, @field.form_field)
112
+ end
113
+
103
114
  it "can check whether a widget is embedded in the field" do
104
115
  refute(@field.embedded_widget?)
105
116
  @field[:Subtype] = :Wdiget
@@ -342,6 +342,39 @@ describe HexaPDF::Type::AcroForm::Form do
342
342
  end
343
343
  end
344
344
 
345
+ describe "recalculate_fields" do
346
+ before do
347
+ @text1 = @acro_form.create_text_field('text1')
348
+ @text2 = @acro_form.create_text_field('text2')
349
+ @text3 = @acro_form.create_text_field('text3')
350
+ end
351
+
352
+ it "recalculates all fields listed in /CO" do
353
+ @text1.field_value = "10"
354
+ @text2.field_value = "30"
355
+ @text3.set_calculate_action(:sum, fields: ['text1', @text2])
356
+ @acro_form.recalculate_fields
357
+ assert_equal("40", @text3.field_value)
358
+ end
359
+
360
+ it "doesn't change the field's value if there is an error" do
361
+ @text3.set_calculate_action(:sfn, fields: 'text1 - text2')
362
+ @text3[:AA][:C][:JS] = @text3[:AA][:C][:JS].sub('text1', 'text4')
363
+ @acro_form.recalculate_fields
364
+ assert_nil(@text3.field_value)
365
+ end
366
+
367
+ it "works if fields aren't already loaded and correctly wrapped" do
368
+ @text1.field_value = "10"
369
+ @text3.set_calculate_action(:sfn, fields: 'text1')
370
+ @text3[:AA] = {C: HexaPDF::Reference.new(@doc.add(@text3[:AA][:C]).oid)}
371
+ @acro_form[:CO] = [HexaPDF::Reference.new(@text3.oid, @text3.gen)]
372
+ @doc.revisions.current.update(@doc.wrap(@text3, type: HexaPDF::Dictionary))
373
+ @acro_form.recalculate_fields
374
+ assert_equal("10", @text3.field_value)
375
+ end
376
+ end
377
+
345
378
  describe "perform_validation" do
346
379
  it "checks whether the /DR field is available when /DA is set" do
347
380
  @acro_form[:DA] = 'test'
@@ -0,0 +1,226 @@
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
+ def assert_format(arg_string, result_value, result_color)
35
+ @action[:JS] = "AFNumber_Format(#{arg_string});"
36
+ value, text_color = @klass.apply_format(@value, @action)
37
+ assert_equal(result_value, value)
38
+ result_color ? assert_equal(result_color, text_color) : assert_nil(text_color)
39
+ end
40
+
41
+ it "works with both commas and points as decimal separator" do
42
+ @value = '1234567.898'
43
+ assert_format('2, 2, 0, 0, "", false', "1.234.567,90", "black")
44
+ @value = '1234567,898'
45
+ assert_format('2, 2, 0, 0, "", false', "1.234.567,90", "black")
46
+ @value = '123,4567,898'
47
+ assert_format('2, 2, 0, 0, "", false', "123,46", "black")
48
+ end
49
+
50
+ it "respects the set number of decimals" do
51
+ assert_format('0, 2, 0, 0, "E", false', "1.234.568E", "black")
52
+ assert_format('2, 2, 0, 0, "E", false', "1.234.567,90E", "black")
53
+ end
54
+
55
+ it "respects the digit separator style" do
56
+ ["1,234,567.90", "1234567.90", "1.234.567,90", "1234567,90"].each_with_index do |result, style|
57
+ assert_format("2, #{style}, 0, 0, \"\", false", result, "black")
58
+ end
59
+ end
60
+
61
+ it "respects the negative value styling" do
62
+ @value = '-1234567.898'
63
+ [["-E1234567,90", "black"], ["E1234567,90", "red"], ["(E1234567,90)", "black"],
64
+ ["(E1234567,90)", "red"]].each_with_index do |result, style|
65
+ assert_format("2, 3, #{style}, 0, \"E\", true", result[0], result[1])
66
+ end
67
+ end
68
+
69
+ it "respects the specified currency string and position" do
70
+ assert_format('2, 3, 0, 0, " E", false', "1234567,90 E", "black")
71
+ assert_format('2, 3, 0, 0, "E ", true', "E 1234567,90", "black")
72
+ end
73
+
74
+ it "allows omitting the trailing semicolon" do
75
+ @action[:JS] = "AFNumber_Format(2,2,0,0,\"\",false )"
76
+ value, = @klass.apply_format('1234.567', @action)
77
+ assert_equal('1.234,57', value)
78
+ end
79
+
80
+ it "does nothing to the value if the JavasSript method could not be determined " do
81
+ assert_format('2, 3, 0, 0, " E", false, a', "1234567.898765", nil)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "calculate" do
87
+ before do
88
+ @doc = HexaPDF::Document.new
89
+ @form = @doc.acro_form(create: true)
90
+ @form.create_text_field('text')
91
+ @field1 = @form.create_text_field('text.1')
92
+ @field1.field_value = "10"
93
+ @field2 = @form.create_text_field('text.2')
94
+ @field2.field_value = "20"
95
+ @field3 = @form.create_text_field('text.3')
96
+ @field3.field_value = "30"
97
+ end
98
+
99
+ it "returns nil if the calculate action is not a JavaScript action" do
100
+ @action[:S] = :GoTo
101
+ assert_nil(@klass.calculate(@form, @action))
102
+ end
103
+
104
+ it "returns nil if the calculate action contains unknown JavaScript" do
105
+ @action[:JS] = 'Unknown();'
106
+ assert_nil(@klass.calculate(@form, @action))
107
+ end
108
+
109
+ describe "predefined calculations" do
110
+ it "returns a correct JavaScript string" do
111
+ assert_equal('AFSimple_Calculate("SUM", ["text.1","text.2"]);',
112
+ @klass.af_simple_calculate_action(:sum, ['text.1', @field2]))
113
+ end
114
+
115
+ def assert_calculation(function, fields, value)
116
+ fields = fields.map {|field| "\"#{field.full_field_name}\"" }.join(", ")
117
+ @action[:JS] = "AFSimple_Calculate(\"#{function}\", new Array(#{fields}));"
118
+ assert_equal(value, @klass.calculate(@form, @action))
119
+ end
120
+
121
+ it "can sum fields" do
122
+ assert_calculation('SUM', [@field1, @field2, @field3], "60")
123
+ end
124
+
125
+ it "can average fields" do
126
+ assert_calculation('AVG', [@field1, @field2, @field3], "20")
127
+ end
128
+
129
+ it "can multiply fields" do
130
+ assert_calculation('PRD', [@field1, @field2, @field3], "6000")
131
+ end
132
+
133
+ it "can find the minimum field value" do
134
+ assert_calculation('MIN', [@field1, @field2, @field3], "10")
135
+ end
136
+
137
+ it "can find the maximum field value" do
138
+ assert_calculation('MAX', [@field1, @field2, @field3], "30")
139
+ end
140
+
141
+ it "works with floats" do
142
+ @field1.field_value = "10,54"
143
+ assert_calculation('SUM', [@field1, @field2], "30.54")
144
+ end
145
+
146
+ it "returns nil if a field cannot be resolved" do
147
+ @action[:JS] = 'AFSimple_Calculate("SUM", ["unknown"]);'
148
+ assert_nil(@klass.calculate(@form, @action))
149
+ end
150
+
151
+ it "allows omitting the trailing semicolon" do
152
+ @action[:JS] = 'AFSimple_Calculate("SUM", ["text.1"] )'
153
+ assert_equal("10", @klass.calculate(@form, @action))
154
+ end
155
+ end
156
+
157
+ describe "simplified field notation calculations" do
158
+ it "returns a correct JavaScript string" do
159
+ sfn = '(text.1 + text.2) * text.3 - text.1 / text.1 + 0 + 5.43 + 7,24'
160
+ assert_equal("/** BVCALC #{sfn} EVCALC **/ " \
161
+ 'event.value = (AFMakeNumber(getField("text.1").value) + ' \
162
+ 'AFMakeNumber(getField("text.2").value)) * ' \
163
+ 'AFMakeNumber(getField("text.3").value) - ' \
164
+ 'AFMakeNumber(getField("text.1").value) / ' \
165
+ 'AFMakeNumber(getField("text.1").value) ' \
166
+ '+ 0.0 + 5.43 + 7.24',
167
+ @klass.simplified_field_notation_action(@form, sfn))
168
+ end
169
+
170
+ it "fails if the SFN string is invalid when generating a JavaScript action string" do
171
+ assert_raises(ArgumentError) { @klass.simplified_field_notation_action(@form, '(test') }
172
+ end
173
+
174
+ def assert_calculation(sfn, value)
175
+ @action[:JS] = "/** BVCALC #{sfn} EVCALC **/"
176
+ result = @klass.calculate(@form, @action)
177
+ value ? assert_equal(value, result) : assert_nil(result)
178
+ end
179
+
180
+ it "works for additions" do
181
+ assert_calculation('text.1 + text.2 + text.1', "40")
182
+ end
183
+
184
+ it "works for substraction" do
185
+ assert_calculation('text.2-text\.1', "10")
186
+ end
187
+
188
+ it "works for multiplication" do
189
+ assert_calculation('text.2\* text\.1 * text\.3', "6000")
190
+ end
191
+
192
+ it "works for division" do
193
+ assert_calculation('text.2 /text\.1', "2")
194
+ end
195
+
196
+ it "works with parentheses" do
197
+ assert_calculation('(text.2 + (text.1*text.3))', "320")
198
+ end
199
+
200
+ it "works with numbers" do
201
+ assert_calculation('text.1 + 10.54 - 1,54 + 3', "22")
202
+ end
203
+
204
+ it "works in a more complex case" do
205
+ assert_calculation('(text.1 + text.2)/(text.3) * text.1', "10")
206
+ end
207
+
208
+ it "works with floats" do
209
+ @field1.field_value = "10,54"
210
+ assert_calculation('text.1 + text.2', "30.54")
211
+ end
212
+
213
+ it "fails if a referenced field is not a terminal field" do
214
+ assert_calculation('text + text.2', nil)
215
+ end
216
+
217
+ it "fails if a referenced field does not exist" do
218
+ assert_calculation('unknown + text.2', nil)
219
+ end
220
+
221
+ it "fails if parentheses don't match" do
222
+ assert_calculation('(text.1 + text.2', nil)
223
+ end
224
+ end
225
+ end
226
+ end
@@ -124,6 +124,10 @@ describe HexaPDF::Type::AcroForm::TextField do
124
124
  assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
125
125
  end
126
126
 
127
+ it "fails if the provided value is not a string" do
128
+ assert_raises(HexaPDF::Error) { @field.field_value = 10 }
129
+ end
130
+
127
131
  it "fails if the value exceeds the length set by /MaxLen" do
128
132
  @field[:MaxLen] = 5
129
133
  assert_raises(HexaPDF::Error) { @field.field_value = 'testdf' }
@@ -197,6 +201,46 @@ describe HexaPDF::Type::AcroForm::TextField do
197
201
  end
198
202
  end
199
203
 
204
+ describe "set_format_action" do
205
+ it "applies the number format" do
206
+ @doc.acro_form(create: true)
207
+ @field.set_format_action(:number, decimals: 0)
208
+ assert(@field.key?(:AA))
209
+ assert(@field[:AA].key?(:F))
210
+ assert_equal('AFNumber_Format(0, 0, 0, 0, "", true);', @field[:AA][:F][:JS])
211
+ end
212
+
213
+ it "fails if an unknown format action is specified" do
214
+ assert_raises(ArgumentError) { @field.set_format_action(:unknown) }
215
+ end
216
+ end
217
+
218
+ describe "set_calculate_action" do
219
+ before do
220
+ @form = @doc.acro_form(create: true)
221
+ @form.create_text_field('text1')
222
+ @form.create_text_field('text2')
223
+ end
224
+
225
+ it "sets the calculate action using AFSimple_Calculate" do
226
+ @field.set_calculate_action(:sum, fields: ['text1', @form.field_by_name('text2')])
227
+ assert(@field.key?(:AA))
228
+ assert(@field[:AA].key?(:C))
229
+ assert_equal('AFSimple_Calculate("SUM", ["text1","text2"]);', @field[:AA][:C][:JS])
230
+ assert_equal([@field], @form[:CO].value)
231
+ end
232
+
233
+ it "sets the simplified field notation calculate action" do
234
+ @field.set_calculate_action(:sfn, fields: "text1")
235
+ assert_equal('/** BVCALC text1 EVCALC **/ event.value = AFMakeNumber(getField("text1").value)',
236
+ @field[:AA][:C][:JS])
237
+ end
238
+
239
+ it "fails if an unknown calculate action is specified" do
240
+ assert_raises(ArgumentError) { @field.set_calculate_action(:unknown) }
241
+ end
242
+ end
243
+
200
244
  describe "validation" do
201
245
  it "checks the value of the /FT field" do
202
246
  @field.delete(:FT)
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.41.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-05 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
@@ -429,6 +430,7 @@ files:
429
430
  - lib/hexapdf/font_loader/from_configuration.rb
430
431
  - lib/hexapdf/font_loader/from_file.rb
431
432
  - lib/hexapdf/font_loader/standard14.rb
433
+ - lib/hexapdf/font_loader/variant_from_name.rb
432
434
  - lib/hexapdf/image_loader.rb
433
435
  - lib/hexapdf/image_loader/jpeg.rb
434
436
  - lib/hexapdf/image_loader/pdf.rb
@@ -477,6 +479,7 @@ files:
477
479
  - lib/hexapdf/type/acro_form/choice_field.rb
478
480
  - lib/hexapdf/type/acro_form/field.rb
479
481
  - lib/hexapdf/type/acro_form/form.rb
482
+ - lib/hexapdf/type/acro_form/java_script_actions.rb
480
483
  - lib/hexapdf/type/acro_form/signature_field.rb
481
484
  - lib/hexapdf/type/acro_form/text_field.rb
482
485
  - lib/hexapdf/type/acro_form/variable_text_field.rb
@@ -698,6 +701,7 @@ files:
698
701
  - test/hexapdf/font_loader/test_from_configuration.rb
699
702
  - test/hexapdf/font_loader/test_from_file.rb
700
703
  - test/hexapdf/font_loader/test_standard14.rb
704
+ - test/hexapdf/font_loader/test_variant_from_name.rb
701
705
  - test/hexapdf/image_loader/test_jpeg.rb
702
706
  - test/hexapdf/image_loader/test_pdf.rb
703
707
  - test/hexapdf/image_loader/test_png.rb
@@ -748,6 +752,7 @@ files:
748
752
  - test/hexapdf/type/acro_form/test_choice_field.rb
749
753
  - test/hexapdf/type/acro_form/test_field.rb
750
754
  - test/hexapdf/type/acro_form/test_form.rb
755
+ - test/hexapdf/type/acro_form/test_java_script_actions.rb
751
756
  - test/hexapdf/type/acro_form/test_signature_field.rb
752
757
  - test/hexapdf/type/acro_form/test_text_field.rb
753
758
  - test/hexapdf/type/acro_form/test_variable_text_field.rb