Files
docuseal/lib/submissions/generate_result_attachments.rb
T

267 lines
9.5 KiB
Ruby
Raw Normal View History

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
2023-05-28 23:20:48 +03:00
FONT_NAME = 'Helvetica'
2023-06-26 00:46:01 +03:00
INFO_CREATOR = "#{Docuseal::PRODUCT_NAME} (#{Docuseal::PRODUCT_URL})".freeze
2023-07-07 21:29:38 +03:00
SIGN_REASON = 'Signed by %<email>s with DocuSeal.co'
2023-06-24 12:27:34 +03:00
2023-06-24 22:50:12 +03:00
TEXT_LEFT_MARGIN = 1
TEXT_TOP_MARGIN = 1
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
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)
2023-06-18 16:30:35 +03:00
layouter = HexaPDF::Layout::TextLayouter.new(valign: :center)
2023-06-27 00:57:29 +03:00
cell_layouter = HexaPDF::Layout::TextLayouter.new(valign: :center, align: :center)
2023-06-13 00:29:07 +03:00
template = submitter.submission.template
2023-06-30 20:31:17 +03:00
certs = Accounts.load_signing_certs(submitter.submission.template.account)
2023-05-28 23:20:48 +03:00
2023-06-13 00:29:07 +03:00
pdfs_index = build_pdfs_index(submitter)
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']]
page = pdf.pages[area['page']]
2023-08-12 12:34:02 +03:00
page[:Annots] ||= []
page[:Annots] = page[:Annots].reject { |e| e[:A] && e[:A][:URI].to_s.starts_with?('file:///docuseal_field') }
2023-05-28 23:20:48 +03:00
width = page.box.width
height = page.box.height
2023-08-09 09:33:14 +03:00
font_size = ((page.box.width / 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
2023-07-08 20:51:37 +03:00
next if Array.wrap(value).compact_blank.blank?
2023-05-28 23:20:48 +03:00
canvas = page.canvas(type: :overlay)
case field['type']
when 'image', 'signature'
2023-06-14 02:08:26 +03:00
attachment = submitter.attachments.find { |a| a.uuid == value }
2023-06-24 22:50:12 +03:00
2023-07-07 21:29:38 +03:00
image = Vips::Image.new_from_buffer(attachment.download, '').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-07-07 21:29:38 +03:00
io = StringIO.new(image.resize([scale * 4, 1].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-06-04 01:08:45 +03:00
when 'file'
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 }
next_index =
lines[(index + 1)..].index { |l| l.items.first.is_a?(HexaPDF::Layout::InlineBox) } || (lines.size - 1)
page[:Annots] << pdf.add(
{
Type: :Annot, Subtype: :Link,
Rect: [
2023-06-24 22:50:12 +03:00
(area['x'] * width) + TEXT_LEFT_MARGIN,
2023-06-18 16:30:35 +03:00
height - (area['y'] * height) - lines[...index].sum(&:height) + height_diff,
2023-06-24 22:50:12 +03:00
(area['x'] * width) + (area['w'] * width) + TEXT_LEFT_MARGIN,
2023-06-18 16:30:35 +03:00
height - (area['y'] * height) - lines[..next_index].sum(&:height) + height_diff
],
A: { Type: :Action, S: :URI,
URI: h.rails_blob_url(attachment, **Docuseal.default_url_options) }
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-06-18 16:30:35 +03:00
when 'checkbox'
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
)
2023-06-27 00:57:29 +03:00
when 'cells'
cell_width = area['cell_w'] * width
value.chars.each_with_index do |char, index|
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-06-30 01:26:13 +03:00
value = I18n.l(Date.parse(value), format: :long) if field['type'] == 'date'
2023-06-18 16:30:35 +03:00
text = HexaPDF::Layout::TextFragment.create(Array.wrap(value).join(', '), font: pdf.fonts.add(FONT_NAME),
2023-08-09 09:33:14 +03:00
font_size:)
2023-06-18 16:30:35 +03:00
lines = layouter.fit([text], area['w'] * width, height).lines
box_height = lines.sum(&:height)
height_diff = [0, box_height - (area['h'] * height)].max
layouter.fit([text], 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-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
2023-06-22 01:31:39 +03:00
image_pdfs = []
original_documents = template.documents.preload(:blob)
results =
2023-07-26 09:58:50 +03:00
submitter.submission.template_schema.map do |item|
2023-06-22 01:31:39 +03:00
pdf = pdfs_index[item['attachment_uuid']]
2023-06-30 20:31:17 +03:00
attachment = save_signed_pdf(pdf:, submitter:, certs:, uuid: item['attachment_uuid'], name: item['name'])
2023-05-21 17:59:36 +03:00
2023-06-22 01:31:39 +03:00
image_pdfs << pdf if original_documents.find { |a| a.uuid == item['attachment_uuid'] }.image?
2023-05-21 17:59:36 +03:00
2023-06-22 01:31:39 +03:00
attachment
end
return results if image_pdfs.size < 2
2023-05-28 23:20:48 +03:00
2023-06-22 01:31:39 +03:00
images_pdf =
image_pdfs.each_with_object(HexaPDF::Document.new) do |pdf, doc|
pdf.pages.each { |page| doc.pages << doc.import(page) }
end
2023-05-28 23:20:48 +03:00
2023-06-22 01:31:39 +03:00
images_pdf_result =
save_signed_pdf(
pdf: images_pdf,
submitter:,
2023-06-30 20:31:17 +03:00
certs:,
2023-06-22 01:31:39 +03:00
uuid: images_pdf_uuid(original_documents.select(&:image?)),
name: template.name
2023-06-13 00:29:07 +03:00
)
2023-06-22 01:31:39 +03:00
results + [images_pdf_result]
2023-05-21 17:59:36 +03:00
end
2023-05-28 23:20:48 +03:00
# rubocop:enable Metrics
2023-06-13 00:29:07 +03:00
2023-06-30 20:31:17 +03:00
def save_signed_pdf(pdf:, submitter:, certs:, uuid:, name:)
2023-06-22 01:31:39 +03:00
io = StringIO.new
2023-06-24 22:50:12 +03:00
pdf.trailer.info[:Creator] = INFO_CREATOR
2023-06-24 12:27:34 +03:00
2023-06-26 00:46:01 +03:00
pdf.sign(io, reason: format(SIGN_REASON, email: submitter.email),
2023-06-30 20:31:17 +03:00
certificate: certs[:cert],
key: certs[:key],
certificate_chain: [certs[:sub_ca], certs[:root_ca]])
2023-06-22 01:31:39 +03:00
ActiveStorage::Attachment.create!(
uuid:,
blob: ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(io.string), filename: "#{name}.pdf"
),
name: 'documents',
record: submitter
)
end
def images_pdf_uuid(attachments)
Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, attachments.map(&:uuid).sort.join(':'))
end
2023-06-13 00:29:07 +03:00
def build_pdfs_index(submitter)
2023-07-01 13:16:07 +03:00
latest_submitter =
submitter.submission.submitters
.select(&:completed_at?)
.select { |e| e.id != submitter.id && e.completed_at <= submitter.completed_at }
.max_by(&:completed_at)
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
documents ||= submitter.submission.template.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
[attachment.uuid, pdf]
2023-06-13 00:29:07 +03:00
end
end
2023-06-22 01:31:39 +03:00
def build_pdf_from_image(attachment)
pdf = HexaPDF::Document.new
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
def h
Rails.application.routes.url_helpers
end
2023-05-21 17:59:36 +03:00
end
end