Module: HexaPDF::Task::MergeAcroForm

Defined in:
lib/hexapdf/task/merge_acro_form.rb

Overview

Task for merging an AcroForm from one PDF into another.

It takes care of

  • adding the fields to the main Type::AcroForm::Form dictionary,

  • adjusting the field names so that they are unique,

  • and merging the properties of the main AcroForm dictionary itself and adjusting field information appropriately.

Note that the pages with the fields need to be imported already.

The steps for using this task are:

  1. Import the pages into the target document and add all imported pages to an array

  2. Call this task using the created array of pages.

Example:

pages = doc.pages.map {|page| target.pages.add(target.import(page)) }
target.task(:merge_acro_form, source: doc, pages: pages)

Class Method Summary collapse

Class Method Details

.call(doc, source:, pages:) ⇒ Object

Performs the necessary steps to merge the AcroForm fields from the source into the target document doc.

source

Specifies the source PDF document the information from which should be merged into the target document.

pages

An array of pages that were imported from source and contain the widgets of the fields that should be merged.



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/hexapdf/task/merge_acro_form.rb', line 74

def self.call(doc, source:, pages:)
  return unless source.acro_form

  acro_form = doc.acro_form(create: true)

  # Determine a unique name for root field and create root field
  import_name = 'merged_' +
                (acro_form.root_fields.select {|field| field[:T] =~ /\Amerged_\d+\z/ }.
                  map {|field| field[:T][/\d+/].to_i }.sort.last || 0).succ.to_s
  root_field = doc.add({T: import_name, Kids: []})
  acro_form.root_fields << root_field

  # Merge the main AcroForm dictionary
  font_name_mapping = merge_form_dictionary(acro_form, source.acro_form, root_field)
  font_name_re = font_name_mapping.keys.map {|name| Regexp.escape(name) }.join('|')
  root_field[:DA] && root_field[:DA].sub!(font_name_re, font_name_mapping)

  # Process all field widgets of the given pages
  process_calculate_actions = false
  signature_field_seen = false
  pages.each do |page|
    page.each_annotation do |widget|
      next unless widget[:Subtype] == :Widget
      field = widget.form_field

      # Correct the font name in the default appearance string
      widget[:DA] && widget[:DA].sub!(font_name_re, font_name_mapping)
      field[:DA] && field[:DA].sub!(font_name_re, font_name_mapping)

      process_calculate_actions = true if field[:AA]&.[](:C)
      signature_field_seen = true if field.field_type == :Sig

      # Add to the root field
      field = field[:Parent] while field[:Parent]
      if field != root_field
        field[:Parent] = root_field
        root_field[:Kids] << field
      end
    end
  end

  # Update calculation JavaScript actions with changed field names
  fix_calculate_actions(acro_form, source.acro_form, import_name) if process_calculate_actions

  # Update signature flags if necessary
  if signature_field_seen && source.acro_form.signature_flag?(:signatures_exist)
    acro_form.signature_flag(:signatures_exist)
  end
end

.fix_calculate_actions(acro_form, source_form, import_name) ⇒ Object

Fixes the calculate actions listed in the /CO entry of the main AcroForm dictionary to use the new names of the fields.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/hexapdf/task/merge_acro_form.rb', line 144

def self.fix_calculate_actions(acro_form, source_form, import_name)
  if source_form[:CO]
    acro_form[:CO] ||= []
    acro_form[:CO].value.concat(acro_form.document.import(source_form[:CO]).value)
    acro_form[:CO].each do |field|
      next unless (action = field[:AA]&.[](:C))
      action[:JS].gsub!(/"(.*?)"/) do |match|
        if source_form.field_by_name($1)
          "\"#{import_name}.#{$1}\""
        else
          match
        end
      end
    end
  end
end

.merge_form_dictionary(target_form, source_form, root_field) ⇒ Object

Merges the AcroForm source_form into the target_form and returns a mapping of old font names to new ones.



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/hexapdf/task/merge_acro_form.rb', line 126

def self.merge_form_dictionary(target_form, source_form, root_field)
  target_resources = target_form.default_resources
  font_name_mapping = {}
  serializer = HexaPDF::Serializer.new

  source_form.default_resources[:Font].each do |font_name, value|
    new_name = target_resources.add_font(target_form.document.import(value))
    font_name_mapping[serializer.serialize(font_name)] = serializer.serialize(new_name)
  end

  root_field[:DA] = target_form.document.import(source_form[:DA])
  root_field[:Q] = target_form.document.import(source_form[:Q])

  font_name_mapping
end