hexapdf 0.41.0 → 0.43.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 (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