make pdf links clickable

pull/105/head
Alex Turchyn 2 years ago
parent c4daf8ed49
commit 52ef0ae929

@ -20,6 +20,7 @@ gem 'oj'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection' gem 'omniauth-rails_csrf_protection'
gem 'pagy' gem 'pagy'
gem 'pdf-reader'
gem 'pg', require: false gem 'pg', require: false
gem 'premailer-rails' gem 'premailer-rails'
gem 'puma' gem 'puma'

@ -1,6 +1,7 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
Ascii85 (1.1.0)
actioncable (7.0.7) actioncable (7.0.7)
actionpack (= 7.0.7) actionpack (= 7.0.7)
activesupport (= 7.0.7) activesupport (= 7.0.7)
@ -68,6 +69,7 @@ GEM
tzinfo (~> 2.0) tzinfo (~> 2.0)
addressable (2.8.5) addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0) public_suffix (>= 2.0.2, < 6.0)
afm (0.2.2)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
@ -233,6 +235,7 @@ GEM
os (>= 0.9, < 2.0) os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a) signet (>= 0.16, < 2.a)
hashdiff (1.0.1) hashdiff (1.0.1)
hashery (2.1.2)
hashie (5.0.0) hashie (5.0.0)
hexapdf (0.33.0) hexapdf (0.33.0)
cmdparse (~> 3.0, >= 3.0.3) cmdparse (~> 3.0, >= 3.0.3)
@ -335,6 +338,12 @@ GEM
parser (3.2.2.3) parser (3.2.2.3)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
pdf-reader (2.11.0)
Ascii85 (~> 1.0)
afm (~> 0.2.1)
hashery (~> 2.0)
ruby-rc4
ttfunk
pg (1.5.3) pg (1.5.3)
premailer (1.21.0) premailer (1.21.0)
addressable addressable
@ -461,6 +470,7 @@ GEM
rubocop-capybara (~> 2.17) rubocop-capybara (~> 2.17)
rubocop-factory_bot (~> 2.22) rubocop-factory_bot (~> 2.22)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby-vips (2.1.4) ruby-vips (2.1.4)
ffi (~> 1.12) ffi (~> 1.12)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
@ -502,6 +512,7 @@ GEM
thor (1.2.2) thor (1.2.2)
timeout (0.4.0) timeout (0.4.0)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
ttfunk (1.7.0)
turbo-rails (1.4.0) turbo-rails (1.4.0)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activejob (>= 6.0.0) activejob (>= 6.0.0)
@ -564,6 +575,7 @@ DEPENDENCIES
omniauth-google-oauth2 omniauth-google-oauth2
omniauth-rails_csrf_protection omniauth-rails_csrf_protection
pagy pagy
pdf-reader
pg pg
premailer-rails premailer-rails
pry-rails pry-rails

@ -7,12 +7,7 @@ module Api
@template = current_account.templates.find(params[:template_id]) @template = current_account.templates.find(params[:template_id])
documents = documents = Templates::CreateAttachments.call(@template, params)
find_or_create_blobs.map do |blob|
document = @template.documents.create!(blob:)
Templates::ProcessDocument.call(document)
end
schema = documents.map do |doc| schema = documents.map do |doc|
{ attachment_uuid: doc.uuid, name: doc.filename.base } { attachment_uuid: doc.uuid, name: doc.filename.base }
@ -27,19 +22,5 @@ module Api
) )
} }
end end
private
def find_or_create_blobs
blobs = params[:blobs]&.map do |attrs|
ActiveStorage::Blob.find_signed(attrs[:signed_id])
end
blobs || params[:files].map do |file|
ActiveStorage::Blob.create_and_upload!(io: file.open,
filename: file.original_filename,
content_type: file.content_type)
end
end
end end
end end

@ -18,7 +18,7 @@ class EsignSettingsController < ApplicationController
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present? default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
custom_pkcs_list = (cert_data['custom'] || []).map do |e| custom_pkcs_list = (cert_data['custom'] || []).map do |e|
{ 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password']), { 'pkcs' => OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s),
'name' => e['name'], 'name' => e['name'],
'status' => e['status'] } 'status' => e['status'] }
end end

@ -6,7 +6,7 @@ class SubmissionsController < ApplicationController
def show def show
@submission = @submission =
Submission.joins(:template).where(template: { account_id: current_account.id }) Submission.joins(:template).where(template: { account_id: current_account.id })
.preload(template: { documents_attachments: { preview_images_attachments: :blob } }) .preload(:template, template_schema_documents: [:blob, { preview_images_attachments: :blob }])
.find(params[:id]) .find(params[:id])
render :show, layout: 'plain' render :show, layout: 'plain'

@ -7,7 +7,9 @@ class SubmitFormController < ApplicationController
def show def show
@submitter = @submitter =
Submitter.preload(submission: { template: { documents_attachments: { preview_images_attachments: :blob } } }) Submitter.preload(submission: [
:template, { template_schema_documents: [:blob, { preview_images_attachments: :blob }] }
])
.find_by!(slug: params[:slug]) .find_by!(slug: params[:slug])
return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at? return redirect_to submit_form_completed_path(@submitter.slug) if @submitter.completed_at?

@ -10,13 +10,13 @@ class VerifyPdfSignatureController < ApplicationController
cert_data = if Docuseal.multitenant? cert_data = if Docuseal.multitenant?
Docuseal::CERTS Docuseal::CERTS
else else
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {} EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
end end
default_pkcs = GenerateCertificate.load_pkcs(cert_data) default_pkcs = GenerateCertificate.load_pkcs(cert_data)
custom_certs = (cert_data['custom'] || []).map do |e| custom_certs = cert_data.fetch('custom', []).map do |e|
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password']) OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
end end
trusted_certs = [default_pkcs.certificate, trusted_certs = [default_pkcs.certificate,

@ -38,6 +38,10 @@ class Submission < ApplicationRecord
attribute :source, :string, default: 'link' attribute :source, :string, default: 'link'
has_many :template_schema_documents,
->(e) { where(uuid: (e.template_schema.presence || e.template.schema).pluck('attachment_uuid')) },
through: :template, source: :documents_attachments
scope :active, -> { where(deleted_at: nil) } scope :active, -> { where(deleted_at: nil) }
enum :source, { enum :source, {

@ -0,0 +1 @@
<a href="<%= annot['value'] %>" rel="noopener noreferrer nofollow" target="_blank" class="flex absolute" style="width: <%= annot['w'] * 100 %>%; height: <%= annot['h'] * 100 %>%; left: <%= annot['x'] * 100 %>%; top: <%= annot['y'] * 100 %>%"></a>

@ -24,7 +24,7 @@
<div class="flex md:max-h-[calc(100vh-60px)]"> <div class="flex md:max-h-[calc(100vh-60px)]">
<div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5"> <div class="overflow-y-auto overflow-x-hidden hidden lg:block w-52 flex-none pr-3 mt-0.5 pt-0.5">
<% (@submission.template_schema || @submission.template.schema).each do |item| %> <% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template.documents.find { |a| item['attachment_uuid'] == a.uuid } %> <% document = @submission.template_schema_documents.find { |a| item['attachment_uuid'] == a.uuid } %>
<a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer"> <a href="#<%= "page-#{document.uuid}-0" %>" onclick="[event.preventDefault(), window[event.target.closest('a').href.split('#')[1]].scrollIntoView({ behavior: 'smooth', block: 'start' })]" class="block cursor-pointer">
<img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy"> <img src="<%= document.preview_images.first.url %>" width="<%= document.preview_images.first.metadata['width'] %>" height="<%= document.preview_images.first.metadata['height'] %>" class="rounded border" loading="lazy">
<div class="pb-2 pt-1.5 text-center"> <div class="pb-2 pt-1.5 text-center">
@ -39,11 +39,15 @@
<% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %> <% values = @submission.submitters.reduce({}) { |acc, sub| acc.merge(sub.values) } %>
<% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %> <% attachments_index = ActiveStorage::Attachment.where(record: @submission.submitters, name: :attachments).preload(:blob).index_by(&:uuid) %>
<% (@submission.template_schema || @submission.template.schema).each do |item| %> <% (@submission.template_schema || @submission.template.schema).each do |item| %>
<% document = @submission.template.documents.find { |e| e.uuid == item['attachment_uuid'] } %> <% document = @submission.template_schema_documents.find { |e| e.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %> <% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative"> <div id="<%= "page-#{document.uuid}-#{index}" %>" class="relative">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>" loading="lazy"> <img src="<%= page.url %>" width="<%= page.metadata['width'] %>" class="shadow-md mb-4" height="<%= page.metadata['height'] %>" loading="lazy">
<div class="top-0 bottom-0 left-0 right-0 absolute"> <div class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %>
<% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% next if value.blank? %> <% next if value.blank? %>

@ -8,11 +8,15 @@
<%= render 'banner' %> <%= render 'banner' %>
</div> </div>
<% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %> <% (@submitter.submission.template_schema || @submitter.submission.template.schema).each do |item| %>
<% document = @submitter.submission.template.documents.find { |a| a.uuid == item['attachment_uuid'] } %> <% document = @submitter.submission.template_schema_documents.find { |a| a.uuid == item['attachment_uuid'] } %>
<% document_annots_index = document.metadata.dig('pdf', 'annotations')&.group_by { |e| e['page'] } || {} %>
<% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %> <% document.preview_images.sort_by { |a| a.filename.base.to_i }.each_with_index do |page, index| %>
<div class="relative my-4 shadow-md"> <div class="relative my-4 shadow-md">
<img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy"> <img src="<%= page.url %>" width="<%= page.metadata['width'] %>" height="<%= page.metadata['height'] %>" loading="lazy">
<div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute"> <div id="page-<%= [document.uuid, index].join('-') %>" class="top-0 bottom-0 left-0 right-0 absolute">
<% document_annots_index[index]&.each do |annot| %>
<%= render 'submissions/annotation', annot: %>
<% end %>
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %> <% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
<% value = values[field['uuid']] %> <% value = values[field['uuid']] %>
<% next if value.blank? %> <% next if value.blank? %>

@ -54,7 +54,7 @@
</div> </div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('certificate', class: 'w-5 h-5 inline') %> <%= svg_icon('certificate', class: 'w-5 h-5 inline') %>
<span><%= signature.signer_name %></span> <span><%= signature.signer_name.force_encoding('UTF-8') %></span>
</div> </div>
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<%= svg_icon('lock_access', class: 'w-5 h-5 inline') %> <%= svg_icon('lock_access', class: 'w-5 h-5 inline') %>

@ -51,7 +51,7 @@ module Accounts
end end
if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' }) if (default_cert = cert_data['custom']&.find { |e| e['status'] == 'default' })
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(default_cert['data']), default_cert['password']) OpenSSL::PKCS12.new(Base64.urlsafe_decode64(default_cert['data']), default_cert['password'].to_s)
else else
GenerateCertificate.load_pkcs(cert_data) GenerateCertificate.load_pkcs(cert_data)
end end

@ -0,0 +1,35 @@
# frozen_string_literal: true
module Templates
module BuildAnnotations
module_function
def call(data)
pdf = PDF::Reader.new(StringIO.new(data))
pdf.pages.flat_map.with_index do |page, index|
annotations = page.objects.deref!(page.attributes[:Annots]) || []
annotations.filter_map do |annot|
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
end
def build_external_link_hash(page, annot)
left, bottom, right, top = annot[:Rect]
{
'type' => 'external_link',
'value' => annot[:A][:URI],
'x' => left / page.width,
'y' => (page.height - top) / page.height,
'w' => (right - left) / page.width,
'h' => (top - bottom) / page.height
}
end
end
end

@ -0,0 +1,47 @@
# frozen_string_literal: true
module Templates
module CreateAttachments
PDF_CONTENT_TYPE = 'application/pdf'
module_function
def call(template, params)
find_or_create_blobs(params).map do |blob|
document = template.documents.create!(blob:)
document_data = blob.download
if blob.content_type == PDF_CONTENT_TYPE && blob.metadata['pdf'].nil?
blob.metadata['pdf'] = { 'annotations' => Templates::BuildAnnotations.call(document_data) }
end
blob.save!
Templates::ProcessDocument.call(document, document_data)
end
end
def find_or_create_blobs(params)
blobs = params[:blobs]&.map do |attrs|
ActiveStorage::Blob.find_signed(attrs[:signed_id])
end
blobs || params[:files].map do |file|
data = file.read
if file.content_type == PDF_CONTENT_TYPE
metadata = { 'identified' => true, 'analyzed' => true,
'pdf' => { 'annotations' => Templates::BuildAnnotations.call(data) } }
end
ActiveStorage::Blob.create_and_upload!(
io: StringIO.new(data),
filename: file.original_filename,
metadata:,
content_type: file.content_type
)
end
end
end
end

@ -12,22 +12,20 @@ module Templates
module_function module_function
def call(attachment) def call(attachment, data)
if attachment.content_type == PDF_CONTENT_TYPE if attachment.content_type == PDF_CONTENT_TYPE
generate_pdf_preview_images(attachment) generate_pdf_preview_images(attachment, data)
elsif attachment.image? elsif attachment.image?
generate_preview_image(attachment) generate_preview_image(attachment, data)
end end
attachment attachment
end end
def generate_preview_image(attachment) def generate_preview_image(attachment, data)
binary = attachment.download
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
image = Vips::Image.new_from_buffer(binary, '') image = Vips::Image.new_from_buffer(data, '')
image = image.autorot.resize(MAX_WIDTH / image.width.to_f) image = image.autorot.resize(MAX_WIDTH / image.width.to_f)
io = StringIO.new(image.write_to_buffer(FORMAT, Q: Q, interlace: true)) io = StringIO.new(image.write_to_buffer(FORMAT, Q: Q, interlace: true))
@ -42,14 +40,12 @@ module Templates
) )
end end
def generate_pdf_preview_images(attachment) def generate_pdf_preview_images(attachment, data)
binary = attachment.download
ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all ActiveStorage::Attachment.where(name: ATTACHMENT_NAME, record: attachment).destroy_all
number_of_pages = HexaPDF::Document.new(io: StringIO.new(binary)).pages.size - 1 number_of_pages = PDF::Reader.new(StringIO.new(data)).pages.size - 1
(0..number_of_pages).each do |page_number| (0..number_of_pages).each do |page_number|
page = Vips::Image.new_from_buffer(binary, '', dpi: DPI, page: page_number) page = Vips::Image.new_from_buffer(data, '', dpi: DPI, page: page_number)
page = page.resize(MAX_WIDTH / page.width.to_f) page = page.resize(MAX_WIDTH / page.width.to_f)
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true)) io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))

@ -19,7 +19,7 @@ FactoryBot.define do
record: template record: template
) )
Templates::ProcessDocument.call(attachment) Templates::ProcessDocument.call(attachment, attachment.download)
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }] template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
template.submitters = [ template.submitters = [

Loading…
Cancel
Save