Skip to content

Instantly share code, notes, and snippets.

@revant
Created October 11, 2024 05:54
Show Gist options
  • Save revant/39456fa8f9ce8c47f695fac9611586d1 to your computer and use it in GitHub Desktop.
Save revant/39456fa8f9ce8c47f695fac9611586d1 to your computer and use it in GitHub Desktop.
Frappe Framework and PyHanko digital signatures
# License: MIT
# Store signatures in "Digital Signature"
# it uploads signatures on S3 for secure storage.
# Code snippet to fetch signature and sign document
import io
import os
import ntpath
import boto3
import botocore.exceptions
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import signers
from pyhanko.sign.fields import SigFieldSpec, append_signature_field
import frappe
from frappe.utils.pdf import get_pdf
def validate_s3_settings():
settings_doc = frappe.get_single("Digital Signature Settings")
if (
not settings_doc.s3_endpoint_url
or not settings_doc.s3_region_name
or not settings_doc.s3_bucket
or not settings_doc.aws_access_key_id
or not settings_doc.get_password("aws_secret_access_key")
):
frappe.throw(
"Please enter all mandatory fields for S3 Details in Digital Signature Settings" # noqa
)
def get_s3_client(settings_doc):
aws_secret_access_key = settings_doc.get_password("aws_secret_access_key")
return boto3.client(
service_name="s3",
endpoint_url=settings_doc.s3_endpoint_url,
region_name=settings_doc.s3_region_name,
aws_access_key_id=settings_doc.aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
)
def get_pfx_from_s3(key):
validate_s3_settings()
settings_doc = frappe.get_single("Digital Signature Settings")
s3_client = get_s3_client(settings_doc)
try:
path = "/tmp/" + ntpath.basename(key)
s3_client.download_file(settings_doc.s3_bucket, key, path)
return path
except botocore.exceptions.ClientError:
frappe.throw("Failed to get signature file.", title="Error")
def get_signed_pdf(doctype, docname, print_format=None):
doc = frappe.get_doc(doctype, docname)
html = frappe.get_print(doctype, docname, print_format or "Offer Letter")
file = get_pdf(html)
file = io.BytesIO(file)
w = IncrementalPdfFileWriter(file)
p = None
if doc.name_of_primary_officer:
p = sign_officer(doc.name_of_primary_officer, w, 43, 297, 1)
if doc.name_of_authorized_signatory:
p = sign_officer(doc.name_of_authorized_signatory, w, 350, 297, 1)
return p.getbuffer() if p else get_pdf(html)
def get_officer_signer(officer, file_path):
signer = signers.SimpleSigner.load_pkcs12(
pfx_file=file_path,
passphrase=(officer.get_password("sign_secret")).encode(),
)
return signer
def sign_officer(officer_name, w, x1, y1, page):
"""
Manipulate following values to adjust signature:
x1: Horizontal Position (increase to move right)
y1: Vertical Position (increase to move upward)
x2: Width
y2: Height
"""
officer = frappe.get_doc("Digital Signature", officer_name)
if officer.s3_key:
x2 = x1 + 140
y2 = y1 + 44
file_path = get_pfx_from_s3(officer.s3_key)
officer_signer = get_officer_signer(officer, file_path)
append_signature_field(
w,
SigFieldSpec(
sig_field_name=officer.name,
box=(x1, y1, x2, y2),
on_page=page,
),
)
out = signers.sign_pdf(
w,
signers.PdfSignatureMetadata(field_name=officer.name),
signer=officer_signer,
)
os.remove(file_path)
return out
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment