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
@@ -278,6 +278,14 @@ module HexaPDF
278
278
  # If the same argument is provided in multiple invocations, the import is done only once and
279
279
  # the previously imported object is returned.
280
280
  #
281
+ # Note: If you first create a PDF document from scratch and then want to import objects from it
282
+ # into another PDF document, you need to run the following on the source document:
283
+ #
284
+ # doc.dispatch_message(:complete_objects)
285
+ # doc.validate
286
+ #
287
+ # This ensures that the source document has all the necessary PDF structures set-up correctly.
288
+ #
281
289
  # See: Importer
282
290
  def import(obj)
283
291
  source = (obj.kind_of?(HexaPDF::Object) ? obj.document : nil)
@@ -617,13 +625,18 @@ module HexaPDF
617
625
  # writing the document.
618
626
  #
619
627
  # The security handler used for encrypting is selected via the +name+ argument. All other
620
- # arguments are passed on the security handler.
628
+ # arguments are passed on to the security handler.
621
629
  #
622
630
  # If the document should not be encrypted, the +name+ argument has to be set to +nil+. This
623
631
  # removes the security handler and deletes the trailer's Encrypt dictionary.
624
632
  #
625
633
  # See: Encryption::SecurityHandler#set_up_encryption and
626
634
  # Encryption::StandardSecurityHandler::EncryptionOptions for possible encryption options.
635
+ #
636
+ # Examples:
637
+ #
638
+ # document.encrypt(name: nil) # remove the existing encryption
639
+ # document.encrypt(algorithm: :aes, key_length: 256, permissions: [:print, :extract_content]
627
640
  def encrypt(name: :Standard, **options)
628
641
  if name.nil?
629
642
  trailer.delete(:Encrypt)
@@ -46,6 +46,23 @@ module HexaPDF
46
46
  #
47
47
  # This module contains all encryption and security related code to facilitate PDF encryption.
48
48
  #
49
+ # === Working With Encrypted Documents
50
+ #
51
+ # When a PDF document is opened, an encryption password can be specified. This is necessary if a
52
+ # user password is set on the file and optional otherwise (because the default password is
53
+ # automatically tried):
54
+ #
55
+ # HexaPDF::Document.open(filename, decryption_opts: {password: 'somepassword'}) do |doc|
56
+ # end
57
+ #
58
+ # To remove the encryption from a PDF document, use the following:
59
+ #
60
+ # document.encrypt(name: nil)
61
+ #
62
+ # To encrypt a PDF document, use the same method but specify the required encryption options:
63
+ #
64
+ # document.encrypt(algorithm: :aes, key_length: 256)
65
+ #
49
66
  #
50
67
  # === Security Handlers
51
68
  #
@@ -35,6 +35,7 @@
35
35
  #++
36
36
  require 'hexapdf/layout/style'
37
37
  require 'geom2d/utils'
38
+ require 'hexapdf/utils'
38
39
 
39
40
  module HexaPDF
40
41
  module Layout
@@ -47,8 +47,8 @@ module HexaPDF
47
47
  #
48
48
  # * Then use the #fit method to fit boxes one after the other. No drawing is done.
49
49
  #
50
- # * Once all boxes have been fitted, the #fit_results, #remaining_boxes and #fit_successful?
51
- # methods can be used to get the result:
50
+ # * Once all boxes have been fitted, the #fit_results, #remaining_boxes and #success? methods
51
+ # can be used to get the result:
52
52
  #
53
53
  # - If there are no remaining boxes, all boxes were successfully fitted into the frames.
54
54
  # - If there are remaining boxes but no fit results, the first box could not be fitted.
@@ -126,7 +126,7 @@ module HexaPDF
126
126
  end
127
127
 
128
128
  # Returns +true+ if all boxes were successfully fitted.
129
- def fit_successful?
129
+ def success?
130
130
  @remaining_boxes.empty?
131
131
  end
132
132
 
@@ -186,7 +186,7 @@ module HexaPDF
186
186
 
187
187
  children.each {|box| @box_fitter.fit(box) }
188
188
 
189
- fit_successful = @box_fitter.fit_successful?
189
+ fit_successful = @box_fitter.success?
190
190
  initial_fit_successful = fit_successful if initial_fit_successful.nil?
191
191
 
192
192
  if fit_successful
@@ -211,7 +211,7 @@ module HexaPDF
211
211
  @draw_pos_x = frame.x + reserved_width_left
212
212
  @draw_pos_y = frame.y - @height + reserved_height_bottom
213
213
 
214
- @box_fitter.fit_successful?
214
+ @box_fitter.success?
215
215
  end
216
216
 
217
217
  private
@@ -137,7 +137,7 @@ module HexaPDF
137
137
  @box_fitter = BoxFitter.new([my_frame])
138
138
  children.each {|box| @box_fitter.fit(box) }
139
139
 
140
- if @box_fitter.fit_successful?
140
+ if @box_fitter.success?
141
141
  update_content_width do
142
142
  result = @box_fitter.fit_results.max_by {|r| r.mask.x + r.mask.width }
143
143
  children.empty? ? 0 : result.mask.x + result.mask.width - my_frame.left
@@ -173,6 +173,10 @@ module HexaPDF
173
173
  attr_accessor :items
174
174
 
175
175
  # An optional horizontal offset that should be taken into account when positioning the line.
176
+ #
177
+ # This offset always describes the offset from the left side (and not, for example, the offset
178
+ # from the right side of another line even if those two lines are actually on the same
179
+ # horizontal level).
176
180
  attr_accessor :x_offset
177
181
 
178
182
  # An optional vertical offset that should be taken into account when positioning the line.
@@ -248,14 +248,14 @@ module HexaPDF
248
248
  top -= item_result.height + item_spacing
249
249
  height -= item_result.height + item_spacing
250
250
 
251
- break if !box_fitter.fit_successful? || height <= 0
251
+ break if !box_fitter.success? || height <= 0
252
252
  end
253
253
 
254
254
  @height = @results.sum(&:height) + (@results.count - 1) * item_spacing + reserved_height
255
255
 
256
256
  @draw_pos_x = frame.x + reserved_width_left
257
257
  @draw_pos_y = frame.y - @height + reserved_height_bottom
258
- @all_items_fitted = @results.all? {|r| r.box_fitter.fit_successful? } &&
258
+ @all_items_fitted = @results.all? {|r| r.box_fitter.success? } &&
259
259
  @results.size == @children.size
260
260
  @fit_successful = @all_items_fitted || (@initial_height > 0 && style.overflow == :truncate)
261
261
  end
@@ -233,7 +233,7 @@ module HexaPDF
233
233
  @preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
234
234
  @height = @preferred_height = box_fitter.content_heights[0] + reserved_height
235
235
  @fit_results = box_fitter.fit_results
236
- @fit_successful = box_fitter.fit_successful?
236
+ @fit_successful = box_fitter.success?
237
237
  else
238
238
  @preferred_width = reserved_width
239
239
  @height = @preferred_height = reserved_height
@@ -52,6 +52,7 @@ module HexaPDF
52
52
  @tl = TextLayouter.new(style)
53
53
  @items = items
54
54
  @result = nil
55
+ @x_offset = 0
55
56
  end
56
57
 
57
58
  # Returns the text that will be drawn.
@@ -80,7 +81,7 @@ module HexaPDF
80
81
  (@initial_height > 0 && @initial_height > available_height)
81
82
 
82
83
  frame = frame.child_frame(box: self)
83
- @width = @height = 0
84
+ @width = @x_offset = @height = 0
84
85
  @result = if style.position == :flow
85
86
  @tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
86
87
  apply_first_text_indent: !split_box?, frame: frame)
@@ -93,6 +94,14 @@ module HexaPDF
93
94
  end
94
95
  @width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
95
96
  width
97
+ elsif style.position == :flow
98
+ min_x = +Float::INFINITY
99
+ max_x = -Float::INFINITY
100
+ @result.lines.each do |line|
101
+ min_x = [min_x, line.x_offset].min
102
+ max_x = [max_x, line.x_offset + line.width].max
103
+ end
104
+ min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0
96
105
  else
97
106
  @result.lines.max_by(&:width)&.width || 0
98
107
  end
@@ -125,6 +134,11 @@ module HexaPDF
125
134
  end
126
135
  end
127
136
 
137
+ # :nodoc:
138
+ def draw(canvas, x, y)
139
+ super(canvas, x + @x_offset, y)
140
+ end
141
+
128
142
  # :nodoc:
129
143
  def empty?
130
144
  super && (!@result || @result.lines.empty?)
@@ -142,7 +156,7 @@ module HexaPDF
142
156
  end
143
157
 
144
158
  return if @result.lines.empty?
145
- @result.draw(canvas, x, y + content_height)
159
+ @result.draw(canvas, x - @x_offset, y + content_height)
146
160
  end
147
161
 
148
162
  # Creates a new TextBox instance for the items remaining after fitting the box.
@@ -362,29 +362,32 @@ module HexaPDF
362
362
  pos = @io.pos
363
363
  lines = @io.read(step_size + 40).split(/[\r\n]+/)
364
364
 
365
- eof_index = lines.rindex {|l| l.strip == '%%EOF' }
366
- if !eof_index
367
- eof_not_found = true
368
- elsif lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
- startxref_offset = $1.to_i
370
- startxref_mangled = true
371
- break # we found it even if it the syntax is not entirely correct
372
- elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
373
- startxref_missing = true
374
- else
375
- startxref_offset = lines[eof_index - 1].to_i
376
- break # we found it
365
+ # Need to iterate through the whole lines array in case there are multiple %%EOF to try
366
+ eof_index = 0
367
+ while (eof_index = lines[0..(eof_index - 1)].rindex {|l| l.strip == '%%EOF' })
368
+ if lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
369
+ startxref_offset = $1.to_i
370
+ startxref_mangled = true
371
+ break # we found it even if it the syntax is not entirely correct
372
+ elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
373
+ startxref_missing = true
374
+ else
375
+ startxref_offset = lines[eof_index - 1].to_i
376
+ break # we found it
377
+ end
377
378
  end
379
+ eof_not_found ||= !eof_index
380
+ break if startxref_offset
378
381
  end
379
382
 
380
- if eof_not_found
381
- maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
382
- force: !eof_index)
383
- elsif startxref_mangled
383
+ if startxref_mangled
384
384
  maybe_raise("PDF file trailer keyword startxref on same line as value", pos: pos)
385
385
  elsif startxref_missing
386
386
  maybe_raise("PDF file trailer is missing startxref keyword", pos: pos,
387
- force: eof_index < 2 || lines[eof_index - 2].strip != "startxref")
387
+ force: !startxref_offset)
388
+ elsif eof_not_found
389
+ maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
390
+ force: !startxref_offset)
388
391
  end
389
392
 
390
393
  @startxref_offset = startxref_offset
@@ -165,14 +165,16 @@ module HexaPDF
165
165
  # nothing is stored for them (e.g a no-op).
166
166
  #
167
167
  # Check boxes:: Provide +nil+ or +false+ as value to toggle all check box widgets off. If
168
- # there is only one possible value, +true+ may be used for checking the box,
169
- # i.e. toggling it to the on state. Otherwise provide the value (a Symbol or
170
- # an object responding to +#to_sym+) of the check box widget that should be
171
- # toggled on.
168
+ # +true+ is provided, all check box widgets with the same name as the first
169
+ # one are toggled on. Otherwise provide the value (a Symbol or an object
170
+ # responding to +#to_sym+) of the check box widget that should be toggled on.
172
171
  #
173
172
  # Radio buttons:: To turn all radio buttons off, provide +nil+ as value. Otherwise provide
174
173
  # the value (a Symbol or an object responding to +#to_sym+) of a radio
175
174
  # button that should be turned on.
175
+ #
176
+ # Note that in most cases the field needs to already have widgets because the value is
177
+ # checked against the possibly allowed values which depend on the existing widgets.
176
178
  def field_value=(value)
177
179
  normalized_field_value_set(:V, value)
178
180
  end
@@ -303,7 +305,7 @@ module HexaPDF
303
305
  elsif check_box?
304
306
  if value == false
305
307
  :Off
306
- elsif value == true && av.size == 1
308
+ elsif value == true && av.size >= 1
307
309
  av[0]
308
310
  elsif av.include?(value.to_sym)
309
311
  value.to_sym
@@ -163,10 +163,21 @@ module HexaPDF
163
163
  field
164
164
  end
165
165
 
166
+ # Creates an untyped namespace field for creating hierarchies.
167
+ #
168
+ # Example:
169
+ #
170
+ # form.create_namespace_field('text')
171
+ # form.create_text_field('text.a1')
172
+ def create_namespace_field(name)
173
+ create_field(name)
174
+ end
175
+
166
176
  # Creates a new text field with the given name and adds it to the form.
167
177
  #
168
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
169
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
178
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
179
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
180
+ # the +name+ doesn't contain dots, a top-level field is created.
170
181
  #
171
182
  # The optional keyword arguments allow setting often used properties of the field:
172
183
  #
@@ -202,8 +213,9 @@ module HexaPDF
202
213
 
203
214
  # Creates a new multiline text field with the given name and adds it to the form.
204
215
  #
205
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
206
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
216
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
217
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
218
+ # the +name+ doesn't contain dots, a top-level field is created.
207
219
  #
208
220
  # The optional keyword arguments allow setting often used properties of the field, see
209
221
  # #create_text_field for details.
@@ -221,8 +233,9 @@ module HexaPDF
221
233
  # The +max_chars+ argument defines the maximum number of characters the comb text field can
222
234
  # accommodate.
223
235
  #
224
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
225
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
236
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
237
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
238
+ # the +name+ doesn't contain dots, a top-level field is created.
226
239
  #
227
240
  # The optional keyword arguments allow setting often used properties of the field, see
228
241
  # #create_text_field for details.
@@ -238,8 +251,9 @@ module HexaPDF
238
251
 
239
252
  # Creates a new file select field with the given name and adds it to the form.
240
253
  #
241
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
242
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
254
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
255
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
256
+ # the +name+ doesn't contain dots, a top-level field is created.
243
257
  #
244
258
  # The optional keyword arguments allow setting often used properties of the field, see
245
259
  # #create_text_field for details.
@@ -254,8 +268,9 @@ module HexaPDF
254
268
 
255
269
  # Creates a new password field with the given name and adds it to the form.
256
270
  #
257
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
258
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
271
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
272
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
273
+ # the +name+ doesn't contain dots, a top-level field is created.
259
274
  #
260
275
  # The optional keyword arguments allow setting often used properties of the field, see
261
276
  # #create_text_field for details.
@@ -270,24 +285,33 @@ module HexaPDF
270
285
 
271
286
  # Creates a new check box with the given name and adds it to the form.
272
287
  #
273
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
274
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
288
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
289
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
290
+ # the +name+ doesn't contain dots, a top-level field is created.
291
+ #
292
+ # Before a field value other than +false+ can be assigned to the check box, a widget needs
293
+ # to be created.
275
294
  def create_check_box(name)
276
295
  create_field(name, :Btn, &:initialize_as_check_box)
277
296
  end
278
297
 
279
298
  # Creates a radio button with the given name and adds it to the form.
280
299
  #
281
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
282
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
300
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
301
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
302
+ # the +name+ doesn't contain dots, a top-level field is created.
303
+ #
304
+ # Before a field value other than +nil+ can be assigned to the radio button, at least one
305
+ # widget needs to be created.
283
306
  def create_radio_button(name)
284
307
  create_field(name, :Btn, &:initialize_as_radio_button)
285
308
  end
286
309
 
287
310
  # Creates a combo box with the given name and adds it to the form.
288
311
  #
289
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
290
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
312
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
313
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
314
+ # the +name+ doesn't contain dots, a top-level field is created.
291
315
  #
292
316
  # The optional keyword arguments allow setting often used properties of the field:
293
317
  #
@@ -313,8 +337,9 @@ module HexaPDF
313
337
 
314
338
  # Creates a list box with the given name and adds it to the form.
315
339
  #
316
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
317
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
340
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
341
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
342
+ # the +name+ doesn't contain dots, a top-level field is created.
318
343
  #
319
344
  # The optional keyword arguments allow setting often used properties of the field:
320
345
  #
@@ -339,10 +364,77 @@ module HexaPDF
339
364
 
340
365
  # Creates a signature field with the given name and adds it to the form.
341
366
  #
342
- # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
343
- # fields must already exist. If it doesn't contain dots, a top-level field is created.
367
+ # The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
368
+ # already exist, they are created as pure namespace fields (see #create_namespace_field). If
369
+ # the +name+ doesn't contain dots, a top-level field is created.
344
370
  def create_signature_field(name)
345
- create_field(name, :Sig) {}
371
+ create_field(name, :Sig)
372
+ end
373
+
374
+ # :call-seq:
375
+ # form.delete_field(name)
376
+ # form.delete_field(field)
377
+ #
378
+ # Deletes the field specified by the given name or via the given field object.
379
+ #
380
+ # If the field is a signature field, the associated signature dictionary is also deleted.
381
+ def delete_field(name_or_field)
382
+ field = (name_or_field.kind_of?(String) ? field_by_name(name_or_field) : name_or_field)
383
+ document.delete(field[:V]) if field.field_type == :Sig
384
+
385
+ to_delete = field.each_widget(direct_only: false).to_a
386
+ document.pages.each do |page|
387
+ next unless page.key?(:Annots)
388
+ page_annots = page[:Annots].to_a - to_delete
389
+ page[:Annots].value.replace(page_annots)
390
+ end
391
+ to_delete.each {|widget| document.delete(widget) }
392
+
393
+ if field[:Parent]
394
+ field[:Parent][:Kids].delete(field)
395
+ else
396
+ self[:Fields].delete(field)
397
+ end
398
+ document.delete(field)
399
+ end
400
+
401
+ # Fills form fields with the values from the given +data+ hash.
402
+ #
403
+ # The keys of the +data+ hash need to be full field names and the values are the respective
404
+ # values, usually in string form. It is possible to specify only some of the fields of the
405
+ # form.
406
+ #
407
+ # What kind of values are supported for a field depends on the field type:
408
+ #
409
+ # * For fields containing text (single/multiline/comb text fields, file select fields, combo
410
+ # boxes and list boxes) the value needs to be a string and it is assigned as is.
411
+ #
412
+ # * For check boxes, the values "y"/"yes"/"t"/"true" are handled as assigning +true+ to the
413
+ # field, the values "n"/"no"/"f"/"false" are handled as assigning +false+ to the field,
414
+ # and every other string value is assigned as is. See ButtonField#field_value= for
415
+ # details.
416
+ #
417
+ # * For radio buttons the value needs to be a String or a Symbol representing the name of
418
+ # the radio button widget to select.
419
+ def fill(data)
420
+ data.each do |field_name, value|
421
+ field = field_by_name(field_name)
422
+ raise HexaPDF::Error, "AcroForm field named '#{field_name}' not found" unless field
423
+
424
+ case field.concrete_field_type
425
+ when :single_line_text_field, :multiline_text_field, :comb_text_field, :file_select_field,
426
+ :combo_box, :list_box, :editable_combo_box, :radio_button
427
+ field.field_value = value
428
+ when :check_box
429
+ field.field_value = case value
430
+ when /\A(?:y(es)?|t(rue)?)\z/ then true
431
+ when /\A(?:n(o)?|f(alse)?)\z/ then false
432
+ else value
433
+ end
434
+ else
435
+ raise HexaPDF::Error, "AcroForm field type #{field.concrete_field_type} not yet supported"
436
+ end
437
+ end
346
438
  end
347
439
 
348
440
  # Returns the dictionary containing the default resources for form field appearance streams.
@@ -440,23 +532,27 @@ module HexaPDF
440
532
 
441
533
  private
442
534
 
443
- # Creates a new field with the full name +name+ and the field type +type+.
444
- def create_field(name, type)
535
+ # Creates a new field with the full name +name+ and the optional field type +type+.
536
+ def create_field(name, type = nil)
445
537
  parent_name, _, name = name.rpartition('.')
446
538
  parent_field = parent_name.empty? ? nil : field_by_name(parent_name)
447
539
  if !parent_name.empty? && !parent_field
448
- raise HexaPDF::Error, "Parent field '#{parent_name}' not found"
540
+ parent_field = create_namespace_field(parent_name)
449
541
  end
450
542
 
451
- field = document.add({FT: type, T: name, Parent: parent_field},
452
- type: :XXAcroFormField, subtype: type)
543
+ field = if type
544
+ document.add({FT: type, T: name, Parent: parent_field},
545
+ type: :XXAcroFormField, subtype: type)
546
+ else
547
+ document.add({T: name, Parent: parent_field}, type: :XXAcroFormField)
548
+ end
453
549
  if parent_field
454
550
  (parent_field[:Kids] ||= []) << field
455
551
  else
456
552
  (self[:Fields] ||= []) << field
457
553
  end
458
554
 
459
- yield(field)
555
+ yield(field) if block_given?
460
556
 
461
557
  field
462
558
  end