Ruby on Rails : Add multiple signatures on a PDF - ruby-on-rails

I would like to digital sign several times (twice would be fine) a PDF using Ruby on Rails.
I have tried using Origami Gem which works great for a single signature (thank you MrWater for your very helpful post: to Insert digital signature into existing pdf file)
However, I can't sign twice the document. When I do, using the same method, my pdf file displays a signature error (signature invalid).
Do you have any idea of how to make that work using Origami or any other Ruby Gem?
Thank you in advance for your help.
Update 2015-10-25:
You will find below my code to sign a document (maybe it can help to find a solution to the problem, and at least it shows you how to make a single signature, which works quite fine). In comment is my failing attempt for a double signature. I also tried to sign a first time doing the whole process, and sign a second time with the same process but without any success:
PDF_ORI = "contrat_old.pdf"
PDF_NEW = "contrat_new.pdf"
pdf = Origami::PDF.read(PDF_ORI)
# Open certificate files
key = OpenSSL::PKey::RSA.new 2048
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = 0
cert.not_before = Time.now
cert.not_after = Time.now + 10 * 365 * 24 * 60 * 60 # 10 years validity
cert.public_key = key.public_key
cert.issuer = OpenSSL::X509::Name.parse('CN=Test')
cert.subject = OpenSSL::X509::Name.parse('CN=test1 ESSAI1')
# Open certificate files
#key2 = OpenSSL::PKey::RSA.new 2048
#cert2 = OpenSSL::X509::Certificate.new
#cert2.version = 2
#cert2.serial = 0
#cert2.not_before = Time.now
#cert2.not_after = Time.now + 10 * 365 * 24 * 60 * 60 # 10 years validity
#cert2.public_key = key2.public_key
#cert2.issuer = OpenSSL::X509::Name.parse('CN=Test2')
#cert2.subject = OpenSSL::X509::Name.parse('CN=test2 ESSAI2')
sigannot = Origami::Annotation::Widget::Signature.new
sigannot.Rect = Origami::Rectangle[:llx => 89.0, :lly => 386.0, :urx => 190.0, :ury => 353.0]
pdf.get_page(1).add_annot(sigannot)
#sigannot2 = Origami::Annotation::Widget::Signature.new
#sigannot2.Rect = Origami::Rectangle[:llx => 89.0, :lly => 386.0, :urx => 190.0, :ury => 353.0]
#pdf.get_page(1).add_annot(sigannot2)
# Sign the PDF with the specified keys
pdf.sign(cert, key,
:method => 'adbe.pkcs7.sha1',
:annotation => sigannot,
:location => "France",
:contact => "tmp#security.org",
:reason => "Proof of Concept"
)
# Sign the PDF with the specified keys
#pdf.sign(cert2, key2,
# :method => 'adbe.pkcs7.sha1',
# :annotation => sigannot2,
# :location => "France",
# :contact => "tmp#security.org",
# :reason => "Proof of Concept"
#)
# Save the resulting file
pdf.save(PDF_NEW)
I know it is quite tricky, but no one can help me?
Or using another solution maybe?

you can use CombinePDF to add watermark to PDF i hve use it in the past and it works great:
To add content to existing PDF pages, first import the new content from an existing PDF file. After that, add the content to each of the pages in your existing PDF.
In this example, we will add a company logo to each page:
company_logo = CombinePDF.load("company_logo.pdf").pages[0]
pdf = CombinePDF.load "content_file.pdf"
pdf.pages.each {|page| page << company_logo}
# notice the << operator is on a page and not a PDF object.
pdf.save "content_with_logo.pdf"
Notice the << operator is on a page and not a PDF object. The << operator acts differently on PDF objects and on Pages.
The << operator defaults to secure injection by renaming references to avoid conflics. For overlaying pages using compressed data that might not be editable (due to limited filter support), you can use:
pdf.pages(nil, false).each {|page| page << stamp_page}
You can see more details here:
https://github.com/boazsegev/combine_pdf

Related

Validate PDF is stampable - Rails, Prawn, CombinePDF

I'm working at a company where we upload a good amount of PDFs, which we later stamp using Prawn. Occasionally these PDFs upload and save fine, but when we try to stamp them later they don't work and our managers have to re-convert the file, and re-input a bunch of data.
As such we're looking for ways to validate the PDFs before they're attached to ensure they're going to be stampable later, or convert them to a PDF format we know is going to work with Prawn.
I have two questions
is there anything wrong with our stamping code? (posted below)
is there any way to do that sort of validation? including
converting to a Prawn doc before uploading
converting to a Prawn doc and attempting some trivial operation before uploading
any other solutions
begin
paid_stamp_pdf_file = Tempfile.new('paid')
Prawn::Document.generate(paid_stamp_pdf_file.path) do |pdf|
if self.is_paid_by_trust? && self.submitted_to_trust_date.present?
text = "Submitted to Trust - " + self.submitted_to_trust_date.strftime('%m/%d/%Y') + "\nPAID #{Date.parse(paid_on_date).strftime('%m/%d/%Y')}" + " - $#{'%.2f' % amount}" + payment_method_text
else
text = "PAID #{Date.parse(paid_on_date).strftime('%m/%d/%Y')}" + " - $#{'%.2f' % amount}" + payment_method_text
end
pdf.transparent(0.6) do
pdf.fill_color "ff0000"
pdf.text text, :size => 30, style: :bold, align: :center, valign: :center
end
end
# Stamp "PAID" to every page of the file
paid_stamp = CombinePDF.load(paid_stamp_pdf_file.path).pages[0]
URI.open(self.account_statement_file.blob.url) do |tmp_pdf_file|
pdf = CombinePDF.load tmp_pdf_file.path
pdf.pages.each {|page| page << paid_stamp}
ActiveRecord::Base.transaction do
if pdf.save tmp_pdf_file.path
file_name = self.account_statement_file.filename
self.account_statement_file.purge
self.account_statement_file.attach(io: File.open(tmp_pdf_file.path), filename: file_name, content_type: 'application/pdf')
self.update(is_paid: true, paid_date: paid_on_date, marked_paid_by_user_id: user.id)
return true
else
return false
end
end
end
rescue Exception => e
Rails.logger.error("Failed to mark statement ID #{self.id}: #{e.message}")
return false
end
Any help is greatly appreciated!
ruby 2.7.2
rails 6.1.1
prawn 2.4.0
combine_pdf 1.0.21
Edit:
Was able to replicated error, trying to load from file url
Occurs at line
Same error occurs when trying to parse downloaded file
For anyone else who sees this it was related to CombinePDF only parsing until it reaches what the metadata says the length, but some files lie about that so it causes them to fail and produce a RangeError: index out of range. Adding this work around, then using the relaxed option it adds fixed the issues for me, hopefully it merges into the gem itself soon.
https://github.com/boazsegev/combine_pdf/issues/191

Trying to add custom fields to envelope definition from docusign rails example

I got the embedded signing method I got from rails example on how to implement docusign embedded signing into your rails app.
I added a custom_fields object and added to the envelope object I created from the example
def embedded_signing
# base_url is the url of this application. Eg http://localhost:3000
base_url = request.base_url
user = HiringManager.find params[:hiring_manager_id]
# Fill in these constants
# Obtain an OAuth token from https://developers.hqtest.tst/oauth-token-generator
access_token = Token.access_token
# Obtain your accountId from demo.docusign.com -- the account id is shown in the drop down on the
# upper right corner of the screen by your picture or the default picture.
account_id = ENV["docusign_client_id"]
# Recipient Information:
signer_name = user.full_name
signer_email = user.email
base_path = 'http://demo.docusign.net/restapi'
client_user_id = user.id # Used to indicate that the signer will use an embedded
# Signing Ceremony. Represents the signer's userId within
# your application.
authentication_method = 'None' # How is this application authenticating
# the signer? See the `authenticationMethod' definition
file_name = 'agreement.pdf' # The document to be signed.
# Step 1. Create the envelope request definition
envelope_definition = DocuSign_eSign::EnvelopeDefinition.new
envelope_definition.email_subject = "Please sign this Newcraft Placement Agreement"
doc = DocuSign_eSign::Document.new({
:documentBase64 => Base64.encode64(File.binread(File.join('data', file_name))),
:name => "Agreement signed", :fileExtension => "pdf", :documentId => "1"})
# The order in the docs array determines the order in the envelope
envelope_definition.documents = [doc]
# create a signer recipient to sign the document, identified by name and email
# We're setting the parameters via the object creation
signer = DocuSign_eSign::Signer.new ({
:email => signer_email, :name => signer_name,
:clientUserId => client_user_id, :recipientId => 1
})
sign_here = DocuSign_eSign::SignHere.new ({
:documentId => '1', :pageNumber => '4',
:recipientId => '1', :tabLabel => 'SignHereTab',
:xPosition => '75', :yPosition => '70'
})
# Tabs are set per recipient / signer
tabs = DocuSign_eSign::Tabs.new({:signHereTabs => [sign_here]})
signer.tabs = tabs
# Add the recipients to the envelope object
recipients = DocuSign_eSign::Recipients.new({:signers => [signer]})
envelope_definition.recipients = recipients
# Add custom fields to the envelope object
custom_fields = DocuSign_eSign::CustomFieldV2.new({
:configuration_type => 'text', :required => 'true',
:name => 'date', :fieldId => '', :value => 'Todays date'
})
envelope_definition.custom_fields = custom_fields
# Request that the envelope be sent by setting |status| to "sent".
# To request that the envelope be created as a draft, set to "created"
envelope_definition.status = "sent"
# Step 2. Call DocuSign with the envelope definition to have the
# envelope created and sent
configuration = DocuSign_eSign::Configuration.new
configuration.host = base_path
api_client = DocuSign_eSign::ApiClient.new configuration
api_client.default_headers["Authorization"] = "Bearer " + access_token
envelopes_api = DocuSign_eSign::EnvelopesApi.new api_client
results = envelopes_api.create_envelope account_id, envelope_definition
envelope_id = results.envelope_id
# Step 3. create the recipient view request for the Signing Ceremony
view_request = DocuSign_eSign::RecipientViewRequest.new
# Set the url where you want the recipient to go once they are done signing
# should typically be a callback route somewhere in your app.
view_request.return_url = "https://juice.newcraft.io/edit-manager"
# How has your app authenticated the user? In addition to your app's
# authentication, you can include authenticate steps from DocuSign.
# Eg, SMS authentication
view_request.authentication_method = authentication_method
# Recipient information must match embedded recipient info
# we used to create the envelope.
view_request.email = signer_email
view_request.user_name = signer_name
view_request.client_user_id = client_user_id
# Step 4. call the CreateRecipientView API
results = envelopes_api.create_recipient_view account_id, envelope_id, view_request
user.signed_agreement = true
user.save
# Step 5. Redirect the user to the Signing Ceremony
# Don't use an iFrame!
# State can be stored/recovered using the framework's session or a
# query parameter on the returnUrl (see the makeRecipientViewRequest method)
render json: results
rescue DocuSign_eSign::ApiError => e
#error_msg = e.response_body
render json: #error_msg
end
I am finding it difficult understanding how to insert a custom field that user can manually fill on the pdf agreement document that is displayed for users signature. I also know I need to add the position the custom field tab will reside which the documentation does not really explain how to add to an envelop object you create from a method.
First, let's try to see if we understand your requirement. You want user to fill in some data on the envelope and then collect this data in your application after the envelope is complete, is that correct?
To do that, you don't need custom fields. You can easily to that with regular tabs. Text tabs are probably the simplest way to do so. You add a text tab to your envelope, similar to how you added a SignHere tab and the user would have to fill in the text/value. You can then get this information using other API calls.
Here is the API call to obtain the tab value:
https://developers.docusign.com/esign-rest-api/reference/Envelopes/EnvelopeRecipientTabs/
You basically do a GET /v2.1/accounts/{accountId}/envelopes/{envelopeId}/recipients/{recipientId}/tabs if you are using the v2 or V2.1 API (just replace 2.1 with 2)

Rails Nokogiri XML parsing working locally, not on Heroku

I have the following script running in my rails app. When it runs locally, it parses all of the courses correctly (9673 results) but when I push it to Heroku, it returns exactly 2x this number (19346 results). Any ideas? Are there possible Heroku performance limitations that could be causing the cycle to repeat itself?
# Pulls all course data for specified year, based on subject list
subjectdoc= Nokogiri.XML(open("https://courseroster.reg.cornell.edu/courses/roster/SP15/xml/"))
# Reads each subject and stores it in local variable prefix
subjectdoc.xpath("//subject/#subject").each do |prefix|
# Link to course pages, substituting in prefix in URL
classdoc= Nokogiri.XML(open("https://courseroster.reg.cornell.edu/courses/roster/SP15/#{prefix}/xml/"))
# Reads each course and stores listed vars
classdoc.xpath("/courses/course").each do |course|
num = course["catalog_nbr"] || "Not provided"
subj = course["subject"] || "Not provided"
title = (course.at("course_title/text()") || "Not provided").to_s
cid = (course.at("sections/section/#class_number") || "Not provided").to_s
inst = (course.at("sections/section/meeting/instructors/instructor/text()") || "Not provided").to_s
# Creates a cornell class in Cornellclasses table
Cornellclass.create(:prefix => subj, :coursenumber => num, :instructor => inst, :title => title, :courseid => cid)
end
end

Get large images from Page Feed? using Koala Gem & Rails 3.2

Does anyone know how to pull different size images from the Page Feed?
I was trying to use the Type hash that works great for friends and profile pictures.
#page-feed = #graph.get_connections("somepage", "feed", {"type" => "large"})
but for some reason I'm always getting the same picture size for all posts.
Thanks !
Reading the code here: https://github.com/arsduo/koala/blob/81e66f459df840d9d5e122c0d498e2fb9d146655/lib/koala/api/graph_api.rb (line 178, def get_picture) you can see that the method accepts options hash:
Gem source:
# Fetches a photo.
# (Facebook returns the src of the photo as a response header; this method parses that properly,
# unlike using get_connections("photo").)
#
# #param options options for Facebook (see #get_object).
# To get a different size photo, pass :type => size (small, normal, large, square).
# #param block (see Koala::Facebook::API#api)
#
# #note to delete photos or videos, use delete_object(id)
#
# #return the URL to the image
def get_picture(object, args = {}, options = {}, &block)
# Gets a picture object, returning the URL (which Facebook sends as a header)
resolved_result = graph_call("#{object}/picture", args, "get", options.merge(:http_component => :headers)) do |result|
result ? result["Location"] : nil
end
block ? block.call(resolved_result) : resolved_result
end
So you can call it like .get_picture(id, type: :large). Like this:
graph = Koala::Facebook::API.new(token)
profile = graph.get_object('me')
graph.get_picture(profile['id'], type: :large)
Just in case anyone else comes across this, this is what I had to do in order to retrieve the large images. Note that I'm only grabbing the first post in the feed.
In my controller:
#fb_post = #facebook.get_connections(page_id, 'posts').first
#photo = #facebook.get_connections(#fb_post['id'], 'attachments').first
Then, to grab the proper link in the view inside of an img tag, I used:
#photo["media"]["image"]["src"]
For anyone still struggling with this, I was able to use the 'full_picture' field in my Koala object to retrieve a full-resolution URLs of the images:
fields = ['id','picture','full_picture']
graphObj = Koala::Facebook::API.new(accessToken)
hashes = graphObj.get_connection(groupId, 'feed', { limit: 10, fields: fields })
hashes.each do |hash|
mash = Hashie::Mash.new(hash)
fullPicture = mash.full_picture
end

Save large text file without using too much memory

I have a model that creates a KML file. I treat that KML as a string and then forward that to mailer which then delivers it:
def write_kml(coords3d, time)
kml = String.new
kml << header
coords3d.each do |coords|
coordinates = String.new
coords.each do |coord|
lat = coord[0].to_f
lng = coord[1].to_f
coordinates << "#{lng}" + "," "#{lat}" + ",0 "
kml << polygon(coordinates)
end
end
kml << footer
kml
end
This gets used here:
CsvMailer.kml_send(kml,time, mode, email).deliver
Mailer:
def kml_send(kml, time, mode, email)
#time = (time / 60).to_i
#mode = mode
gen_time = Time.now
file_name = gen_time.strftime('%Y-%m-%d %H:%M:%S') + " #{#mode.to_s}" + " #{#time.to_s}(mins)"
attachments[file_name + '(KML).kml'] = { mime_type: 'text/kml', content: kml}
mail to: email, subject: ' KML Filem'
end
It takes up a huge amount of memory. Some of these files are quite large (200MB), so on Heroku for example, they take up too much space.
I had some ideas using S3, but I would need to create this file to begin with, so it would still use the memory. Can I write straight to S3 without using memory?
You can do this with s3 multipart uploads, since they don't require you to know the file size upfront.
Parts have to be at least 5MB in size, so the easiest way to use this is to write your data to an in memory buffer and upload the part to s3 every time you get past 5MB. There's a limit of 10000 parts for an upload, so if your file size is going to be > 50GB then you'd need to know that ahead of time so that you can make the parts bigger.
Using the fog library, that would look a little like
def upload_chunk connection, upload_id, chunk, index
md5 = Base64.encode64(Digest::MD5.digest(chunk)).strip
connection.upload_part('bucket', 'a_key', upload_id, chunk_index, chunk, 'Content-MD5' => md5 )
end
connection = Fog::Storage::AWS.new(:aws_access_key_id => '...', :region => '...', :aws_secret_access_key => '...'
upload_id = connection.initiate_multipart_upload('bucket', 'a_key').body['UploadId']
chunk_index = 1
kml = String.new
kml << header
coords3d.each do |coords|
#append to kml
if kml.bytesize > 5 * 1024 * 1024
upload_chunk connection, upload_id, kml, chunk_index
chunk_index += 1
kml = ''
end
end
upload_chunk connection, upload_id, kml, chunk_index
#when you've uploaded all the chunks
connection.complete_multipart_upload('bucket', 'a_key', upload_id)
You could probably come up with something neater if you created an uploader class to wrap the buffer and stuck all the s3 logic in there. Then your kml code doesn't have to know whether it has an actual string or a string that flushes to s3 periodically

Resources