You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
docuseal/lib/submissions/generate_voided_documents.rb

153 lines
4.5 KiB

# frozen_string_literal: true
module Submissions
module GenerateVoidedDocuments
WATERMARK_COLOR = [0.85, 0.10, 0.10].freeze
WATERMARK_STROKE_OPACITY = 0.85
WATERMARK_FONT_SIZE = 150
WATERMARK_STROKE_WIDTH = 2.2
FOOTER_FONT_SIZE = 10
FOOTER_HEIGHT = 36
module_function
def call(submission)
return [] unless submission.voided_at?
submission.with_lock do
source_documents = source_attachments(submission)
.select { |a| a.blob.content_type == 'application/pdf' }
.uniq { |a| a.blob.checksum }
existing = submission.voided_documents.attachments.reload
return existing.to_a if existing.size == source_documents.size && existing.any?
submission.voided_documents.purge if existing.any?
voided_by = submission.voided_by_user
reason = submission.void_reason
voided_at = submission.voided_at
source_documents.each do |source|
watermarked_io = stamp_pdf(source.download, voided_at:, voided_by:, reason:)
submission.voided_documents.attach(
io: watermarked_io,
filename: build_filename(source.filename),
content_type: 'application/pdf'
)
end
end
submission.voided_documents.attachments.reload.to_a
end
def source_attachments(submission)
if submission.documents.attached?
submission.documents.attachments
elsif submission.template
submission.template.documents_attachments.to_a
else
[]
end
end
def stamp_pdf(pdf_data, voided_at:, voided_by:, reason:)
doc = HexaPDF::Document.new(io: StringIO.new(pdf_data))
doc.pages.each do |page|
draw_watermark_on_page(doc, page)
draw_footer_on_page(doc, page, voided_at:, voided_by:, reason:)
end
out = StringIO.new
doc.write(out, validate: false)
out.tap(&:rewind)
end
def draw_watermark_on_page(doc, page)
box = page.box(:media)
width = box.width
height = box.height
font = doc.fonts.add('Helvetica', variant: :bold)
fragment = HexaPDF::Layout::TextFragment.create(
'VOIDED',
font:,
font_size: WATERMARK_FONT_SIZE,
stroke_color: WATERMARK_COLOR,
stroke_width: WATERMARK_STROKE_WIDTH,
text_rendering_mode: :stroke,
stroke_alpha: WATERMARK_STROKE_OPACITY
)
text_width = fragment.width
text_height = fragment.height
canvas = page.canvas(type: :overlay)
canvas.save_graphics_state do
center_x = width / 2.0
center_y = height / 2.0
angle_rad = Math::PI / 6
cos_a = Math.cos(angle_rad)
sin_a = Math.sin(angle_rad)
offset_x = -(text_width / 2.0) * cos_a + (text_height / 2.0) * sin_a
offset_y = -(text_width / 2.0) * sin_a - (text_height / 2.0) * cos_a
canvas.transform(cos_a, sin_a, -sin_a, cos_a, center_x + offset_x, center_y + offset_y)
fragment.draw(canvas, 0, 0)
end
end
def draw_footer_on_page(doc, page, voided_at:, voided_by:, reason:)
box = page.box(:media)
width = box.width
canvas = page.canvas(type: :overlay)
canvas.save_graphics_state do
canvas.fill_color(0.95, 0.20, 0.20)
canvas.rectangle(0, 0, width, FOOTER_HEIGHT).fill
end
font = doc.fonts.add('Helvetica', variant: :bold)
white = [1.0, 1.0, 1.0]
line1 = build_footer_line1(voided_at, voided_by)
line2 = build_footer_line2(reason)
[line1, line2].each_with_index do |text, idx|
next if text.blank?
fragment = HexaPDF::Layout::TextFragment.create(text, font:, font_size: FOOTER_FONT_SIZE,
fill_color: white)
canvas.save_graphics_state do
fragment.draw(canvas, 12, FOOTER_HEIGHT - 14 - (idx * 14))
end
end
end
def build_footer_line1(voided_at, voided_by)
timestamp = voided_at.utc.strftime('%Y-%m-%d %H:%M UTC')
voided_by_label = voided_by ? (voided_by.full_name.presence || voided_by.email) : 'an authorized user'
"VOIDED on #{timestamp} by #{voided_by_label}"
end
def build_footer_line2(reason)
return nil if reason.blank?
"Reason: #{reason.to_s.tr("\r\n", ' ').squeeze(' ').strip[0, 200]}"
end
def build_filename(original_filename)
base = original_filename.base.to_s
ext = original_filename.extension.presence || 'pdf'
"#{base}-voided.#{ext}"
end
end
end