mirror of https://github.com/docusealco/docuseal
Compare commits
11 Commits
c216e43d24
...
11856a17ac
| Author | SHA1 | Date |
|---|---|---|
|
|
11856a17ac | 2 weeks ago |
|
|
7b4c74d711 | 2 weeks ago |
|
|
e0b0d4e803 | 2 weeks ago |
|
|
ec04a95e36 | 2 weeks ago |
|
|
887d80f6d7 | 2 weeks ago |
|
|
9ad281d457 | 2 weeks ago |
|
|
e316d2006f | 2 weeks ago |
|
|
f9669b0e16 | 2 weeks ago |
|
|
785eb789ad | 3 weeks ago |
|
|
436d947f6a | 3 weeks ago |
|
|
033ce3a169 | 3 weeks ago |
@ -0,0 +1,31 @@
|
||||
<% content_for(:html_title, "#{@template.name} | DocuSeal") %>
|
||||
<% I18n.with_locale(@template.account.locale) do %>
|
||||
<% content_for(:html_description, t('account_name_has_invited_you_to_fill_and_sign_documents_online_effortlessly_with_a_secure_fast_and_user_friendly_digital_document_signing_solution', account_name: @template.account.name)) %>
|
||||
<% end %>
|
||||
<main class="max-w-md mx-auto px-2 mt-12 mb-4">
|
||||
<div class="space-y-6 mx-auto">
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<%= render 'start_form/banner' %>
|
||||
</div>
|
||||
<div class="flex items-center bg-base-200 rounded-xl p-4 mb-4">
|
||||
<div class="flex items-center">
|
||||
<div class="mr-3">
|
||||
<%= svg_icon('writing_sign', class: 'w-10 h-10') %>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-lg font-bold mb-1"><%= @template.name %></p>
|
||||
<p dir="auto" class="text-sm"><%= t('invited_by_html', name: @template.account.name) %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert items-start bg-base-100 border border-base-300">
|
||||
<%= svg_icon('info_circle', class: 'stroke-current shrink-0 h-6 w-6 mt-1') %>
|
||||
<div class="text-sm"><%= @error_message %></div>
|
||||
</div>
|
||||
<div dir="auto" class="form-control">
|
||||
<%= link_to t('back'), @resubmit_submitter ? submit_form_path(@resubmit_submitter.slug) : start_form_path(@template.slug, @submitter.slice(:name, :email, :phone).compact_blank), class: 'base-button' %>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ -1,40 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module BuildAnnotations
|
||||
module_function
|
||||
|
||||
def call(data)
|
||||
pdf = HexaPDF::Document.new(io: StringIO.new(data))
|
||||
|
||||
pdf.pages.flat_map.with_index do |page, index|
|
||||
(page[:Annots] || []).filter_map do |annot|
|
||||
next if annot.blank?
|
||||
next if annot.is_a?(Integer) || annot.is_a?(Symbol) || annot.is_a?(HexaPDF::PDFArray)
|
||||
next if annot[:A].blank? || annot[:A][:URI].blank?
|
||||
next unless annot[:Subtype] == :Link
|
||||
next if !annot[:A][:URI].starts_with?('https://') && !annot[:A][:URI].starts_with?('http://')
|
||||
|
||||
build_external_link_hash(page, annot).merge('page' => index)
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
[]
|
||||
end
|
||||
|
||||
def build_external_link_hash(page, annot)
|
||||
left, bottom, right, top = annot[:Rect]
|
||||
|
||||
{
|
||||
'type' => 'external_link',
|
||||
'value' => annot[:A][:URI],
|
||||
'x' => left / page.box.width.to_f,
|
||||
'y' => (page.box.height - top) / page.box.height.to_f,
|
||||
'w' => (right - left) / page.box.width.to_f,
|
||||
'h' => (top - bottom) / page.box.height.to_f
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,288 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Templates
|
||||
module FindAcroFields
|
||||
PDF_CONTENT_TYPE = 'application/pdf'
|
||||
|
||||
FIELD_NAME_REGEXP = /\A(?=.*\p{L})[\p{L}\d\s-]+\z/
|
||||
SKIP_FIELD_DESCRIPTION = %w[undefined].freeze
|
||||
SELECT_PLACEHOLDER_REGEXP = /\b(
|
||||
Select |
|
||||
Choose |
|
||||
Wählen |
|
||||
Auswählen |
|
||||
Sélectionner|
|
||||
Choisir |
|
||||
Seleccionar |
|
||||
Elegir |
|
||||
Seleziona |
|
||||
Scegliere |
|
||||
Selecionar |
|
||||
Escolher
|
||||
)\b/ix
|
||||
|
||||
DATE_FORMAT_REGEXP = %r{[myd]{2,4}[-\\/\s.][myd]{2,4}[-\\/\s.][myd]{2,4}}i
|
||||
|
||||
FIELD_ALIGNMENT = {
|
||||
0 => 'left',
|
||||
1 => 'center',
|
||||
2 => 'right'
|
||||
}.freeze
|
||||
|
||||
module_function
|
||||
|
||||
# rubocop:disable Metrics
|
||||
def call(pdf, attachment, data)
|
||||
return [] if pdf.acro_form.blank? && data.exclude?('/Form')
|
||||
|
||||
fields, annots_index = build_fields_with_pages(pdf)
|
||||
|
||||
fields.filter_map do |field|
|
||||
areas = Array.wrap(field[:Kids] || field).filter_map do |child_field|
|
||||
page = annots_index[child_field.hash]
|
||||
|
||||
next unless page
|
||||
|
||||
media_box = page[:CropBox] || page[:MediaBox]
|
||||
crop_box = page[:CropBox] || media_box
|
||||
|
||||
media_box_start = [media_box[0], media_box[1]]
|
||||
crop_shift = [crop_box[0] - media_box[0], crop_box[1] - media_box[1]]
|
||||
|
||||
next unless child_field[:Rect]
|
||||
|
||||
x0, y0, x1, y1 = child_field[:Rect]
|
||||
|
||||
x0, y0 = correct_coordinates(x0, y0, crop_shift, media_box_start)
|
||||
x1, y1 = correct_coordinates(x1, y1, crop_shift, media_box_start)
|
||||
|
||||
page_width = media_box[2] - media_box[0]
|
||||
page_height = media_box[3] - media_box[1]
|
||||
|
||||
x = x0
|
||||
y = y0
|
||||
w = x1 - x0
|
||||
h = y1 - y0
|
||||
|
||||
transformed_y = page_height - y - h
|
||||
|
||||
attrs = {
|
||||
page: page.index,
|
||||
x: x / page_width.to_f,
|
||||
y: transformed_y / page_height.to_f,
|
||||
w: w / page_width.to_f,
|
||||
h: h / page_height.to_f,
|
||||
attachment_uuid: attachment.uuid
|
||||
}
|
||||
|
||||
next if attrs[:w].zero? || attrs[:h].zero?
|
||||
|
||||
if child_field[:MaxLen] && child_field.try(:concrete_field_type) == :comb_text_field
|
||||
attrs[:cell_w] = w / page_width.to_f / child_field[:MaxLen].to_f
|
||||
end
|
||||
|
||||
attrs
|
||||
end
|
||||
|
||||
next if areas.blank?
|
||||
|
||||
field_properties = build_field_properties(field)
|
||||
|
||||
next if field_properties.blank?
|
||||
next if field_properties[:default_value].present?
|
||||
|
||||
if field_properties[:type].in?(%w[radio multiple])
|
||||
if areas.size != field_properties[:options].size
|
||||
field_properties[:options] = build_options(Array.new(areas.size, ''))
|
||||
end
|
||||
|
||||
areas.each_with_index do |area, index|
|
||||
area[:option_uuid] = field_properties[:options][index][:uuid]
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
uuid: SecureRandom.uuid,
|
||||
required: field.flags.include?(:required),
|
||||
preferences: {},
|
||||
areas:,
|
||||
**field_properties
|
||||
}
|
||||
end
|
||||
rescue StandardError => e
|
||||
raise if Rails.env.local?
|
||||
|
||||
Rollbar.error(e) if defined?(Rollbar)
|
||||
|
||||
[]
|
||||
end
|
||||
|
||||
def correct_coordinates(x_coord, y_coord, shift, media_box_start)
|
||||
corrected_x = x_coord + shift[0] - media_box_start[0]
|
||||
corrected_y = y_coord + shift[1] - media_box_start[1]
|
||||
|
||||
[corrected_x, corrected_y]
|
||||
end
|
||||
|
||||
def build_field_properties(field)
|
||||
field_name = field.full_field_name if field.full_field_name.to_s.match?(FIELD_NAME_REGEXP)
|
||||
|
||||
field_name = field_name&.encode('utf-8', invalid: :replace, undef: :replace, replace: '')
|
||||
|
||||
attrs = { name: field_name.to_s }
|
||||
attrs[:description] = field[:TU] if field[:TU].present? &&
|
||||
field[:TU] != field.full_field_name &&
|
||||
!field[:TU].in?(SKIP_FIELD_DESCRIPTION)
|
||||
|
||||
if field[:Q].present? && field.field_type == :Tx
|
||||
attrs[:preferences] ||= {}
|
||||
attrs[:preferences][:align] = FIELD_ALIGNMENT.fetch(field[:Q], 'left')
|
||||
end
|
||||
|
||||
if field.field_type == :Btn && field.concrete_field_type == :radio_button && field[:Opt].present?
|
||||
selected_option_index = (field.allowed_values || []).find_index(field.field_value)
|
||||
selected_option = field[:Opt][selected_option_index] if selected_option_index
|
||||
|
||||
{
|
||||
**attrs,
|
||||
type: 'radio',
|
||||
options: build_options(field[:Opt], 'radio'),
|
||||
default_value: selected_option
|
||||
}
|
||||
elsif field.field_type == :Btn && %i[check_box radio_button].include?(field.concrete_field_type) &&
|
||||
field[:Kids].present? && field[:Kids].size > 1 && field.allowed_values.size > 1
|
||||
selected_option = (field.allowed_values || []).find { |v| v == field.field_value }
|
||||
|
||||
return {} if field.allowed_values.include?(:BBox)
|
||||
|
||||
{
|
||||
**attrs,
|
||||
type: 'radio',
|
||||
options: build_options(field.allowed_values, 'radio'),
|
||||
default_value: selected_option
|
||||
}
|
||||
elsif field.field_type == :Btn && %i[check_box radio_button].include?(field.concrete_field_type)
|
||||
{
|
||||
**attrs,
|
||||
type: 'checkbox',
|
||||
default_value: field.field_value.present?
|
||||
}
|
||||
elsif field.field_type == :Ch &&
|
||||
%i[combo_box editable_combo_box].include?(field.concrete_field_type) && field[:Opt].present?
|
||||
{
|
||||
**attrs,
|
||||
type: 'select',
|
||||
options: build_options(field[:Opt], 'select'),
|
||||
default_value: field.field_value.to_s.match?(SELECT_PLACEHOLDER_REGEXP) ? nil : field.field_value.presence
|
||||
}
|
||||
elsif field.field_type == :Ch && field.concrete_field_type == :multi_select && field[:Opt].present?
|
||||
{
|
||||
**attrs,
|
||||
type: 'multiple',
|
||||
options: build_options(field[:Opt], 'multiple'),
|
||||
default_value: field.field_value.presence
|
||||
}
|
||||
elsif field.field_type == :Tx && field.concrete_field_type == :comb_text_field
|
||||
{
|
||||
**attrs,
|
||||
type: 'cells',
|
||||
default_value: field.field_value.presence
|
||||
}
|
||||
elsif field.field_type == :Tx
|
||||
if field[:AA] && ((field[:AA][:F] && field[:AA][:F][:JS].include?('AFDate_')) ||
|
||||
(field[:AA][:K] && field[:AA][:K][:JS].include?('AFDate_')))
|
||||
if (format = field[:AA][:F][:JS][DATE_FORMAT_REGEXP])
|
||||
attrs[:preferences] ||= {}
|
||||
attrs[:preferences][:format] = format.upcase
|
||||
end
|
||||
|
||||
{
|
||||
**attrs,
|
||||
type: 'date',
|
||||
default_value: field.field_value.presence
|
||||
}
|
||||
else
|
||||
{
|
||||
**attrs,
|
||||
type: 'text',
|
||||
default_value: field.field_value.presence
|
||||
}
|
||||
end
|
||||
elsif field.field_type == :Sig
|
||||
{
|
||||
**attrs,
|
||||
type: field.try(:field_name).to_s.downcase.include?('initials') ? 'initials' : 'signature'
|
||||
}
|
||||
else
|
||||
{}
|
||||
end.compact
|
||||
end
|
||||
|
||||
def build_options(values, type = nil)
|
||||
is_skip_single_value = type.in?(%w[radio multiple]) && values.uniq.size == 1
|
||||
|
||||
values.filter_map do |option|
|
||||
is_option_number = option.is_a?(Symbol) && option.to_s.match?(/\A\d+\z/)
|
||||
|
||||
option = option[1] if option.is_a?(Array) && option.size == 2
|
||||
|
||||
if option.is_a?(String) || option.is_a?(Symbol)
|
||||
option = option.to_s.encode('utf-8', invalid: :replace, undef: :replace, replace: '')
|
||||
end
|
||||
|
||||
next if type == 'select' && option.to_s.match?(SELECT_PLACEHOLDER_REGEXP)
|
||||
|
||||
{
|
||||
uuid: SecureRandom.uuid,
|
||||
value: is_option_number || is_skip_single_value ? '' : option.presence
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def build_fields_with_pages(pdf)
|
||||
fields_index = {}
|
||||
annots_index = {}
|
||||
|
||||
pdf.pages.each do |page|
|
||||
page.each_annotation do |annot|
|
||||
annots_index[annot.hash] = page
|
||||
|
||||
if !annot.key?(:Parent) && annot.key?(:FT)
|
||||
fields_index[annot.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, annot)
|
||||
elsif annot.key?(:Parent)
|
||||
field = annot[:Parent]
|
||||
seen = Set.new.compare_by_identity
|
||||
|
||||
field = field[:Parent] while field[:Parent] && seen.add?(field.value)
|
||||
|
||||
annots_index[field.hash] ||= page
|
||||
fields_index[field.hash] ||= HexaPDF::Type::AcroForm::Field.wrap(pdf, field)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[process_fields_array(pdf, fields_index.values), annots_index]
|
||||
end
|
||||
|
||||
def process_fields_array(pdf, array, acc = [], seen = Set.new.compare_by_identity)
|
||||
array.each_with_index do |field, index|
|
||||
next if field.nil?
|
||||
|
||||
unless field.respond_to?(:type) && field.type == :XXAcroFormField
|
||||
array[index] = field = HexaPDF::Type::AcroForm::Field.wrap(pdf, field)
|
||||
end
|
||||
|
||||
next unless seen.add?(field.value)
|
||||
|
||||
if field.terminal_field?
|
||||
acc << field
|
||||
else
|
||||
process_fields_array(pdf, field[:Kids], acc, seen)
|
||||
end
|
||||
end
|
||||
|
||||
acc
|
||||
end
|
||||
# rubocop:enable Metrics
|
||||
end
|
||||
end
|
||||
Loading…
Reference in new issue