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
@@ -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'