mirror of
https://github.com/docusealco/docuseal.git
synced 2026-08-07 07:14:43 +00:00
make pdf links clickable
This commit is contained in:
@@ -20,6 +20,7 @@ gem 'oj'
|
||||
gem 'omniauth-google-oauth2'
|
||||
gem 'omniauth-rails_csrf_protection'
|
||||
gem 'pagy'
|
||||
gem 'pdf-reader'
|
||||
gem 'pg', require: false
|
||||
gem 'premailer-rails'
|
||||
gem 'puma'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
Ascii85 (1.1.0)
|
||||
actioncable (7.0.7)
|
||||
actionpack (= 7.0.7)
|
||||
activesupport (= 7.0.7)
|
||||
@@ -68,6 +69,7 @@ GEM
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.5)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
afm (0.2.2)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
@@ -233,6 +235,7 @@ GEM
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
hashdiff (1.0.1)
|
||||
hashery (2.1.2)
|
||||
hashie (5.0.0)
|
||||
hexapdf (0.33.0)
|
||||
cmdparse (~> 3.0, >= 3.0.3)
|
||||
@@ -335,6 +338,12 @@ GEM
|
||||
parser (3.2.2.3)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
pdf-reader (2.11.0)
|
||||
Ascii85 (~> 1.0)
|
||||
afm (~> 0.2.1)
|
||||
hashery (~> 2.0)
|
||||
ruby-rc4
|
||||
ttfunk
|
||||
pg (1.5.3)
|
||||
premailer (1.21.0)
|
||||
addressable
|
||||
@@ -461,6 +470,7 @@ GEM
|
||||
rubocop-capybara (~> 2.17)
|
||||
rubocop-factory_bot (~> 2.22)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-rc4 (0.1.5)
|
||||
ruby-vips (2.1.4)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
@@ -502,6 +512,7 @@ GEM
|
||||
thor (1.2.2)
|
||||
timeout (0.4.0)
|
||||
trailblazer-option (0.1.2)
|
||||
ttfunk (1.7.0)
|
||||
turbo-rails (1.4.0)
|
||||
actionpack (>= 6.0.0)
|
||||
activejob (>= 6.0.0)
|
||||
@@ -564,6 +575,7 @@ DEPENDENCIES
|
||||
omniauth-google-oauth2
|
||||
omniauth-rails_csrf_protection
|
||||
pagy
|
||||
pdf-reader
|
||||
pg
|
||||
premailer-rails
|
||||
pry-rails
|
||||
|
||||
@@ -7,12 +7,7 @@ module Api
|
||||
|
||||
@template = current_account.templates.find(params[:template_id])
|
||||
|
||||
documents =
|
||||
find_or_create_blobs.map do |blob|
|
||||
document = @template.documents.create!(blob:)
|
||||
|
||||
Templates::ProcessDocument.call(document)
|
||||
end
|
||||
documents = Templates::CreateAttachments.call(@template, params)
|
||||
|
||||
schema = documents.map do |doc|
|
||||
{ attachment_uuid: doc.uuid, name: doc.filename.base }
|
||||
@@ -27,19 +22,5 @@ module Api
|
||||
)
|
||||
}
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@ class EsignSettingsController < ApplicationController
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data) if cert_data['cert'].present?
|
||||
|
||||
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'],
|
||||
'status' => e['status'] }
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class SubmissionsController < ApplicationController
|
||||
def show
|
||||
@submission =
|
||||
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])
|
||||
|
||||
render :show, layout: 'plain'
|
||||
|
||||
@@ -7,7 +7,9 @@ class SubmitFormController < ApplicationController
|
||||
|
||||
def show
|
||||
@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])
|
||||
|
||||
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?
|
||||
Docuseal::CERTS
|
||||
else
|
||||
EncryptedConfig.find_by(account:, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||
EncryptedConfig.find_by(account: current_account, key: EncryptedConfig::ESIGN_CERTS_KEY)&.value || {}
|
||||
end
|
||||
|
||||
default_pkcs = GenerateCertificate.load_pkcs(cert_data)
|
||||
|
||||
custom_certs = (cert_data['custom'] || []).map do |e|
|
||||
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'])
|
||||
custom_certs = cert_data.fetch('custom', []).map do |e|
|
||||
OpenSSL::PKCS12.new(Base64.urlsafe_decode64(e['data']), e['password'].to_s)
|
||||
end
|
||||
|
||||
trusted_certs = [default_pkcs.certificate,
|
||||
|
||||
@@ -38,6 +38,10 @@ class Submission < ApplicationRecord
|
||||
|
||||
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) }
|
||||
|
||||
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="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| %>
|
||||
<% 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">
|
||||
<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">
|
||||
@@ -39,11 +39,15 @@
|
||||
<% 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) %>
|
||||
<% (@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| %>
|
||||
<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">
|
||||
<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)| %>
|
||||
<% value = values[field['uuid']] %>
|
||||
<% next if value.blank? %>
|
||||
|
||||
@@ -8,11 +8,15 @@
|
||||
<%= render 'banner' %>
|
||||
</div>
|
||||
<% (@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| %>
|
||||
<div class="relative my-4 shadow-md">
|
||||
<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">
|
||||
<% document_annots_index[index]&.each do |annot| %>
|
||||
<%= render 'submissions/annotation', annot: %>
|
||||
<% end %>
|
||||
<% fields_index.dig(document.uuid, index)&.each do |(area, field)| %>
|
||||
<% value = values[field['uuid']] %>
|
||||
<% next if value.blank? %>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-1">
|
||||
<%= 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 class="flex items-center space-x-1">
|
||||
<%= svg_icon('lock_access', class: 'w-5 h-5 inline') %>
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ module Accounts
|
||||
end
|
||||
|
||||
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
|
||||
GenerateCertificate.load_pkcs(cert_data)
|
||||
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
|
||||
|
||||
def call(attachment)
|
||||
def call(attachment, data)
|
||||
if attachment.content_type == PDF_CONTENT_TYPE
|
||||
generate_pdf_preview_images(attachment)
|
||||
generate_pdf_preview_images(attachment, data)
|
||||
elsif attachment.image?
|
||||
generate_preview_image(attachment)
|
||||
generate_preview_image(attachment, data)
|
||||
end
|
||||
|
||||
attachment
|
||||
end
|
||||
|
||||
def generate_preview_image(attachment)
|
||||
binary = attachment.download
|
||||
|
||||
def generate_preview_image(attachment, data)
|
||||
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)
|
||||
|
||||
io = StringIO.new(image.write_to_buffer(FORMAT, Q: Q, interlace: true))
|
||||
@@ -42,14 +40,12 @@ module Templates
|
||||
)
|
||||
end
|
||||
|
||||
def generate_pdf_preview_images(attachment)
|
||||
binary = attachment.download
|
||||
|
||||
def generate_pdf_preview_images(attachment, data)
|
||||
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|
|
||||
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)
|
||||
|
||||
io = StringIO.new(page.write_to_buffer(FORMAT, Q: Q, interlace: true))
|
||||
|
||||
@@ -19,7 +19,7 @@ FactoryBot.define do
|
||||
record: template
|
||||
)
|
||||
|
||||
Templates::ProcessDocument.call(attachment)
|
||||
Templates::ProcessDocument.call(attachment, attachment.download)
|
||||
|
||||
template.schema = [{ attachment_uuid: attachment.uuid, name: 'sample-document' }]
|
||||
template.submitters = [
|
||||
|
||||
Reference in New Issue
Block a user