hexapdf 0.40.0 → 0.42.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -36,6 +36,7 @@
36
36
 
37
37
  require 'hexapdf/error'
38
38
  require 'hexapdf/type/acro_form/variable_text_field'
39
+ require 'hexapdf/type/acro_form/java_script_actions'
39
40
 
40
41
  module HexaPDF
41
42
  module Type
@@ -168,6 +169,8 @@ module HexaPDF
168
169
  raise HexaPDF::Error, "Storing a field value for a password field is not allowed"
169
170
  elsif comb_text_field? && !key?(:MaxLen)
170
171
  raise HexaPDF::Error, "A comb text field need a valid /MaxLen value"
172
+ elsif str && !str.kind_of?(String)
173
+ str = @document.config['acro_form.on_invalid_value'].call(self, str)
171
174
  end
172
175
  str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
173
176
  if key?(:MaxLen) && str && str.length > self[:MaxLen]
@@ -241,6 +244,93 @@ module HexaPDF
241
244
  create_appearances(force: true)
242
245
  end
243
246
 
247
+ # Sets the specified JavaScript format action on the field's widgets.
248
+ #
249
+ # This action is executed when the field value needs to be formatted for rendering in the
250
+ # appearance streams of the associated widgets.
251
+ #
252
+ # The argument +type+ can be one of the following:
253
+ #
254
+ # :number::
255
+ # Assumes that the field value is a number and formats it according to the given
256
+ # arguments. See JavaScriptActions.af_number_format_action for details on the arguments.
257
+ #
258
+ # :percent::
259
+ # Assumes that the field value is a number and formats it as percentage (where 1=100%
260
+ # and 0=0%). See JavaScriptActions.af_percent_format_action for details on the
261
+ # arguments.
262
+ #
263
+ # :time::
264
+ # Assumes that the field value is a string with a time value and formats it according to
265
+ # the given argument. See JavaScriptActions.af_time_format_action for details on the
266
+ # arguments.
267
+ def set_format_action(type, **arguments)
268
+ action_string = case type
269
+ when :number then JavaScriptActions.af_number_format_action(**arguments)
270
+ when :percent then JavaScriptActions.af_percent_format_action(**arguments)
271
+ when :time then JavaScriptActions.af_time_format_action(**arguments)
272
+ else
273
+ raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
274
+ end
275
+ self[:AA] ||= {}
276
+ self[:AA][:F] = {S: :JavaScript, JS: action_string}
277
+ end
278
+
279
+ # Sets the specified JavaScript calculate action on the field.
280
+ #
281
+ # This action is executed by a viewer when any field's value changes so as to recalculate
282
+ # the value of this field. Usually, the field is also flagged as read only to avoid a user
283
+ # changing the value manually.
284
+ #
285
+ # Note that HexaPDF *doesn't* automatically recalculate field values, use
286
+ # Form#recalculate_fields to manually kick off recalculation.
287
+ #
288
+ # The argument +type+ can be one of the following:
289
+ #
290
+ # :sum::
291
+ # Sums the values of the given +fields+.
292
+ #
293
+ # :average::
294
+ # Calculates the average value of the given +fields+.
295
+ #
296
+ # :product::
297
+ # Multiplies the values of the given +fields+.
298
+ #
299
+ # :min::
300
+ # Uses the minimum value of the given +fields+.
301
+ #
302
+ # :max::
303
+ # Uses the maximum value of the given +fields+.
304
+ #
305
+ # :sfn::
306
+ # Uses the Simplified Field Notation for calculating the field's value. This allows for
307
+ # more complex calculations involving addition, subtraction, multiplication and
308
+ # division. Field values are specified by using the full field names which should not
309
+ # contain spaces or punctuation characters except point.
310
+ #
311
+ # The +fields+ argument needs to contain a string with the SFN calculation rule.
312
+ #
313
+ # Here are some examples:
314
+ #
315
+ # field1 + field2 - field3
316
+ # (field.1 + field.2) * (field.3 - field.4)
317
+ #
318
+ # Note: Setting this action appends the field to the main Form's /CO entry which specifies
319
+ # the calculation order. Rearrange the entries as needed.
320
+ def set_calculate_action(type, fields: nil)
321
+ action_string = case type
322
+ when :sum, :average, :product, :min, :max
323
+ JavaScriptActions.af_simple_calculate_action(type, fields)
324
+ when :sfn
325
+ JavaScriptActions.simplified_field_notation_action(document.acro_form, fields)
326
+ else
327
+ raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
328
+ end
329
+ self[:AA] ||= {}
330
+ self[:AA][:C] = {S: :JavaScript, JS: action_string}
331
+ (document.acro_form[:CO] ||= []) << self
332
+ end
333
+
244
334
  private
245
335
 
246
336
  def perform_validation #:nodoc:
@@ -51,6 +51,7 @@ module HexaPDF
51
51
  autoload(:SignatureField, 'hexapdf/type/acro_form/signature_field')
52
52
 
53
53
  autoload(:AppearanceGenerator, 'hexapdf/type/acro_form/appearance_generator')
54
+ autoload(:JavaScriptActions, 'hexapdf/type/acro_form/java_script_actions')
54
55
 
55
56
  end
56
57
 
@@ -85,7 +85,7 @@ module HexaPDF
85
85
  define_field :BS, type: :Border, version: '1.2'
86
86
  define_field :Parent, type: Dictionary
87
87
 
88
- # Returs the AcroForm field object to which this widget annotation belongs.
88
+ # Returns the AcroForm field object to which this widget annotation belongs.
89
89
  #
90
90
  # Since a widget and a field can share the same dictionary object, the returned object is
91
91
  # often just the widget re-wrapped in the correct field class.
@@ -143,7 +143,8 @@ module HexaPDF
143
143
  #
144
144
  # If the dictionary is not found, an error is raised.
145
145
  def font(name)
146
- object_getter(:Font, name)
146
+ font = object_getter(:Font, name)
147
+ font.kind_of?(Hash) ? document.wrap(font) : font
147
148
  end
148
149
 
149
150
  # Adds the font dictionary to the resources and returns the name under which it is stored.
data/lib/hexapdf/utils.rb CHANGED
@@ -39,8 +39,27 @@ require 'geom2d/utils'
39
39
  module HexaPDF
40
40
 
41
41
  # This module contains helper methods for the whole library.
42
+ #
43
+ # Furthermore, it refines Numeric to provide #mm, #cm, and #inch methods.
42
44
  module Utils
43
45
 
46
+ refine Numeric do
47
+ # Intrepeting self as millimeters returns the equivalent number of points.
48
+ def mm
49
+ self * 72 / 25.4
50
+ end
51
+
52
+ # Intrepeting self as centimeters returns the equivalent number of points.
53
+ def cm
54
+ self * 72 / 2.54
55
+ end
56
+
57
+ # Intrepeting self as inches returns the equivalent number of points.
58
+ def inch
59
+ self * 72
60
+ end
61
+ end
62
+
44
63
  # The precision with which to compare floating point numbers.
45
64
  #
46
65
  # This is chosen with respect to precision that is used for serializing floating point numbers.
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.40.0'
40
+ VERSION = '0.42.0'
41
41
 
42
42
  end
@@ -17,7 +17,6 @@ describe HexaPDF::Encryption::AES do
17
17
  end
18
18
 
19
19
  def process(data)
20
- raise "invalid data" if data.empty? || data.length % 16 != 0
21
20
  data
22
21
  end
23
22
  end
@@ -56,10 +55,17 @@ describe HexaPDF::Encryption::AES do
56
55
  assert_equal('', @algorithm_class.decrypt('some key' * 2, 'iv' * 8))
57
56
  end
58
57
 
59
- it "fails on decryption if not enough bytes are provided" do
58
+ it "handles invalid files where not enough bytes are provided" do
59
+ @algorithm_class.decrypt('some' * 4, 'a' * 36) do |msg|
60
+ assert_match(/32 \+ 16/, msg)
61
+ false
62
+ end
60
63
  assert_raises(HexaPDF::EncryptionError) do
61
64
  @algorithm_class.decrypt('some' * 4, 'no iv')
62
65
  end
66
+ assert_raises(HexaPDF::EncryptionError) do
67
+ @algorithm_class.decrypt('some' * 4, 'no iv') { true }
68
+ end
63
69
  end
64
70
  end
65
71
 
@@ -126,12 +132,16 @@ describe HexaPDF::Encryption::AES do
126
132
  assert_equal('', collector(@algorithm_class.decryption_fiber('key', Fiber.new { '' })))
127
133
  end
128
134
 
129
- it "fails on decryption if not enough bytes are provided" do
130
- [4, 20, 40].each do |length|
131
- assert_raises(HexaPDF::EncryptionError) do
132
- collector(@algorithm_class.decryption_fiber('some' * 4,
133
- Fiber.new { 'a' * length }))
134
- end
135
+ it "handles invalid files where not enough bytes are provided" do
136
+ collector(@algorithm_class.decryption_fiber('some' * 4, Fiber.new { 'a' * 40 }) do |msg|
137
+ assert_match(/32 \+ 16/, msg)
138
+ false
139
+ end)
140
+ assert_raises(HexaPDF::EncryptionError) do
141
+ collector(@algorithm_class.decryption_fiber('some' * 4, Fiber.new { 'a' * 40 }))
142
+ end
143
+ assert_raises(HexaPDF::EncryptionError) do
144
+ collector(@algorithm_class.decryption_fiber('some' * 4, Fiber.new { 'a' * 40 })) { true }
135
145
  end
136
146
  end
137
147
  end
@@ -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
@@ -6,6 +6,22 @@ require 'hexapdf/utils'
6
6
  describe HexaPDF::Utils do
7
7
  include HexaPDF::Utils
8
8
 
9
+ describe "Numeric refinement" do
10
+ using HexaPDF::Utils
11
+
12
+ it "converts mm to points" do
13
+ assert_equal(72, 25.4.mm)
14
+ end
15
+
16
+ it "converts cm to points" do
17
+ assert_equal(72, 2.54.cm)
18
+ end
19
+
20
+ it "converts inch to points" do
21
+ assert_equal(144, 2.inch)
22
+ end
23
+ end
24
+
9
25
  it "checks floats for equality with a certain precision" do
10
26
  assert(float_equal(1.0, 1))
11
27
  assert(float_equal(1.0, 1.0000003))
@@ -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
@@ -93,6 +93,11 @@ describe HexaPDF::Type::AcroForm::ButtonField do
93
93
  @field.field_value = "check"
94
94
  assert_equal(:check, @field[:V])
95
95
  assert_raises(HexaPDF::Error) { @field.field_value = :unknown }
96
+
97
+ @field.field_value = :Off
98
+ @field.create_widget(@doc.pages[0], value: :other)
99
+ @field.field_value = true
100
+ assert_equal(:check, @field[:V])
96
101
  end
97
102
 
98
103
  it "returns the correct concrete field type" 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
@@ -241,6 +241,53 @@ describe HexaPDF::Type::AcroForm::Form do
241
241
  end
242
242
  end
243
243
 
244
+ describe "fill" do
245
+ it "works for text field types" do
246
+ field = @acro_form.create_text_field('test')
247
+ @acro_form.fill("test" => "value")
248
+ assert_equal("value", field.field_value)
249
+ end
250
+
251
+ it "works for radio buttons" do
252
+ field = @acro_form.create_radio_button("test")
253
+ field.create_widget(@doc.pages.add, value: :name)
254
+ @acro_form.fill("test" => "name")
255
+ assert_equal(:name, field.field_value)
256
+ end
257
+
258
+ it "works for check boxes" do
259
+ field = @acro_form.create_check_box('test')
260
+ field.create_widget(@doc.pages.add)
261
+
262
+ ["t", "true", "y", "yes"].each do |value|
263
+ @acro_form.fill("test" => value)
264
+ assert_equal(:Yes, field.field_value)
265
+ field.field_value = :Off
266
+ end
267
+
268
+ ["f", "false", "n", "no"].each do |value|
269
+ @acro_form.fill("test" => value)
270
+ assert_nil(field.field_value)
271
+ field.field_value = :Yes
272
+ end
273
+
274
+ field.create_widget(@doc.pages.add, value: :Other)
275
+ @acro_form.fill("test" => "Other")
276
+ assert_equal(:Other, field.field_value)
277
+ end
278
+
279
+ it "raises an error if a field is not found" do
280
+ error = assert_raises(HexaPDF::Error) { @acro_form.fill("unknown" => "test") }
281
+ assert_match(/named 'unknown' not found/, error.message)
282
+ end
283
+
284
+ it "raises an error if a field type is not supported for filling in" do
285
+ @acro_form.create_check_box('test').initialize_as_push_button
286
+ error = assert_raises(HexaPDF::Error) { @acro_form.fill("test" => "test") }
287
+ assert_match(/push_button not yet supported/, error.message)
288
+ end
289
+ end
290
+
244
291
  it "returns the default resources" do
245
292
  assert_kind_of(HexaPDF::Type::Resources, @acro_form.default_resources)
246
293
  end
@@ -342,6 +389,39 @@ describe HexaPDF::Type::AcroForm::Form do
342
389
  end
343
390
  end
344
391
 
392
+ describe "recalculate_fields" do
393
+ before do
394
+ @text1 = @acro_form.create_text_field('text1')
395
+ @text2 = @acro_form.create_text_field('text2')
396
+ @text3 = @acro_form.create_text_field('text3')
397
+ end
398
+
399
+ it "recalculates all fields listed in /CO" do
400
+ @text1.field_value = "10"
401
+ @text2.field_value = "30"
402
+ @text3.set_calculate_action(:sum, fields: ['text1', @text2])
403
+ @acro_form.recalculate_fields
404
+ assert_equal("40", @text3.field_value)
405
+ end
406
+
407
+ it "doesn't change the field's value if there is an error" do
408
+ @text3.set_calculate_action(:sfn, fields: 'text1 - text2')
409
+ @text3[:AA][:C][:JS] = @text3[:AA][:C][:JS].sub('text1', 'text4')
410
+ @acro_form.recalculate_fields
411
+ assert_nil(@text3.field_value)
412
+ end
413
+
414
+ it "works if fields aren't already loaded and correctly wrapped" do
415
+ @text1.field_value = "10"
416
+ @text3.set_calculate_action(:sfn, fields: 'text1')
417
+ @text3[:AA] = {C: HexaPDF::Reference.new(@doc.add(@text3[:AA][:C]).oid)}
418
+ @acro_form[:CO] = [HexaPDF::Reference.new(@text3.oid, @text3.gen)]
419
+ @doc.revisions.current.update(@doc.wrap(@text3, type: HexaPDF::Dictionary))
420
+ @acro_form.recalculate_fields
421
+ assert_equal("10", @text3.field_value)
422
+ end
423
+ end
424
+
345
425
  describe "perform_validation" do
346
426
  it "checks whether the /DR field is available when /DA is set" do
347
427
  @acro_form[:DA] = 'test'