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