Class: HexaPDF::Type::AcroForm::Form

Inherits:
Dictionary show all
Extended by:
Utils::BitField
Defined in:
lib/hexapdf/type/acro_form/form.rb

Overview

Represents the PDF’s interactive form dictionary. It is linked from the catalog dictionary via the /AcroForm entry.

Overview

An interactive form consists of fields which can be structured hierarchically and shown on pages by using Annotations::Widget annotations. This means one field can have zero, one or more visual representations on one or more pages. The fields at the bottom of the hierarchy which have no parent are called “root fields” and are stored in /Fields.

Each field in a form has a certain type which determines how it should be displayed and what a user can do with it. The most common type is “text field” which allows the user to enter one or more lines of text. There are also check boxes, radio buttons, list boxes and combo boxes.

Visual Appearance

The visual appearance of a field is normally provided by the application creating the PDF. This is done by generating the so called appearances for all widgets of a field. However, it is also possible to instruct the PDF reader application to generate the appearances on the fly using the /NeedAppearances key, see #need_appearances!.

HexaPDF uses the configuration option acro_form.create_appearance_streams to determine whether appearances should automatically be generated.

See: PDF2.0 s12.7.3, Field, HexaPDF::Type::Annotations::Widget

Constant Summary

Constants included from DictionaryFields

DictionaryFields::Boolean, DictionaryFields::PDFByteString, DictionaryFields::PDFDate

Instance Attribute Summary

Attributes inherited from Object

#data, #document, #must_be_indirect

Instance Method Summary collapse

Methods included from Utils::BitField

bit_field

Methods inherited from Dictionary

#[], #[]=, define_field, define_type, #delete, #each, each_field, #empty?, field, #key?, #to_hash, type, #type

Methods inherited from Object

#<=>, #==, #cache, #cached?, #clear_cache, deep_copy, #deep_copy, #document?, #eql?, field, #gen, #gen=, #hash, #indirect?, #initialize, #inspect, make_direct, #must_be_indirect?, #null?, #oid, #oid=, #type, #validate, #value, #value=

Constructor Details

This class inherits a constructor from HexaPDF::Object

Instance Method Details

#create_appearances(force: false) ⇒ Object

Creates the appearances for all widgets of all terminal fields if they don’t exist.

If force is true, new appearances are created even if there are existing ones.



464
465
466
467
468
# File 'lib/hexapdf/type/acro_form/form.rb', line 464

def create_appearances(force: false)
  each_field do |field|
    field.create_appearances(force: force) if field.respond_to?(:create_appearances)
  end
end

#create_check_box(name) ⇒ Object

Creates a new check box with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

Before a field value other than false can be assigned to the check box, a widget needs to be created.



289
290
291
# File 'lib/hexapdf/type/acro_form/form.rb', line 289

def create_check_box(name)
  create_field(name, :Btn, &:initialize_as_check_box)
end

#create_comb_text_field(name, max_chars:, font: nil, font_options: nil, font_size: nil, font_color: nil, align: nil) ⇒ Object

Creates a new comb text field with the given name and adds it to the form.

The max_chars argument defines the maximum number of characters the comb text field can accommodate.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

The optional keyword arguments allow setting often used properties of the field, see #create_text_field for details.



237
238
239
240
241
242
243
244
245
# File 'lib/hexapdf/type/acro_form/form.rb', line 237

def create_comb_text_field(name, max_chars:, font: nil, font_options: nil, font_size: nil,
                           font_color: nil, align: nil)
  create_field(name, :Tx) do |field|
    field.initialize_as_comb_text_field
    apply_variable_text_properties(field, font: font, font_options: font_options,
                                   font_size: font_size, font_color: font_color, align: align)
    field[:MaxLen] = max_chars
  end
end

#create_combo_box(name, option_items: nil, editable: nil, font: nil, font_options: nil, font_size: nil, font_color: nil, align: nil) ⇒ Object

Creates a combo box with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

The optional keyword arguments allow setting often used properties of the field:

option_items

Specifies the values of the list box.

editable

If set to true, the combo box allows entering an arbitrary value in addition to selecting one of the provided option items.

font, font_options, font_size and align

See #create_text_field



322
323
324
325
326
327
328
329
330
331
# File 'lib/hexapdf/type/acro_form/form.rb', line 322

def create_combo_box(name, option_items: nil, editable: nil, font: nil,
                     font_options: nil, font_size: nil, font_color: nil, align: nil)
  create_field(name, :Ch) do |field|
    field.initialize_as_combo_box
    field.option_items = option_items if option_items
    field.flag(:edit) if editable
    apply_variable_text_properties(field, font: font, font_options: font_options,
                                   font_size: font_size, font_color: font_color, align: align)
  end
end

#create_file_select_field(name, font: nil, font_options: nil, font_size: nil, font_color: nil, align: nil) ⇒ Object

Creates a new file select field with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

The optional keyword arguments allow setting often used properties of the field, see #create_text_field for details.



255
256
257
258
259
260
261
262
# File 'lib/hexapdf/type/acro_form/form.rb', line 255

def create_file_select_field(name, font: nil, font_options: nil, font_size: nil,
                             font_color: nil, align: nil)
  create_field(name, :Tx) do |field|
    field.initialize_as_file_select_field
    apply_variable_text_properties(field, font: font, font_options: font_options,
                                   font_size: font_size, font_color: font_color, align: align)
  end
end

#create_list_box(name, option_items: nil, multi_select: nil, font: nil, font_options: nil, font_size: nil, font_color: nil, align: nil) ⇒ Object

Creates a list box with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

The optional keyword arguments allow setting often used properties of the field:

option_items

Specifies the values of the list box.

multi_select

If set to true, the list box allows selecting multiple items instead of only one.

font, font_options, font_size and align

See #create_text_field.



349
350
351
352
353
354
355
356
357
358
# File 'lib/hexapdf/type/acro_form/form.rb', line 349

def create_list_box(name, option_items: nil, multi_select: nil, font: nil,
                    font_options: nil, font_size: nil, font_color: nil, align: nil)
  create_field(name, :Ch) do |field|
    field.initialize_as_list_box
    field.option_items = option_items if option_items
    field.flag(:multi_select) if multi_select
    apply_variable_text_properties(field, font: font, font_options: font_options,
                                   font_size: font_size, font_color: font_color, align: align)
  end
end

#create_multiline_text_field(name, font: nil, font_options: nil, font_size: nil, font_color: nil, align: nil) ⇒ Object

Creates a new multiline text field with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

The optional keyword arguments allow setting often used properties of the field, see #create_text_field for details.



217
218
219
220
221
222
223
224
# File 'lib/hexapdf/type/acro_form/form.rb', line 217

def create_multiline_text_field(name, font: nil, font_options: nil, font_size: nil,
                                font_color: nil, align: nil)
  create_field(name, :Tx) do |field|
    field.initialize_as_multiline_text_field
    apply_variable_text_properties(field, font: font, font_options: font_options,
                                   font_size: font_size, font_color: font_color, align: align)
  end
end

#create_namespace_field(name) ⇒ Object

Creates an untyped namespace field for creating hierarchies.

Example:

form.create_namespace_field('text')
form.create_text_field('text.a1')


173
174
175
# File 'lib/hexapdf/type/acro_form/form.rb', line 173

def create_namespace_field(name)
  create_field(name)
end

#create_password_field(name, font: nil, font_options: nil, font_size: nil, font_color: nil, align: nil) ⇒ Object

Creates a new password field with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

The optional keyword arguments allow setting often used properties of the field, see #create_text_field for details.



272
273
274
275
276
277
278
279
# File 'lib/hexapdf/type/acro_form/form.rb', line 272

def create_password_field(name, font: nil, font_options: nil, font_size: nil,
                          font_color: nil, align: nil)
  create_field(name, :Tx) do |field|
    field.initialize_as_password_field
    apply_variable_text_properties(field, font: font, font_options: font_options,
                                   font_size: font_size, font_color: font_color, align: align)
  end
end

#create_radio_button(name) ⇒ Object

Creates a radio button with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

Before a field value other than nil can be assigned to the radio button, at least one widget needs to be created.



301
302
303
# File 'lib/hexapdf/type/acro_form/form.rb', line 301

def create_radio_button(name)
  create_field(name, :Btn, &:initialize_as_radio_button)
end

#create_signature_field(name) ⇒ Object

Creates a signature field with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.



365
366
367
# File 'lib/hexapdf/type/acro_form/form.rb', line 365

def create_signature_field(name)
  create_field(name, :Sig)
end

#create_text_field(name, font: nil, font_options: nil, font_size: nil, font_color: nil, align: nil) ⇒ Object

Creates a new text field with the given name and adds it to the form.

The name may contain dots to signify a field hierarchy. If the parent fields don’t already exist, they are created as pure namespace fields (see #create_namespace_field). If the name doesn’t contain dots, a top-level field is created.

The optional keyword arguments allow setting often used properties of the field:

font

The font that should be used for the text of the field. If not specified, it defaults to Helvetica.

font_options

A hash with font options like :variant that should be used. If not specified, it defaults to the empty hash.

font_size

The font size that should be used. If not specified, it defaults to 0 (= auto-sizing).

font_color

The font color that should be used. If not specified, it defaults to 0 (i.e. black).

align

The alignment of the text, either :left, :center or :right.



201
202
203
204
205
206
207
# File 'lib/hexapdf/type/acro_form/form.rb', line 201

def create_text_field(name, font: nil, font_options: nil, font_size: nil, font_color: nil,
                      align: nil)
  create_field(name, :Tx) do |field|
    apply_variable_text_properties(field, font: font, font_options: font_options,
                                   font_size: font_size, font_color: font_color, align: align)
  end
end

#default_resourcesObject

Returns the dictionary containing the default resources for form field appearance streams.



437
438
439
# File 'lib/hexapdf/type/acro_form/form.rb', line 437

def default_resources
  self[:DR] ||= document.wrap({}, type: :XXResources)
end

#delete_field(name_or_field) ⇒ Object

:call-seq:

form.delete_field(name)
form.delete_field(field)

Deletes the field specified by the given name or via the given field object.

If the field is a signature field, the associated signature dictionary is also deleted.



376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/hexapdf/type/acro_form/form.rb', line 376

def delete_field(name_or_field)
  field = (name_or_field.kind_of?(String) ? field_by_name(name_or_field) : name_or_field)
  document.delete(field[:V]) if field.field_type == :Sig

  to_delete = field.each_widget(direct_only: false).to_a
  document.pages.each do |page|
    next unless page.key?(:Annots)
    page_annots = page[:Annots].to_a - to_delete
    page[:Annots].value.replace(page_annots)
  end

  if field[:Parent]
    field[:Parent][:Kids].delete(field)
  else
    self[:Fields].delete(field)
  end

  to_delete.each {|widget| document.delete(widget) }
  document.delete(field)
end

#each_field(terminal_only: true) ⇒ Object

:call-seq:

acroform.each_field(terminal_only: true) {|field| block}    -> acroform
acroform.each_field(terminal_only: true)                    -> Enumerator

Yields all terminal fields or all fields, depending on the terminal_only argument.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/hexapdf/type/acro_form/form.rb', line 126

def each_field(terminal_only: true)
  return to_enum(__method__, terminal_only: terminal_only) unless block_given?

  process_field_array = lambda do |array|
    array.each_with_index do |field, index|
      next if field.nil?
      unless field.respond_to?(:type) && field.type == :XXAcroFormField
        array[index] = field = Field.wrap(document, field)
      end
      if field.terminal_field?
        yield(field)
      else
        yield(field) unless terminal_only
        process_field_array.call(field[:Kids])
      end
    end
  end

  process_field_array.call(root_fields)
  self
end

#field_by_name(name) ⇒ Object

Returns the field with the given name or nil if no such field exists.



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/hexapdf/type/acro_form/form.rb', line 149

def field_by_name(name)
  fields = root_fields
  field = nil

  name.split('.').each do |part|
    field = nil
    fields&.each do |f|
      f = Field.wrap(document, f)
      next unless f[:T] == part
      field = f
      fields = field[:Kids] unless field.terminal_field?
      break
    end
  end

  field
end

#fill(data) ⇒ Object

Fills form fields with the values from the given data hash.

The keys of the data hash need to be full field names and the values are the respective values, usually in string form. It is possible to specify only some of the fields of the form.

What kind of values are supported for a field depends on the field type:

  • For fields containing text (single/multiline/comb text fields, file select fields, combo boxes and list boxes) the value needs to be a string and it is assigned as is.

  • For check boxes, the values “y”/“yes”/“t”/“true” are handled as assigning true to the field, the values “n”/“no”/“f”/“false” are handled as assigning false to the field, and every other string value is assigned as is. See ButtonField#field_value= for details.

  • For radio buttons the value needs to be a String or a Symbol representing the name of the radio button widget to select.



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/hexapdf/type/acro_form/form.rb', line 415

def fill(data)
  data.each do |field_name, value|
    field = field_by_name(field_name)
    raise HexaPDF::Error, "AcroForm field named '#{field_name}' not found" unless field

    case field.concrete_field_type
    when :single_line_text_field, :multiline_text_field, :comb_text_field, :file_select_field,
        :combo_box, :list_box, :editable_combo_box, :radio_button
      field.field_value = value
    when :check_box
      field.field_value = case value
                          when /\A(?:y(es)?|t(rue)?)\z/ then true
                          when /\A(?:n(o)?|f(alse)?)\z/ then false
                          else value
                          end
    else
      raise HexaPDF::Error, "AcroForm field type #{field.concrete_field_type} not yet supported"
    end
  end
end

#find_root_fieldsObject

Returns an array with all root fields that were found in the PDF document.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/hexapdf/type/acro_form/form.rb', line 98

def find_root_fields
  result = []
  document.pages.each do |page|
    page.each_annotation do |annot|
      if !annot.key?(:Parent) && annot.key?(:FT)
        result << Field.wrap(document, annot)
      elsif annot.key?(:Parent)
        field = annot[:Parent]
        field = field[:Parent] while field[:Parent]
        result << Field.wrap(document, field)
      end
    end
  end
  result
end

#find_root_fields!Object

Finds all root fields and sets /Fields appropriately.

See: #find_root_fields



117
118
119
# File 'lib/hexapdf/type/acro_form/form.rb', line 117

def find_root_fields!
  self[:Fields] = find_root_fields
end

#flatten(fields: nil, create_appearances: true) ⇒ Object

Flattens the whole interactive form or only the given fields, and returns the fields that couldn’t be flattened.

Flattening means making the appearance streams of the field widgets part of the respective page’s content stream and removing the fields themselves.

If the whole interactive form is flattened, the form object itself is also removed if all fields were flattened.

The create_appearances argument controls whether missing appearances should automatically be created.

See: HexaPDF::Type::Page#flatten_annotations



483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/hexapdf/type/acro_form/form.rb', line 483

def flatten(fields: nil, create_appearances: true)
  remove_form = fields.nil?
  fields ||= each_field.to_a
  if create_appearances
    fields.each {|field| field.create_appearances if field.respond_to?(:create_appearances) }
  end

  not_flattened = fields.map {|field| field.each_widget(direct_only: true).to_a }.flatten
  document.pages.each {|page| not_flattened = page.flatten_annotations(not_flattened) }
  not_flattened.map!(&:form_field)
  fields -= not_flattened

  fields.each do |field|
    (field[:Parent]&.[](:Kids) || self[:Fields]).delete(field)
    document.delete(field)
  end

  if remove_form && not_flattened.empty?
    document.catalog.delete(:AcroForm)
    document.delete(self)
  end

  not_flattened
end

#need_appearances!Object

Sets the /NeedAppearances field to true.

This will make PDF reader applications generate appropriate appearance streams based on the information stored in the fields and associated widgets.



457
458
459
# File 'lib/hexapdf/type/acro_form/form.rb', line 457

def need_appearances!
  self[:NeedAppearances] = true
end

#recalculate_fieldsObject

Recalculates all form fields that have a calculate action applied (which are all fields listed in the /CO entry).

If HexaPDF doesn’t support a calculation method or an error occurs during calculation, the field value is not updated.

Note that calculations are not done automatically when a form field’s value changes since it would lead to possibly many calls to this actions. So first fill in all field values and then call this method.

See: JavaScriptActions



519
520
521
522
523
524
525
526
# File 'lib/hexapdf/type/acro_form/form.rb', line 519

def recalculate_fields
  self[:CO]&.each do |field|
    field = Field.wrap(document, field)
    next unless field && (calculation_action = field[:AA]&.[](:C))
    result = JavaScriptActions.calculate(self, calculation_action)
    field.field_value = result if result
  end
end

#root_fieldsObject

Returns the PDFArray containing the root fields.



93
94
95
# File 'lib/hexapdf/type/acro_form/form.rb', line 93

def root_fields
  self[:Fields] ||= document.wrap([])
end

#set_default_appearance_string(font: 'Helvetica', font_options: {}, font_size: 0, font_color: 0) ⇒ Object

Sets the global default appearance string using the provided values or the default values which provide a sane default.

See VariableTextField::create_appearance_string for information on the arguments.



445
446
447
448
449
450
451
# File 'lib/hexapdf/type/acro_form/form.rb', line 445

def set_default_appearance_string(font: 'Helvetica', font_options: {}, font_size: 0,
                                  font_color: 0)
  self[:DA] = VariableTextField.create_appearance_string(document, font: font,
                                                         font_options: font_options,
                                                         font_size: font_size,
                                                         font_color: font_color)
end