2023-05-21 17:59:36 +03:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
|
|
module Submissions
|
|
|
|
|
module GenerateResultAttachments
|
2023-06-18 16:30:35 +03:00
|
|
|
FONT_SIZE = 11
|
2024-02-16 11:34:30 +02:00
|
|
|
FONT_PATH = '/fonts/GoNotoKurrent-Regular.ttf'
|
2023-08-25 00:27:56 +03:00
|
|
|
FONT_NAME = if File.exist?(FONT_PATH)
|
|
|
|
|
FONT_PATH
|
|
|
|
|
else
|
|
|
|
|
'Helvetica'
|
|
|
|
|
end
|
2023-05-28 23:20:48 +03:00
|
|
|
|
2023-12-21 23:29:06 +02:00
|
|
|
SIGN_REASON = 'Signed by %<name>s with DocuSeal.co'
|
2023-06-24 12:27:34 +03:00
|
|
|
|
2024-01-15 01:45:26 +02:00
|
|
|
RTL_REGEXP = TextUtils::RTL_REGEXP
|
2024-01-07 14:19:21 +02:00
|
|
|
|
2023-06-24 22:50:12 +03:00
|
|
|
TEXT_LEFT_MARGIN = 1
|
|
|
|
|
TEXT_TOP_MARGIN = 1
|
2024-04-22 19:10:58 +03:00
|
|
|
MAX_PAGE_ROTATE = 20
|
2023-06-24 22:50:12 +03:00
|
|
|
|
2023-06-22 01:31:39 +03:00
|
|
|
A4_SIZE = [595, 842].freeze
|
2023-06-24 22:50:12 +03:00
|
|
|
SUPPORTED_IMAGE_TYPES = ['image/png', 'image/jpeg'].freeze
|
2023-06-22 01:31:39 +03:00
|
|
|
|
2024-03-15 19:02:17 +02:00
|
|
|
MISSING_GLYPH_REPLACE = {
|
|
|
|
|
'▪' => '-',
|
2024-02-13 22:23:42 +02:00
|
|
|
'✔️' => 'V',
|
2024-03-15 19:02:17 +02:00
|
|
|
'✔' => 'V',
|
2024-06-04 11:48:02 +03:00
|
|
|
'✓' => 'V',
|
2024-03-15 19:02:17 +02:00
|
|
|
'✅' => 'V'
|
|
|
|
|
}.freeze
|
|
|
|
|
|
|
|
|
|
MISSING_GLYPH_REPLACE_TYPE1 = {
|
|
|
|
|
'▪' => :bullet,
|
|
|
|
|
'✔️' => :V,
|
|
|
|
|
'✔' => :V,
|
2024-06-04 11:48:02 +03:00
|
|
|
'✓' => :V,
|
2024-03-15 19:02:17 +02:00
|
|
|
'✅' => :V
|
2024-02-08 21:11:31 +02:00
|
|
|
}.freeze
|
|
|
|
|
|
2023-05-21 17:59:36 +03:00
|
|
|
module_function
|
|
|
|
|
|
2023-05-28 23:20:48 +03:00
|
|
|
# rubocop:disable Metrics
|
2023-06-13 00:29:07 +03:00
|
|
|
def call(submitter)
|
2024-05-25 20:57:36 +03:00
|
|
|
pdfs_index = generate_pdfs(submitter)
|
2023-06-27 00:57:29 +03:00
|
|
|
|
2023-06-13 00:29:07 +03:00
|
|
|
template = submitter.submission.template
|
2024-05-25 20:57:36 +03:00
|
|
|
account = submitter.account
|
|
|
|
|
|
|
|
|
|
pkcs = Accounts.load_signing_pkcs(account)
|
|
|
|
|
tsa_url = Accounts.load_timeserver_url(account)
|
|
|
|
|
|
|
|
|
|
image_pdfs = []
|
|
|
|
|
original_documents = template.documents.preload(:blob)
|
|
|
|
|
|
|
|
|
|
result_attachments =
|
|
|
|
|
submitter.submission.template_schema.map do |item|
|
|
|
|
|
pdf = pdfs_index[item['attachment_uuid']]
|
|
|
|
|
|
|
|
|
|
attachment = build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:,
|
|
|
|
|
uuid: item['attachment_uuid'],
|
|
|
|
|
name: item['name'])
|
|
|
|
|
|
|
|
|
|
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
|
|
|
|
|
|
|
|
|
|
attachment
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return result_attachments.map { |e| e.tap(&:save!) } if image_pdfs.size < 2
|
|
|
|
|
|
|
|
|
|
images_pdf =
|
|
|
|
|
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
|
|
|
|
|
pdf.pages.each { |page| doc.pages << doc.import(page) }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
images_pdf_attachment =
|
|
|
|
|
build_pdf_attachment(
|
|
|
|
|
pdf: images_pdf,
|
|
|
|
|
submitter:,
|
|
|
|
|
tsa_url:,
|
|
|
|
|
pkcs:,
|
|
|
|
|
uuid: images_pdf_uuid(original_documents.select(&:image?)),
|
|
|
|
|
name: template.name
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
(result_attachments + [images_pdf_attachment]).map { |e| e.tap(&:save!) }
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def generate_pdfs(submitter)
|
|
|
|
|
cell_layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align: :center)
|
2023-06-13 00:29:07 +03:00
|
|
|
|
2024-04-22 21:34:55 +03:00
|
|
|
is_flatten =
|
|
|
|
|
submitter.account.account_configs
|
|
|
|
|
.find_or_initialize_by(key: AccountConfig::FLATTEN_RESULT_PDF_KEY).value != false
|
|
|
|
|
|
2024-03-30 19:37:48 +02:00
|
|
|
account = submitter.account
|
2024-02-23 10:49:28 +02:00
|
|
|
attachments_data_cache = {}
|
2023-05-28 23:20:48 +03:00
|
|
|
|
2024-04-22 21:34:55 +03:00
|
|
|
pdfs_index = build_pdfs_index(submitter, flatten: is_flatten)
|
2023-05-28 23:20:48 +03:00
|
|
|
|
2023-07-26 09:58:50 +03:00
|
|
|
submitter.submission.template_fields.each do |field|
|
2023-06-16 23:39:55 +03:00
|
|
|
next if field['submitter_uuid'] != submitter.uuid
|
|
|
|
|
|
2023-05-28 23:20:48 +03:00
|
|
|
field.fetch('areas', []).each do |area|
|
|
|
|
|
pdf = pdfs_index[area['attachment_uuid']]
|
|
|
|
|
|
2023-12-11 18:28:24 +02:00
|
|
|
next if pdf.nil?
|
|
|
|
|
|
2023-05-28 23:20:48 +03:00
|
|
|
page = pdf.pages[area['page']]
|
2023-12-11 18:28:24 +02:00
|
|
|
|
|
|
|
|
next if page.nil?
|
|
|
|
|
|
2023-09-06 17:27:54 +03:00
|
|
|
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
2023-05-28 23:20:48 +03:00
|
|
|
|
2023-08-12 12:34:02 +03:00
|
|
|
page[:Annots] ||= []
|
2024-04-01 13:09:35 +03:00
|
|
|
page[:Annots] = page[:Annots].try(:reject) do |e|
|
|
|
|
|
next if e.is_a?(Integer)
|
|
|
|
|
|
2023-12-06 16:04:28 +02:00
|
|
|
e.present? && e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field')
|
2024-04-01 13:09:35 +03:00
|
|
|
end || page[:Annots]
|
2023-08-12 12:34:02 +03:00
|
|
|
|
2023-05-28 23:20:48 +03:00
|
|
|
width = page.box.width
|
|
|
|
|
height = page.box.height
|
2024-06-20 16:14:18 +03:00
|
|
|
|
|
|
|
|
preferences_font_size = field.dig('preferences', 'font_size').then { |num| num.present? ? num.to_i : nil }
|
|
|
|
|
|
|
|
|
|
font_size = preferences_font_size
|
|
|
|
|
font_size ||= (([page.box.width, page.box.height].min / A4_SIZE[0].to_f) * FONT_SIZE).to_i
|
2023-05-28 23:20:48 +03:00
|
|
|
|
2023-06-13 00:29:07 +03:00
|
|
|
value = submitter.values[field['uuid']]
|
2023-05-28 23:20:48 +03:00
|
|
|
|
2024-03-17 19:06:00 +02:00
|
|
|
text_align = field.dig('preferences', 'align').to_s.to_sym.presence ||
|
|
|
|
|
(value.to_s.match?(RTL_REGEXP) ? :right : :left)
|
|
|
|
|
|
|
|
|
|
layouter = HexaPDF::Layout::TextLayouter.new(text_valign: :center, text_align:,
|
2024-01-07 14:19:21 +02:00
|
|
|
font: pdf.fonts.add(FONT_NAME), font_size:)
|
|
|
|
|
|
2023-07-08 20:51:37 +03:00
|
|
|
next if Array.wrap(value).compact_blank.blank?
|
|
|
|
|
|
2024-04-22 21:34:55 +03:00
|
|
|
if is_flatten
|
|
|
|
|
begin
|
|
|
|
|
page.flatten_annotations
|
|
|
|
|
rescue StandardError => e
|
|
|
|
|
Rollbar.error(e) if defined?(Rollbar)
|
|
|
|
|
end
|
2024-03-27 18:24:41 +02:00
|
|
|
end
|
|
|
|
|
|
2023-05-28 23:20:48 +03:00
|
|
|
canvas = page.canvas(type: :overlay)
|
2023-08-25 00:27:56 +03:00
|
|
|
canvas.font(FONT_NAME, size: font_size)
|
2023-05-28 23:20:48 +03:00
|
|
|
|
|
|
|
|
case field['type']
|
2023-12-22 13:33:53 +02:00
|
|
|
when 'image', 'signature', 'initials', 'stamp'
|
2023-06-14 02:08:26 +03:00
|
|
|
attachment = submitter.attachments.find { |a| a.uuid == value }
|
2023-06-24 22:50:12 +03:00
|
|
|
|
2024-02-23 10:49:28 +02:00
|
|
|
attachments_data_cache[attachment.uuid] ||= attachment.download
|
|
|
|
|
|
|
|
|
|
image = Vips::Image.new_from_buffer(attachments_data_cache[attachment.uuid], '').autorot
|
2023-05-21 17:59:36 +03:00
|
|
|
|
2023-07-07 21:29:38 +03:00
|
|
|
scale = [(area['w'] * width) / image.width,
|
|
|
|
|
(area['h'] * height) / image.height].min
|
2023-07-07 00:54:32 +03:00
|
|
|
|
2023-12-06 19:01:30 +02:00
|
|
|
io = StringIO.new(image.resize([scale * 4, 1].select(&:positive?).min).write_to_buffer('.png'))
|
2023-07-07 00:54:32 +03:00
|
|
|
|
2023-06-18 16:30:35 +03:00
|
|
|
canvas.image(
|
|
|
|
|
io,
|
|
|
|
|
at: [
|
2023-07-07 21:29:38 +03:00
|
|
|
(area['x'] * width) + (area['w'] * width / 2) - ((image.width * scale) / 2),
|
|
|
|
|
height - (area['y'] * height) - (image.height * scale / 2) - (area['h'] * height / 2)
|
2023-06-18 16:30:35 +03:00
|
|
|
],
|
2023-07-07 21:29:38 +03:00
|
|
|
width: image.width * scale,
|
|
|
|
|
height: image.height * scale
|
2023-06-18 16:30:35 +03:00
|
|
|
)
|
2023-12-14 21:22:26 +02:00
|
|
|
when 'file', 'payment'
|
2023-06-18 16:30:35 +03:00
|
|
|
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
2023-06-14 02:08:26 +03:00
|
|
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
2023-05-21 17:59:36 +03:00
|
|
|
|
2023-08-09 09:33:14 +03:00
|
|
|
acc << HexaPDF::Layout::InlineBox.create(width: font_size, height: font_size,
|
2023-06-18 16:30:35 +03:00
|
|
|
margin: [0, 1, -2, 0]) do |cv, box|
|
|
|
|
|
cv.image(PdfIcons.paperclip_io, at: [0, 0], width: box.content_width)
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
acc << HexaPDF::Layout::TextFragment.create("#{attachment.filename}\n", font: pdf.fonts.add(FONT_NAME),
|
2023-08-09 09:33:14 +03:00
|
|
|
font_size:)
|
2023-05-28 23:20:48 +03:00
|
|
|
end
|
2023-06-18 16:30:35 +03:00
|
|
|
|
|
|
|
|
lines = layouter.fit(items, area['w'] * width, height).lines
|
|
|
|
|
|
|
|
|
|
box_height = lines.sum(&:height)
|
|
|
|
|
height_diff = [0, box_height - (area['h'] * height)].max
|
|
|
|
|
|
|
|
|
|
lines.each_with_index.reduce(0) do |acc, (line, index)|
|
|
|
|
|
next acc unless line.items.first.is_a?(HexaPDF::Layout::InlineBox)
|
|
|
|
|
|
|
|
|
|
attachment_uuid = Array.wrap(value)[acc]
|
|
|
|
|
attachment = submitter.attachments.find { |a| a.uuid == attachment_uuid }
|
|
|
|
|
|
2024-04-23 20:09:39 +03:00
|
|
|
next_index = lines[(index + 1)..].index { |l| l.items.first.is_a?(HexaPDF::Layout::InlineBox) }
|
|
|
|
|
next_index += index if next_index
|
|
|
|
|
next_index ||= lines.size - 1
|
|
|
|
|
|
|
|
|
|
diff = ((area['h'] * height) / 2) - (lines.sum(&:height) / 2)
|
2023-06-18 16:30:35 +03:00
|
|
|
|
|
|
|
|
page[:Annots] << pdf.add(
|
|
|
|
|
{
|
|
|
|
|
Type: :Annot, Subtype: :Link,
|
|
|
|
|
Rect: [
|
2023-06-24 22:50:12 +03:00
|
|
|
(area['x'] * width) + TEXT_LEFT_MARGIN,
|
2024-04-23 20:09:39 +03:00
|
|
|
height - (area['y'] * height) - lines[...index].sum(&:height) +
|
|
|
|
|
height_diff - (height_diff.zero? ? diff : 0),
|
2023-06-24 22:50:12 +03:00
|
|
|
(area['x'] * width) + (area['w'] * width) + TEXT_LEFT_MARGIN,
|
2024-04-23 20:09:39 +03:00
|
|
|
height - (area['y'] * height) - lines[..next_index].sum(&:height) +
|
|
|
|
|
height_diff - (height_diff.zero? ? diff : 0)
|
2023-06-18 16:30:35 +03:00
|
|
|
],
|
2023-07-04 01:58:43 +03:00
|
|
|
A: { Type: :Action, S: :URI,
|
2024-02-18 11:26:30 +02:00
|
|
|
URI: ActiveStorage::Blob.proxy_url(attachment.blob) }
|
2023-06-18 16:30:35 +03:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
acc + 1
|
2023-05-21 17:59:36 +03:00
|
|
|
end
|
2023-06-18 16:30:35 +03:00
|
|
|
|
|
|
|
|
layouter.fit(items, area['w'] * width, height_diff.positive? ? box_height : area['h'] * height)
|
2023-06-24 22:50:12 +03:00
|
|
|
.draw(canvas, (area['x'] * width) + TEXT_LEFT_MARGIN,
|
|
|
|
|
height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN)
|
2023-11-14 01:30:00 +02:00
|
|
|
when ->(type) { type == 'checkbox' || (type.in?(%w[multiple radio]) && area['option_uuid'].present?) }
|
|
|
|
|
if field['type'].in?(%w[multiple radio])
|
|
|
|
|
option = field['options']&.find { |o| o['uuid'] == area['option_uuid'] }
|
|
|
|
|
|
2024-03-26 19:26:03 +02:00
|
|
|
option_name = option['value'].presence || "Option #{field['options'].index(option) + 1}"
|
|
|
|
|
|
|
|
|
|
value = Array.wrap(value).include?(option_name)
|
2023-11-14 01:30:00 +02:00
|
|
|
end
|
|
|
|
|
|
2023-06-18 16:30:35 +03:00
|
|
|
next unless value == true
|
|
|
|
|
|
|
|
|
|
scale = [(area['w'] * width) / PdfIcons::WIDTH, (area['h'] * height) / PdfIcons::HEIGHT].min
|
|
|
|
|
|
|
|
|
|
canvas.image(
|
|
|
|
|
PdfIcons.check_io,
|
|
|
|
|
at: [
|
|
|
|
|
(area['x'] * width) + (area['w'] * width / 2) - (PdfIcons::WIDTH * scale / 2),
|
|
|
|
|
height - (area['y'] * height) - (area['h'] * height / 2) - (PdfIcons::HEIGHT * scale / 2)
|
|
|
|
|
],
|
|
|
|
|
width: PdfIcons::WIDTH * scale,
|
|
|
|
|
height: PdfIcons::HEIGHT * scale
|
|
|
|
|
)
|
2024-02-29 12:57:23 +02:00
|
|
|
when ->(type) { type == 'cells' && area['cell_w'] }
|
2023-06-27 00:57:29 +03:00
|
|
|
cell_width = area['cell_w'] * width
|
|
|
|
|
|
2024-01-15 01:45:26 +02:00
|
|
|
TextUtils.maybe_rtl_reverse(value).chars.each_with_index do |char, index|
|
2023-06-27 00:57:29 +03:00
|
|
|
text = HexaPDF::Layout::TextFragment.create(char, font: pdf.fonts.add(FONT_NAME),
|
2023-08-09 09:33:14 +03:00
|
|
|
font_size:)
|
2023-06-27 00:57:29 +03:00
|
|
|
|
|
|
|
|
cell_layouter.fit([text], cell_width, area['h'] * height)
|
|
|
|
|
.draw(canvas, ((area['x'] * width) + (cell_width * index)),
|
|
|
|
|
height - (area['y'] * height))
|
|
|
|
|
end
|
2023-05-28 23:20:48 +03:00
|
|
|
else
|
2023-12-17 23:32:05 +02:00
|
|
|
if field['type'] == 'date'
|
|
|
|
|
value = TimeUtils.format_date_string(value, field.dig('preferences', 'format'), account.locale)
|
|
|
|
|
end
|
2023-06-18 16:30:35 +03:00
|
|
|
|
2024-05-12 11:12:07 +03:00
|
|
|
value = NumberUtils.format_number(value, field.dig('preferences', 'format')) if field['type'] == 'number'
|
|
|
|
|
|
2024-01-15 01:45:26 +02:00
|
|
|
value = TextUtils.maybe_rtl_reverse(Array.wrap(value).join(', '))
|
2024-01-07 14:19:21 +02:00
|
|
|
|
|
|
|
|
text = HexaPDF::Layout::TextFragment.create(value, font: pdf.fonts.add(FONT_NAME),
|
|
|
|
|
font_size:)
|
2023-06-18 16:30:35 +03:00
|
|
|
|
|
|
|
|
lines = layouter.fit([text], area['w'] * width, height).lines
|
|
|
|
|
box_height = lines.sum(&:height)
|
2023-11-07 11:36:11 +02:00
|
|
|
|
2024-06-20 16:14:18 +03:00
|
|
|
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
2024-01-07 14:19:21 +02:00
|
|
|
text = HexaPDF::Layout::TextFragment.create(value,
|
2023-11-07 11:36:11 +02:00
|
|
|
font: pdf.fonts.add(FONT_NAME),
|
|
|
|
|
font_size: (font_size / 1.4).to_i)
|
|
|
|
|
|
2024-03-17 19:06:00 +02:00
|
|
|
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
2023-11-07 11:36:11 +02:00
|
|
|
|
|
|
|
|
box_height = lines.sum(&:height)
|
|
|
|
|
end
|
|
|
|
|
|
2024-06-20 16:14:18 +03:00
|
|
|
if preferences_font_size.blank? && box_height > (area['h'] * height) + 1
|
2024-03-20 16:52:47 +02:00
|
|
|
text = HexaPDF::Layout::TextFragment.create(value,
|
|
|
|
|
font: pdf.fonts.add(FONT_NAME),
|
|
|
|
|
font_size: (font_size / 1.9).to_i)
|
|
|
|
|
|
|
|
|
|
lines = layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width, height).lines
|
|
|
|
|
|
|
|
|
|
box_height = lines.sum(&:height)
|
|
|
|
|
end
|
|
|
|
|
|
2023-06-18 16:30:35 +03:00
|
|
|
height_diff = [0, box_height - (area['h'] * height)].max
|
|
|
|
|
|
2024-03-17 19:06:00 +02:00
|
|
|
right_align_x_adjustment =
|
|
|
|
|
if field['type'].in?(%w[date number]) && text_align != :left
|
|
|
|
|
(width - (area['w'] * width)) / (text_align == :center ? 2.0 : 1)
|
|
|
|
|
else
|
|
|
|
|
0
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
layouter.fit([text], field['type'].in?(%w[date number]) ? width : area['w'] * width,
|
|
|
|
|
height_diff.positive? ? box_height : area['h'] * height)
|
|
|
|
|
.draw(canvas, (area['x'] * width) - right_align_x_adjustment + TEXT_LEFT_MARGIN,
|
2023-06-24 22:50:12 +03:00
|
|
|
height - (area['y'] * height) + height_diff - TEXT_TOP_MARGIN)
|
2023-05-21 17:59:36 +03:00
|
|
|
end
|
2023-05-28 23:20:48 +03:00
|
|
|
end
|
|
|
|
|
end
|
2023-05-21 17:59:36 +03:00
|
|
|
|
2024-05-25 20:57:36 +03:00
|
|
|
pdfs_index
|
2023-05-21 17:59:36 +03:00
|
|
|
end
|
2023-06-13 00:29:07 +03:00
|
|
|
|
2024-03-15 13:03:10 +02:00
|
|
|
def build_pdf_attachment(pdf:, submitter:, pkcs:, tsa_url:, uuid:, name:)
|
2023-06-22 01:31:39 +03:00
|
|
|
io = StringIO.new
|
|
|
|
|
|
2023-10-11 21:05:42 +03:00
|
|
|
pdf.trailer.info[:Creator] = info_creator
|
2023-06-24 12:27:34 +03:00
|
|
|
|
2023-12-21 23:29:06 +02:00
|
|
|
sign_reason = fetch_sign_reason(submitter)
|
2023-12-02 18:14:33 +02:00
|
|
|
|
2023-12-21 23:29:06 +02:00
|
|
|
if sign_reason
|
|
|
|
|
sign_params = {
|
|
|
|
|
reason: sign_reason,
|
2024-05-01 23:40:09 +03:00
|
|
|
**build_signing_params(pkcs, tsa_url)
|
2023-12-21 23:29:06 +02:00
|
|
|
}
|
|
|
|
|
|
2024-03-15 12:13:32 +02:00
|
|
|
begin
|
|
|
|
|
pdf.sign(io, write_options: { validate: false }, **sign_params)
|
|
|
|
|
rescue HexaPDF::MalformedPDFError => e
|
|
|
|
|
Rollbar.error(e) if defined?(Rollbar)
|
|
|
|
|
|
|
|
|
|
pdf.sign(io, write_options: { validate: false, incremental: false }, **sign_params)
|
|
|
|
|
end
|
2023-12-21 23:29:06 +02:00
|
|
|
else
|
2024-03-15 12:13:32 +02:00
|
|
|
begin
|
|
|
|
|
pdf.write(io, incremental: true, validate: false)
|
|
|
|
|
rescue HexaPDF::MalformedPDFError => e
|
|
|
|
|
Rollbar.error(e) if defined?(Rollbar)
|
|
|
|
|
|
|
|
|
|
pdf.write(io, incremental: false, validate: false)
|
|
|
|
|
end
|
2023-12-21 23:29:06 +02:00
|
|
|
end
|
2024-03-15 12:13:32 +02:00
|
|
|
# rubocop:enable Metrics
|
2023-06-22 01:31:39 +03:00
|
|
|
|
2024-03-15 13:03:10 +02:00
|
|
|
ActiveStorage::Attachment.new(
|
2023-12-21 23:29:06 +02:00
|
|
|
blob: ActiveStorage::Blob.create_and_upload!(io: StringIO.new(io.string), filename: "#{name}.pdf"),
|
2024-02-25 16:25:04 +02:00
|
|
|
metadata: { original_uuid: uuid,
|
|
|
|
|
analyzed: true,
|
|
|
|
|
sha256: Base64.urlsafe_encode64(Digest::SHA256.digest(io.string)) },
|
2023-06-22 01:31:39 +03:00
|
|
|
name: 'documents',
|
|
|
|
|
record: submitter
|
|
|
|
|
)
|
|
|
|
|
end
|
|
|
|
|
|
2024-05-01 23:40:09 +03:00
|
|
|
def build_signing_params(pkcs, tsa_url)
|
|
|
|
|
params = {
|
|
|
|
|
certificate: pkcs.certificate,
|
|
|
|
|
key: pkcs.key,
|
|
|
|
|
certificate_chain: pkcs.ca_certs || []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tsa_url
|
|
|
|
|
params[:timestamp_handler] = Submissions::TimestampHandler.new(tsa_url:)
|
2024-05-08 09:38:16 +03:00
|
|
|
params[:signature_size] = 20_000
|
2024-05-01 23:40:09 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
params
|
|
|
|
|
end
|
|
|
|
|
|
2023-06-22 01:31:39 +03:00
|
|
|
def images_pdf_uuid(attachments)
|
|
|
|
|
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
|
|
|
|
|
end
|
|
|
|
|
|
2024-04-22 21:34:55 +03:00
|
|
|
def build_pdfs_index(submitter, flatten: true)
|
2024-02-25 16:25:04 +02:00
|
|
|
latest_submitter = find_last_submitter(submitter)
|
2023-07-01 13:16:07 +03:00
|
|
|
|
|
|
|
|
Submissions::EnsureResultGenerated.call(latest_submitter) if latest_submitter
|
2023-06-13 00:29:07 +03:00
|
|
|
|
2023-06-22 01:31:39 +03:00
|
|
|
documents = latest_submitter&.documents&.preload(:blob).to_a.presence
|
2023-12-17 23:32:05 +02:00
|
|
|
documents ||= submitter.submission.template_schema_documents.preload(:blob)
|
2023-06-13 00:29:07 +03:00
|
|
|
|
|
|
|
|
documents.to_h do |attachment|
|
2023-06-22 01:31:39 +03:00
|
|
|
pdf =
|
|
|
|
|
if attachment.image?
|
|
|
|
|
build_pdf_from_image(attachment)
|
|
|
|
|
else
|
|
|
|
|
HexaPDF::Document.new(io: StringIO.new(attachment.download))
|
|
|
|
|
end
|
|
|
|
|
|
2024-04-22 19:10:58 +03:00
|
|
|
pdf = maybe_rotate_pdf(pdf)
|
|
|
|
|
|
2024-04-22 21:34:55 +03:00
|
|
|
if flatten
|
|
|
|
|
begin
|
2024-05-27 00:39:36 +03:00
|
|
|
pdf.acro_form.create_appearances(force: true) if pdf.acro_form && pdf.acro_form[:NeedAppearances]
|
2024-04-22 21:34:55 +03:00
|
|
|
pdf.acro_form&.flatten
|
|
|
|
|
rescue StandardError => e
|
|
|
|
|
Rollbar.error(e) if defined?(Rollbar)
|
|
|
|
|
end
|
2024-02-08 17:59:40 +02:00
|
|
|
end
|
|
|
|
|
|
2024-03-15 19:02:17 +02:00
|
|
|
pdf.config['font.on_missing_glyph'] = method(:on_missing_glyph).to_proc
|
|
|
|
|
|
2024-02-25 16:25:04 +02:00
|
|
|
[attachment.metadata['original_uuid'] || attachment.uuid, pdf]
|
2023-06-13 00:29:07 +03:00
|
|
|
end
|
|
|
|
|
end
|
2023-06-22 01:31:39 +03:00
|
|
|
|
2024-04-22 19:10:58 +03:00
|
|
|
def maybe_rotate_pdf(pdf)
|
|
|
|
|
return pdf if pdf.pages.size > MAX_PAGE_ROTATE
|
|
|
|
|
|
|
|
|
|
is_rotated = pdf.pages.filter_map do |page|
|
|
|
|
|
page.rotate(0, flatten: true) if page[:Rotate] != 0
|
|
|
|
|
end.present?
|
|
|
|
|
|
|
|
|
|
return pdf unless is_rotated
|
|
|
|
|
|
|
|
|
|
io = StringIO.new
|
|
|
|
|
|
|
|
|
|
pdf.write(io, incremental: false, validate: false)
|
|
|
|
|
|
|
|
|
|
HexaPDF::Document.new(io:)
|
|
|
|
|
rescue StandardError => e
|
|
|
|
|
Rollbar.error(e) if defined?(Rollbar)
|
|
|
|
|
|
|
|
|
|
pdf
|
|
|
|
|
end
|
|
|
|
|
|
2024-03-15 19:02:17 +02:00
|
|
|
def on_missing_glyph(character, font_wrapper)
|
|
|
|
|
Rollbar.error("Missing glyph: #{character}") if character.present? && defined?(Rollbar)
|
|
|
|
|
|
|
|
|
|
replace_with =
|
|
|
|
|
if font_wrapper.font_type == :Type1
|
|
|
|
|
MISSING_GLYPH_REPLACE_TYPE1[character] || :space
|
|
|
|
|
else
|
|
|
|
|
(MISSING_GLYPH_REPLACE[character] || ' ').bytes.first - 29
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
font_wrapper.custom_glyph(replace_with, character)
|
|
|
|
|
end
|
|
|
|
|
|
2024-02-25 16:25:04 +02:00
|
|
|
def find_last_submitter(submitter)
|
|
|
|
|
submitter.submission.submitters
|
|
|
|
|
.select(&:completed_at?)
|
|
|
|
|
.select { |e| e.id != submitter.id && e.completed_at <= submitter.completed_at }
|
|
|
|
|
.max_by(&:completed_at)
|
|
|
|
|
end
|
|
|
|
|
|
2023-06-22 01:31:39 +03:00
|
|
|
def build_pdf_from_image(attachment)
|
|
|
|
|
pdf = HexaPDF::Document.new
|
2024-05-02 13:40:01 +03:00
|
|
|
|
2023-06-22 01:31:39 +03:00
|
|
|
page = pdf.pages.add
|
|
|
|
|
|
|
|
|
|
scale = [A4_SIZE.first / attachment.metadata['width'].to_f,
|
|
|
|
|
A4_SIZE.last / attachment.metadata['height'].to_f].min
|
|
|
|
|
|
|
|
|
|
page.box.width = attachment.metadata['width'] * scale
|
|
|
|
|
page.box.height = attachment.metadata['height'] * scale
|
|
|
|
|
|
|
|
|
|
page.canvas.image(
|
2023-07-07 00:54:32 +03:00
|
|
|
StringIO.new(attachment.preview_images.first.download),
|
2023-06-22 01:31:39 +03:00
|
|
|
at: [0, 0],
|
|
|
|
|
width: page.box.width,
|
|
|
|
|
height: page.box.height
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
pdf
|
|
|
|
|
end
|
2023-06-29 02:04:17 +03:00
|
|
|
|
2023-12-21 23:29:06 +02:00
|
|
|
def sign_reason(name)
|
|
|
|
|
format(SIGN_REASON, name:)
|
|
|
|
|
end
|
|
|
|
|
|
2024-05-16 23:18:48 +03:00
|
|
|
def single_sign_reason(submitter)
|
|
|
|
|
signers = submitter.submission.submitters.sort_by(&:completed_at).map { |s| s.email || s.name || s.phone }
|
|
|
|
|
|
|
|
|
|
format(SIGN_REASON, name: signers.reverse.join(', '))
|
2023-12-21 23:29:06 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def fetch_sign_reason(submitter)
|
|
|
|
|
reason_name = submitter.email || submitter.name || submitter.phone
|
|
|
|
|
|
2023-12-25 13:16:52 +02:00
|
|
|
config =
|
|
|
|
|
if Docuseal.multitenant?
|
|
|
|
|
AccountConfig.where(account: submitter.account, key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
2024-05-16 23:18:48 +03:00
|
|
|
.first_or_initialize(value: 'single')
|
2023-12-25 13:16:52 +02:00
|
|
|
else
|
|
|
|
|
AccountConfig.where(key: AccountConfig::ESIGNING_PREFERENCE_KEY)
|
2024-05-16 23:18:48 +03:00
|
|
|
.first_or_initialize(value: 'single')
|
2023-12-25 13:16:52 +02:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
return sign_reason(reason_name) if config.value == 'multiple'
|
2023-12-21 23:29:06 +02:00
|
|
|
|
2024-05-16 23:18:48 +03:00
|
|
|
if !submitter.submission.submitters.exists?(completed_at: nil) &&
|
|
|
|
|
submitter.completed_at == submitter.submission.submitters.maximum(:completed_at)
|
|
|
|
|
return single_sign_reason(submitter)
|
|
|
|
|
end
|
2023-12-21 23:29:06 +02:00
|
|
|
|
|
|
|
|
nil
|
2023-10-11 21:05:42 +03:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
def info_creator
|
2023-12-30 01:19:46 +02:00
|
|
|
"#{Docuseal.product_name} (#{Docuseal::PRODUCT_URL})"
|
2023-10-11 21:05:42 +03:00
|
|
|
end
|
|
|
|
|
|
2023-07-04 01:58:43 +03:00
|
|
|
def h
|
|
|
|
|
Rails.application.routes.url_helpers
|
|
|
|
|
end
|
2023-05-21 17:59:36 +03:00
|
|
|
end
|
|
|
|
|
end
|