hexapdf 0.41.0 → 0.43.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 +55 -0
  3. data/Rakefile +1 -1
  4. data/examples/031-acro_form_java_script.rb +36 -24
  5. data/lib/hexapdf/cli/command.rb +14 -11
  6. data/lib/hexapdf/cli/files.rb +31 -7
  7. data/lib/hexapdf/cli/form.rb +10 -31
  8. data/lib/hexapdf/cli/inspect.rb +1 -1
  9. data/lib/hexapdf/cli/usage.rb +215 -0
  10. data/lib/hexapdf/cli.rb +2 -0
  11. data/lib/hexapdf/configuration.rb +1 -1
  12. data/lib/hexapdf/dictionary.rb +3 -3
  13. data/lib/hexapdf/document.rb +14 -1
  14. data/lib/hexapdf/encryption.rb +17 -0
  15. data/lib/hexapdf/layout/box.rb +1 -0
  16. data/lib/hexapdf/layout/box_fitter.rb +3 -3
  17. data/lib/hexapdf/layout/column_box.rb +2 -2
  18. data/lib/hexapdf/layout/container_box.rb +1 -1
  19. data/lib/hexapdf/layout/line.rb +4 -0
  20. data/lib/hexapdf/layout/list_box.rb +2 -2
  21. data/lib/hexapdf/layout/table_box.rb +1 -1
  22. data/lib/hexapdf/layout/text_box.rb +16 -2
  23. data/lib/hexapdf/parser.rb +20 -17
  24. data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
  25. data/lib/hexapdf/type/acro_form/form.rb +123 -27
  26. data/lib/hexapdf/type/acro_form/java_script_actions.rb +165 -14
  27. data/lib/hexapdf/type/acro_form/text_field.rb +13 -1
  28. data/lib/hexapdf/type/resources.rb +2 -1
  29. data/lib/hexapdf/utils.rb +19 -0
  30. data/lib/hexapdf/version.rb +1 -1
  31. data/test/hexapdf/layout/test_box_fitter.rb +3 -3
  32. data/test/hexapdf/layout/test_text_box.rb +27 -1
  33. data/test/hexapdf/test_dictionary.rb +6 -4
  34. data/test/hexapdf/test_parser.rb +12 -0
  35. data/test/hexapdf/test_utils.rb +16 -0
  36. data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
  37. data/test/hexapdf/type/acro_form/test_form.rb +110 -2
  38. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +102 -1
  39. data/test/hexapdf/type/acro_form/test_text_field.rb +22 -4
  40. data/test/hexapdf/type/test_resources.rb +5 -0
  41. metadata +3 -2
@@ -35,6 +35,7 @@
35
35
  #++
36
36
 
37
37
  require 'json'
38
+ require 'time'
38
39
  require 'hexapdf/error'
39
40
  require 'hexapdf/layout/style'
40
41
  require 'hexapdf/layout/text_fragment'
@@ -56,6 +57,7 @@ module HexaPDF
56
57
  # JavaScript actions are:
57
58
  #
58
59
  # * +AFNumber_Format+: See #af_number_format_action and #apply_af_number_format
60
+ # * +AFPercent_Format+: See #af_percent_format_action and #apply_af_percent_format
59
61
  #
60
62
  # Calculating a field's value::
61
63
  #
@@ -184,6 +186,10 @@ module HexaPDF
184
186
  return [value, nil] unless (action_string = action_string(format_action))
185
187
  if action_string.start_with?('AFNumber_Format(')
186
188
  apply_af_number_format(value, action_string)
189
+ elsif action_string.start_with?('AFPercent_Format(')
190
+ apply_af_percent_format(value, action_string)
191
+ elsif action_string.start_with?('AFTime_Format(')
192
+ apply_af_time_format(value, action_string)
187
193
  else
188
194
  [value, nil]
189
195
  end
@@ -235,8 +241,15 @@ module HexaPDF
235
241
  # See: #apply_af_number_format
236
242
  def af_number_format_action(decimals: 2, separator_style: :point, negative_style: :minus_black,
237
243
  currency_string: "", prepend_currency: true)
238
- "AFNumber_Format(#{decimals}, #{AF_NUMBER_FORMAT_MAPPINGS[:separator][separator_style]}, " \
239
- "#{AF_NUMBER_FORMAT_MAPPINGS[:negative][negative_style]}, 0, \"#{currency_string}\", " \
244
+ separator_style = AF_NUMBER_FORMAT_MAPPINGS[:separator].fetch(separator_style) do
245
+ raise ArgumentError, "Unsupported value for separator_style argument: #{separator_style}"
246
+ end
247
+ negative_style = AF_NUMBER_FORMAT_MAPPINGS[:negative].fetch(negative_style) do
248
+ raise ArgumentError, "Unsupported value for negative_style argument: #{negative_style}"
249
+ end
250
+
251
+ "AFNumber_Format(#{decimals}, #{separator_style}, " \
252
+ "#{negative_style}, 0, \"#{currency_string}\", " \
240
253
  "#{prepend_currency});"
241
254
  end
242
255
 
@@ -323,21 +336,141 @@ module HexaPDF
323
336
  end
324
337
  end
325
338
 
326
- result = sprintf(format, value)
339
+ [af_format_number(value, format, match[:sep_style]), text_color]
340
+ end
327
341
 
328
- before_decimal_point, after_decimal_point = result.split('.')
329
- if match[:sep_style] == '0' || match[:sep_style] == '2'
330
- separator = (match[:sep_style] == '0' ? ',' : '.')
331
- before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
342
+ # Returns the appropriate JavaScript action string for the AFPercent_Format function.
343
+ #
344
+ # +decimals+::
345
+ # The number of decimal digits to use. Default 2.
346
+ #
347
+ # +separator_style+::
348
+ # Specifies the character for the decimal and thousands separator, one of:
349
+ #
350
+ # :point:: (Default) Use point as decimal separator and comma as thousands separator.
351
+ # :point_no_thousands:: Use point as decimal separator and no thousands separator.
352
+ # :comma:: Use comma as decimal separator and point as thousands separator.
353
+ # :comma_no_thousands:: Use comma as decimal separator and no thousands separator.
354
+ #
355
+ # See: #apply_af_percent_format
356
+ def af_percent_format_action(decimals: 2, separator_style: :point)
357
+ separator_style = AF_NUMBER_FORMAT_MAPPINGS[:separator].fetch(separator_style) do
358
+ raise ArgumentError, "Unsupported value for separator_style argument: #{separator_style}"
332
359
  end
333
- result = if after_decimal_point
334
- decimal_point = (match[:sep_style] =~ /[01]/ ? '.' : ',')
335
- "#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
336
- else
337
- before_decimal_point
338
- end
360
+ "AFPercent_Format(#{decimals}, #{separator_style});"
361
+ end
362
+
363
+ # Regular expression for matching the AFPercent_Format method.
364
+ #
365
+ # See: #apply_af_percent_format
366
+ AF_PERCENT_FORMAT_RE = /
367
+ \AAFPercent_Format\(
368
+ \s*(?<ndec>\d+)\s*,
369
+ \s*(?<sep_style>[0-3])\s*
370
+ \);?\z
371
+ /x
372
+
373
+ # Implements the JavaScript AFPercent_Format function and returns the formatted field value.
374
+ #
375
+ # The argument +value+ has to be the field's value (a String) and +action_string+ has to be
376
+ # the JavaScript action string.
377
+ #
378
+ # The AFPercent_Format function assumes that the text field's value contains a number (as a
379
+ # string) and formats it according to the instructions.
380
+ #
381
+ # It has the form <tt>AFPercent_Format(no_of_decimals, separator_style)</tt> where the
382
+ # arguments have the following meaning:
383
+ #
384
+ # +no_of_decimals+::
385
+ # The number of decimal places after the decimal point, e.g. for 3 it would result in
386
+ # 123.456.
387
+ #
388
+ # +separator_style+::
389
+ # Defines which decimal separator and whether a thousands separator should be used.
390
+ #
391
+ # Possible values are:
392
+ #
393
+ # +0+:: Comma for thousands separator, point for decimal separator: 12,345.67
394
+ # +1+:: No thousands separator, point for decimal separator: 12345.67
395
+ # +2+:: Point for thousands separator, comma for decimal separator: 12.345,67
396
+ # +3+:: No thousands separator, comma for decimal separator: 12345,67
397
+ def apply_af_percent_format(value, action_string)
398
+ return value unless (match = AF_PERCENT_FORMAT_RE.match(action_string))
399
+ af_format_number(af_make_number(value) * 100, "%.#{match[:ndec]}f%%", match[:sep_style])
400
+ end
339
401
 
340
- [result, text_color]
402
+ AF_TIME_FORMAT_MAPPINGS = { #:nodoc:
403
+ format_integers: {
404
+ hh_mm: 0,
405
+ 0 => 0,
406
+ hh12_mm: 1,
407
+ 1 => 1,
408
+ hh_mm_ss: 2,
409
+ 2 => 2,
410
+ hh12_mm_ss: 3,
411
+ 3 => 3,
412
+ },
413
+ strftime_format: {
414
+ '0' => '%H:%M',
415
+ '1' => '%l:%M %p',
416
+ '2' => '%H:%M:%S',
417
+ '3' => '%l:%M:%S %p',
418
+ },
419
+ }
420
+
421
+ # Returns the appropriate JavaScript action string for the AFTime_Format function.
422
+ #
423
+ # +format+::
424
+ # Specifies the time format, one of:
425
+ #
426
+ # :hh_mm:: (Default) Use 24h time format %H:%M (e.g. 15:25)
427
+ # :hh12_mm:: (Default) Use 12h time format %l:%M %p (e.g. 3:25 PM)
428
+ # :hh_mm_ss:: Use 24h time format with seconds %H:%M:%S (e.g. 15:25:37)
429
+ # :hh12_mm_ss:: Use 24h time format with seconds %l:%M:%S %p (e.g. 3:25:37 PM)
430
+ #
431
+ # See: #apply_af_time_format
432
+ def af_time_format_action(format: :hh_mm)
433
+ format = AF_TIME_FORMAT_MAPPINGS[:format_integers].fetch(format) do
434
+ raise ArgumentError, "Unsupported value for time_format argument: #{format}"
435
+ end
436
+ "AFTime_Format(#{format});"
437
+ end
438
+
439
+ # Regular expression for matching the AFTime_Format method.
440
+ #
441
+ # See: #apply_af_time_format
442
+ AF_TIME_FORMAT_RE = /
443
+ \AAFTime_Format\(
444
+ \s*(?<time_format>[0-3])\s*
445
+ \);?\z
446
+ /x
447
+
448
+ # Implements the JavaScript AFTime_Format function and returns the formatted field value.
449
+ #
450
+ # The argument +value+ has to be the field's value (a String) and +action_string+ has to be
451
+ # the JavaScript action string.
452
+ #
453
+ # The AFTime_Format function assumes that the text field's value contains a valid time
454
+ # string (for HexaPDF that is anything Time.parse can work with) and formats it according to
455
+ # the instructions.
456
+ #
457
+ # It has the form <tt>AFTime_Format(time_format)</tt> where the argument has the following
458
+ # meaning:
459
+ #
460
+ # +time_format+::
461
+ # Defines the time format which should be applied.
462
+ #
463
+ # Possible values are:
464
+ #
465
+ # +0+:: Use 24h time format, e.g. 15:25
466
+ # +1+:: Use 12h time format, e.g. 3:25 PM
467
+ # +2+:: Use 24h time format with seconds, e.g. 15:25:37
468
+ # +3+:: Use 12h time format with seconds, e.g. 3:25:37 PM
469
+ def apply_af_time_format(value, action_string)
470
+ return value unless (match = AF_TIME_FORMAT_RE.match(action_string))
471
+ value = Time.parse(value) rescue nil
472
+ return "" unless value
473
+ value.strftime(AF_TIME_FORMAT_MAPPINGS[:strftime_format][match[:time_format]]).strip
341
474
  end
342
475
 
343
476
  # Handles JavaScript calculate actions for single-line text fields.
@@ -483,6 +616,24 @@ module HexaPDF
483
616
  value.to_s.tr(',', '.').to_f
484
617
  end
485
618
 
619
+ # Formats the numeric value according to the format string and separator style.
620
+ def af_format_number(value, format, sep_style)
621
+ result = sprintf(format, value)
622
+
623
+ before_decimal_point, after_decimal_point = result.split('.')
624
+ if sep_style == '0' || sep_style == '2'
625
+ separator = (sep_style == '0' ? ',' : '.')
626
+ before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
627
+ end
628
+
629
+ if after_decimal_point
630
+ decimal_point = (sep_style <= "1" ? '.' : ',')
631
+ "#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
632
+ else
633
+ before_decimal_point
634
+ end
635
+ end
636
+
486
637
  # Returns the JavaScript action string for the given action.
487
638
  def action_string(action)
488
639
  return nil unless action && action[:S] == :JavaScript
@@ -170,7 +170,7 @@ module HexaPDF
170
170
  elsif comb_text_field? && !key?(:MaxLen)
171
171
  raise HexaPDF::Error, "A comb text field need a valid /MaxLen value"
172
172
  elsif str && !str.kind_of?(String)
173
- @document.config['acro_form.on_invalid_value'].call(self, str)
173
+ str = @document.config['acro_form.on_invalid_value'].call(self, str)
174
174
  end
175
175
  str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
176
176
  if key?(:MaxLen) && str && str.length > self[:MaxLen]
@@ -254,9 +254,21 @@ module HexaPDF
254
254
  # :number::
255
255
  # Assumes that the field value is a number and formats it according to the given
256
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.
257
267
  def set_format_action(type, **arguments)
258
268
  action_string = case type
259
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)
260
272
  else
261
273
  raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
262
274
  end
@@ -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.41.0'
40
+ VERSION = '0.43.0'
41
41
 
42
42
  end
@@ -20,13 +20,13 @@ describe HexaPDF::Layout::BoxFitter do
20
20
  @box_fitter.fit(HexaPDF::Layout::TextBox.new(items: [ibox] * count))
21
21
  end
22
22
 
23
- def check_result(*pos, content_heights:, successful: true, boxes_remain: false)
23
+ def check_result(*pos, content_heights:, success: true, boxes_remain: false)
24
24
  pos.each_slice(2).with_index do |(x, y), index|
25
25
  assert_equal(x, @box_fitter.fit_results[index].x, "x #{index}")
26
26
  assert_equal(y, @box_fitter.fit_results[index].y, "y #{index}")
27
27
  end
28
28
  assert_equal(content_heights, @box_fitter.content_heights)
29
- successful ? assert(@box_fitter.fit_successful?) : refute(@box_fitter.fit_successful?)
29
+ success ? assert(@box_fitter.success?) : refute(@box_fitter.success?)
30
30
  rboxes = @box_fitter.remaining_boxes.empty?
31
31
  boxes_remain ? refute(rboxes) : assert(rboxes)
32
32
  end
@@ -55,7 +55,7 @@ describe HexaPDF::Layout::BoxFitter do
55
55
  fit_box(70)
56
56
  fit_box(40)
57
57
  fit_box(20)
58
- check_result(10, 80, 0, 10, 0, 0, 100, 100, successful: false, boxes_remain: true,
58
+ check_result(10, 80, 0, 10, 0, 0, 100, 100, success: false, boxes_remain: true,
59
59
  content_heights: [90, 50])
60
60
  assert_equal(2, @box_fitter.remaining_boxes.size)
61
61
  end
@@ -50,10 +50,12 @@ describe HexaPDF::Layout::TextBox do
50
50
  end
51
51
 
52
52
  it "fits into the frame's outline" do
53
+ @frame.remove_area(Geom2D::Rectangle(0, 80, 20, 20))
54
+ @frame.remove_area(Geom2D::Rectangle(80, 70, 20, 20))
53
55
  box = create_box([@inline_box] * 20, style: {position: :flow})
54
56
  assert(box.fit(100, 100, @frame))
55
57
  assert_equal(100, box.width)
56
- assert_equal(20, box.height)
58
+ assert_equal(30, box.height)
57
59
  end
58
60
 
59
61
  it "takes the style option last_line_gap into account" do
@@ -190,6 +192,30 @@ describe HexaPDF::Layout::TextBox do
190
192
  [:restore_graphics_state]])
191
193
  end
192
194
 
195
+ it "correctly draws borders, backgrounds... for position :flow" do
196
+ @frame.remove_area(Geom2D::Rectangle(0, 0, 40, 100))
197
+ box = create_box([@inline_box], style: {position: :flow, border: {width: 1}})
198
+ box.fit(60, 100, @frame)
199
+ box.draw(@canvas, 0, 90)
200
+ assert_operators(@canvas.contents, [[:save_graphics_state],
201
+ [:append_rectangle, [40, 90, 10, 10]],
202
+ [:clip_path_non_zero],
203
+ [:end_path],
204
+ [:append_rectangle, [40.5, 90.5, 9.0, 9.0]],
205
+ [:stroke_path],
206
+ [:restore_graphics_state],
207
+ [:save_graphics_state],
208
+ [:restore_graphics_state],
209
+ [:save_graphics_state],
210
+ [:concatenate_matrix, [1, 0, 0, 1, 41, 89]],
211
+ [:save_graphics_state],
212
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
213
+ [:restore_graphics_state],
214
+ [:restore_graphics_state],
215
+ [:save_graphics_state],
216
+ [:restore_graphics_state]])
217
+ end
218
+
193
219
  it "draws nothing onto the canvas if the box is empty" do
194
220
  box = create_box([])
195
221
  box.draw(@canvas, 5, 5)
@@ -323,11 +323,13 @@ describe HexaPDF::Dictionary do
323
323
  end
324
324
  end
325
325
 
326
- describe "to_h" do
327
- it "returns a shallow copy of the value" do
328
- obj = @dict.to_h
326
+ describe "to_hash" do
327
+ it "returns a copy of the value where each entry is pre-processed" do
328
+ @dict[:value] = HexaPDF::Reference.new(1, 0)
329
+ obj = @dict.to_hash
329
330
  refute_equal(obj.object_id, @dict.value.object_id)
330
- assert_equal(obj, @dict.value)
331
+ assert_equal(:obj, obj[:Object])
332
+ assert_equal("deref", obj[:value])
331
333
  end
332
334
  end
333
335
 
@@ -367,6 +367,11 @@ describe HexaPDF::Parser do
367
367
  assert_equal(5, @parser.startxref_offset)
368
368
  end
369
369
 
370
+ it "handles the case of multiple %%EOF and the last one being invalid" do
371
+ create_parser("startxref\n5\n%%EOF\ntartxref\n3\n%%EOF")
372
+ assert_equal(5, @parser.startxref_offset)
373
+ end
374
+
370
375
  it "fails even in big files when nothing is found" do
371
376
  create_parser("\nhallo" * 5000)
372
377
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
@@ -402,6 +407,13 @@ describe HexaPDF::Parser do
402
407
  exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
403
408
  assert_match(/startxref on same line/, exp.message)
404
409
  end
410
+
411
+ it "fails on strict parsing if there are multiple %%EOF and the last one is invalid" do
412
+ @document.config['parser.on_correctable_error'] = proc { true }
413
+ create_parser("startxref\n5\n%%EOF\ntartxref\n3\n%%EOF")
414
+ exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
415
+ assert_match(/missing startxref keyword/, exp.message)
416
+ end
405
417
  end
406
418
 
407
419
  describe "file_header_version" do
@@ -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))
@@ -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
@@ -128,6 +128,12 @@ describe HexaPDF::Type::AcroForm::Form do
128
128
  @acro_form = @doc.acro_form(create: true)
129
129
  end
130
130
 
131
+ it "creates a pure namespace field" do
132
+ field = @acro_form.create_namespace_field('text')
133
+ assert_equal('text', field.full_field_name)
134
+ assert_nil(field.concrete_field_type)
135
+ end
136
+
131
137
  describe "handles the general case" do
132
138
  it "works for names with a dot" do
133
139
  @acro_form[:Fields] = [{T: "root"}]
@@ -142,8 +148,13 @@ describe HexaPDF::Type::AcroForm::Form do
142
148
  assert([field], @acro_form[:Fields])
143
149
  end
144
150
 
145
- it "fails if the parent field is not found" do
146
- assert_raises(HexaPDF::Error) { @acro_form.create_text_field("root.field") }
151
+ it "creates the parent fields as namespace fields if necessary" do
152
+ field = @acro_form.create_text_field("root.sub.field")
153
+ level1 = @acro_form.field_by_name('root')
154
+ assert_equal(1, level1[:Kids].size)
155
+ level2 = @acro_form.field_by_name('root.sub')
156
+ assert_equal(1, level2[:Kids].size)
157
+ assert_same(field, level2[:Kids][0])
147
158
  end
148
159
  end
149
160
 
@@ -241,6 +252,103 @@ describe HexaPDF::Type::AcroForm::Form do
241
252
  end
242
253
  end
243
254
 
255
+ describe "delete_field" do
256
+ before do
257
+ @field = @acro_form.create_signature_field("sig")
258
+ end
259
+
260
+ it "deletes a field via name" do
261
+ @acro_form.delete_field('sig')
262
+ assert_equal(0, @acro_form.root_fields.size)
263
+ end
264
+
265
+ it "deletes a field via field object" do
266
+ @acro_form.delete_field(@field)
267
+ assert_equal(0, @acro_form.root_fields.size)
268
+ end
269
+
270
+ it "deletes the set signature object" do
271
+ obj = @doc.add({})
272
+ @field.field_value = obj
273
+ @acro_form.delete_field(@field)
274
+ assert(obj.null?)
275
+ end
276
+
277
+ it "deletes all widget annotations from the document and the annotation array" do
278
+ widget1 = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
279
+ widget2 = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
280
+ refute(@doc.pages[1][:Annots].empty?)
281
+ @acro_form.delete_field(@field)
282
+ assert(@doc.pages[0][:Annots].empty?)
283
+ assert(@doc.pages[1][:Annots].empty?)
284
+ assert(@doc.object(widget1).null?)
285
+ assert(@doc.object(widget2).null?)
286
+ end
287
+
288
+ it "deletes the field from the field hierarchy" do
289
+ @acro_form.delete_field('sig')
290
+ refute(@acro_form.field_by_name('sig'))
291
+ assert(@acro_form[:Fields].empty?)
292
+
293
+ @acro_form.create_signature_field("sub.sub.sig")
294
+ @acro_form.delete_field("sub.sub.sig")
295
+ refute(@acro_form.field_by_name('sub.sub.sig'))
296
+ assert(@acro_form[:Fields][0][:Kids][0][:Kids].empty?)
297
+ end
298
+
299
+ it "deletes the field itself" do
300
+ @acro_form.delete_field('sig')
301
+ assert(@doc.object(@field).null?)
302
+ end
303
+ end
304
+
305
+ describe "fill" do
306
+ it "works for text field types" do
307
+ field = @acro_form.create_text_field('test')
308
+ @acro_form.fill("test" => "value")
309
+ assert_equal("value", field.field_value)
310
+ end
311
+
312
+ it "works for radio buttons" do
313
+ field = @acro_form.create_radio_button("test")
314
+ field.create_widget(@doc.pages.add, value: :name)
315
+ @acro_form.fill("test" => "name")
316
+ assert_equal(:name, field.field_value)
317
+ end
318
+
319
+ it "works for check boxes" do
320
+ field = @acro_form.create_check_box('test')
321
+ field.create_widget(@doc.pages.add)
322
+
323
+ ["t", "true", "y", "yes"].each do |value|
324
+ @acro_form.fill("test" => value)
325
+ assert_equal(:Yes, field.field_value)
326
+ field.field_value = :Off
327
+ end
328
+
329
+ ["f", "false", "n", "no"].each do |value|
330
+ @acro_form.fill("test" => value)
331
+ assert_nil(field.field_value)
332
+ field.field_value = :Yes
333
+ end
334
+
335
+ field.create_widget(@doc.pages.add, value: :Other)
336
+ @acro_form.fill("test" => "Other")
337
+ assert_equal(:Other, field.field_value)
338
+ end
339
+
340
+ it "raises an error if a field is not found" do
341
+ error = assert_raises(HexaPDF::Error) { @acro_form.fill("unknown" => "test") }
342
+ assert_match(/named 'unknown' not found/, error.message)
343
+ end
344
+
345
+ it "raises an error if a field type is not supported for filling in" do
346
+ @acro_form.create_check_box('test').initialize_as_push_button
347
+ error = assert_raises(HexaPDF::Error) { @acro_form.fill("test" => "test") }
348
+ assert_match(/push_button not yet supported/, error.message)
349
+ end
350
+ end
351
+
244
352
  it "returns the default resources" do
245
353
  assert_kind_of(HexaPDF::Type::Resources, @acro_form.default_resources)
246
354
  end