mirror of https://github.com/docusealco/docuseal
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.
177 lines
5.3 KiB
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
|