You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/lib/templates/field_detection/config_based.rb

177 lines
5.3 KiB

# frozen_string_literal: true
module Templates
module FieldDetection
module ConfigBased
module_function
def call(template, config, documents = nil)
documents ||= template.schema_documents.preload(:blob)
attachment = documents.first
return [] unless attachment
submitter_map = ensure_submitters(template, config)
fields = build_fields(template, config, attachment, submitter_map)
template.fields = fields
template.save!
fields
end
def ensure_submitters(template, config)
submitter_map = {}
config_submitters = config['submitters'] || []
config_submitters.each do |cs|
name = cs['name'].to_s.strip
next if name.blank?
existing = template.submitters.find { |s| s['name'].to_s.downcase == name.downcase }
if existing
submitter_map[name.downcase] = existing['uuid']
else
uuid = SecureRandom.uuid
template.submitters << { 'name' => name, 'uuid' => uuid }
submitter_map[name.downcase] = uuid
end
end
submitter_map
end
def build_fields(template, config, attachment, submitter_map)
config_fields = config['fields'] || []
doc = Pdfium::Document.open_bytes(attachment.blob.download)
page_texts = extract_page_texts(doc)
config_fields.filter_map do |field_config|
build_field(field_config, template, attachment, submitter_map, doc, page_texts)
end
ensure
doc&.close
end
def build_field(field_config, template, attachment, submitter_map, doc, page_texts)
submitter_key = field_config['submitter'].to_s.downcase.strip
submitter_uuid = submitter_map[submitter_key] || template.submitters.first&.dig('uuid')
return nil unless submitter_uuid
area = resolve_area(field_config, doc, page_texts, attachment)
return nil unless area
{
'uuid' => SecureRandom.uuid,
'submitter_uuid' => submitter_uuid,
'name' => field_config['name'].to_s,
'type' => field_config['type'].to_s,
'required' => field_config.fetch('required', true),
'preferences' => field_config.fetch('preferences', {}),
'areas' => [area]
}
end
def resolve_area(field_config, doc, page_texts, attachment)
if field_config['anchor']
resolve_anchor_area(field_config, doc, page_texts, attachment)
elsif field_config['position']
resolve_absolute_area(field_config, doc, attachment)
end
end
def resolve_anchor_area(field_config, doc, page_texts, attachment)
anchor = field_config['anchor']
anchor_text = anchor['text'].to_s
anchor_page = anchor['page'] || 0
area_config = field_config['area'] || {}
resolved_page = resolve_page_index(anchor_page, doc.page_count)
return nil unless resolved_page && resolved_page >= 0 && resolved_page < doc.page_count
text_position = find_text_on_page(page_texts, resolved_page, anchor_text)
return nil unless text_position
{
'attachment_uuid' => attachment.uuid,
'page' => resolved_page,
'x' => clamp(text_position[:x] + (area_config['x_offset'] || 0).to_f),
'y' => clamp(text_position[:y] + (area_config['y_offset'] || 0).to_f),
'w' => clamp_dimension(area_config['w'] || 0.25),
'h' => clamp_dimension(area_config['h'] || 0.05)
}
end
def resolve_absolute_area(field_config, doc, attachment)
position = field_config['position']
page_index = resolve_page_index(position['page'] || 0, doc.page_count)
return nil unless page_index && page_index >= 0 && page_index < doc.page_count
{
'attachment_uuid' => attachment.uuid,
'page' => page_index,
'x' => clamp(position['x'].to_f),
'y' => clamp(position['y'].to_f),
'w' => clamp_dimension(position['w'] || 0.25),
'h' => clamp_dimension(position['h'] || 0.05)
}
end
def extract_page_texts(doc)
(0...doc.page_count).map do |page_index|
page = doc.get_page(page_index)
nodes = page.text_nodes
{ nodes: nodes, page_index: page_index }
ensure
page&.close
end
end
def find_text_on_page(page_texts, page_index, target_text)
page_data = page_texts[page_index]
return nil unless page_data
target_lower = target_text.downcase
accumulated = ''
first_node = nil
page_data[:nodes].each do |node|
first_node = node if accumulated.empty?
accumulated += node.content
return { x: first_node.x, y: first_node.y } if accumulated.downcase.include?(target_lower)
accumulated = '' if accumulated.length > target_text.length * 3
first_node = nil if accumulated.empty?
end
nil
end
def resolve_page_index(page, total_pages)
return nil if total_pages.zero?
page = page.to_i
page >= 0 ? page : total_pages + page
end
def clamp(value)
value.to_f.clamp(0.0, 1.0)
end
def clamp_dimension(value)
value.to_f.clamp(0.001, 1.0)
end
end
end
end