Skip to content

Commit 608e722

Browse files
committed
add pdf signer tutorial
1 parent a49d548 commit 608e722

File tree

7 files changed

+232
-0
lines changed

7 files changed

+232
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -161,5 +161,6 @@ This is a repository of all the tutorials of [The Python Code](https://www.thepy
161161
- [How to Compress PDF Files in Python](https://www.thepythoncode.com/article/compress-pdf-files-in-python). ([code](handling-pdf-files/pdf-compressor))
162162
- [How to Encrypt and Decrypt PDF Files in Python](https://www.thepythoncode.com/article/encrypt-pdf-files-in-python). ([code](handling-pdf-files/encrypt-pdf))
163163
- [How to Merge PDF Files in Python](https://www.thepythoncode.com/article/merge-pdf-files-in-python). ([code](handling-pdf-files/pdf-merger))
164+
- [How to Sign PDF Files in Python](https://www.thepythoncode.com/article/sign-pdf-files-in-python). ([code](handling-pdf-files/pdf-signer))
164165

165166
For any feedback, please consider pulling requests.
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# [How to Sign PDF Files in Python](https://www.thepythoncode.com/article/sign-pdf-files-in-python)
2+
To run this:
3+
- `pip3 install -r requirements.txt`
4+
- Refer to [the tutorial](https://www.thepythoncode.com/article/sign-pdf-files-in-python) on how to run the script.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PDFNetPython3==8.1.0
2+
pyOpenSSL==20.0.1
+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# Import Libraries
2+
import OpenSSL
3+
import os
4+
import time
5+
import argparse
6+
from PDFNetPython3.PDFNetPython import *
7+
from typing import Tuple
8+
9+
10+
def createKeyPair(type, bits):
11+
"""
12+
Create a public/private key pair
13+
Arguments: Type - Key Type, must be one of TYPE_RSA and TYPE_DSA
14+
bits - Number of bits to use in the key (1024 or 2048 or 4096)
15+
Returns: The public/private key pair in a PKey object
16+
"""
17+
pkey = OpenSSL.crypto.PKey()
18+
pkey.generate_key(type, bits)
19+
return pkey
20+
21+
22+
23+
def create_self_signed_cert(pKey):
24+
"""Create a self signed certificate. This certificate will not require to be signed by a Certificate Authority."""
25+
# Create a self signed certificate
26+
cert = OpenSSL.crypto.X509()
27+
# Common Name (e.g. server FQDN or Your Name)
28+
cert.get_subject().CN = "BASSEM MARJI"
29+
# Serial Number
30+
cert.set_serial_number(int(time.time() * 10))
31+
# Not Before
32+
cert.gmtime_adj_notBefore(0) # Not before
33+
# Not After (Expire after 10 years)
34+
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
35+
# Identify issue
36+
cert.set_issuer((cert.get_subject()))
37+
cert.set_pubkey(pKey)
38+
cert.sign(pKey, 'md5') # or cert.sign(pKey, 'sha256')
39+
return cert
40+
41+
42+
def load():
43+
"""Generate the certificate"""
44+
summary = {}
45+
summary['OpenSSL Version'] = OpenSSL.__version__
46+
# Generating a Private Key...
47+
key = createKeyPair(OpenSSL.crypto.TYPE_RSA, 1024)
48+
# PEM encoded
49+
with open('.\static\private_key.pem', 'wb') as pk:
50+
pk_str = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
51+
pk.write(pk_str)
52+
summary['Private Key'] = pk_str
53+
# Done - Generating a private key...
54+
# Generating a self-signed client certification...
55+
cert = create_self_signed_cert(pKey=key)
56+
with open('.\static\certificate.cer', 'wb') as cer:
57+
cer_str = OpenSSL.crypto.dump_certificate(
58+
OpenSSL.crypto.FILETYPE_PEM, cert)
59+
cer.write(cer_str)
60+
summary['Self Signed Certificate'] = cer_str
61+
# Done - Generating a self-signed client certification...
62+
# Generating the public key...
63+
with open('.\static\public_key.pem', 'wb') as pub_key:
64+
pub_key_str = OpenSSL.crypto.dump_publickey(
65+
OpenSSL.crypto.FILETYPE_PEM, cert.get_pubkey())
66+
#print("Public key = ",pub_key_str)
67+
pub_key.write(pub_key_str)
68+
summary['Public Key'] = pub_key_str
69+
# Done - Generating the public key...
70+
# Take a private key and a certificate and combine them into a PKCS12 file.
71+
# Generating a container file of the private key and the certificate...
72+
p12 = OpenSSL.crypto.PKCS12()
73+
p12.set_privatekey(key)
74+
p12.set_certificate(cert)
75+
open('.\static\container.pfx', 'wb').write(p12.export())
76+
# You may convert a PKSC12 file (.pfx) to a PEM format
77+
# Done - Generating a container file of the private key and the certificate...
78+
# To Display A Summary
79+
print("## Initialization Summary ##################################################")
80+
print("\n".join("{}:{}".format(i, j) for i, j in summary.items()))
81+
print("############################################################################")
82+
return True
83+
84+
85+
def sign_file(input_file: str, signatureID: str, x_coordinate: int,
86+
y_coordinate: int, pages: Tuple = None, output_file: str = None
87+
):
88+
"""Sign a PDF file"""
89+
# An output file is automatically generated with the word signed added at its end
90+
if not output_file:
91+
output_file = (os.path.splitext(input_file)[0]) + "_signed.pdf"
92+
# Initialize the library
93+
PDFNet.Initialize()
94+
doc = PDFDoc(input_file)
95+
# Create a signature field
96+
sigField = SignatureWidget.Create(doc, Rect(
97+
x_coordinate, y_coordinate, x_coordinate+100, y_coordinate+50), signatureID)
98+
# Iterate throughout document pages
99+
for page in range(1, (doc.GetPageCount() + 1)):
100+
# If required for specific pages
101+
if pages:
102+
if str(page) not in pages:
103+
continue
104+
pg = doc.GetPage(page)
105+
# Create a signature text field and push it on the page
106+
pg.AnnotPushBack(sigField)
107+
# Signature image
108+
sign_filename = os.path.dirname(
109+
os.path.abspath(__file__)) + "\static\signature.jpg"
110+
# Self signed certificate
111+
pk_filename = os.path.dirname(
112+
os.path.abspath(__file__)) + "\static\container.pfx"
113+
# Retrieve the signature field.
114+
approval_field = doc.GetField(signatureID)
115+
approval_signature_digsig_field = DigitalSignatureField(approval_field)
116+
# Add appearance to the signature field.
117+
img = Image.Create(doc.GetSDFDoc(), sign_filename)
118+
found_approval_signature_widget = SignatureWidget(
119+
approval_field.GetSDFObj())
120+
found_approval_signature_widget.CreateSignatureAppearance(img)
121+
# Prepare the signature and signature handler for signing.
122+
approval_signature_digsig_field.SignOnNextSave(pk_filename, '')
123+
# The signing will be done during the following incremental save operation.
124+
doc.Save(output_file, SDFDoc.e_incremental)
125+
# Develop a Process Summary
126+
summary = {
127+
"Input File": input_file, "Signature ID": signatureID,
128+
"Output File": output_file, "Signature File": sign_filename,
129+
"Certificate File": pk_filename
130+
}
131+
# Printing Summary
132+
print("## Summary ########################################################")
133+
print("\n".join("{}:{}".format(i, j) for i, j in summary.items()))
134+
print("###################################################################")
135+
return True
136+
137+
138+
def sign_folder(**kwargs):
139+
"""Sign all PDF Files within a specified path"""
140+
input_folder = kwargs.get('input_folder')
141+
signatureID = kwargs.get('signatureID')
142+
pages = kwargs.get('pages')
143+
x_coordinate = int(kwargs.get('x_coordinate'))
144+
y_coordinate = int(kwargs.get('y_coordinate'))
145+
# Run in recursive mode
146+
recursive = kwargs.get('recursive')
147+
# Loop though the files within the input folder.
148+
for foldername, dirs, filenames in os.walk(input_folder):
149+
for filename in filenames:
150+
# Check if pdf file
151+
if not filename.endswith('.pdf'):
152+
continue
153+
# PDF File found
154+
inp_pdf_file = os.path.join(foldername, filename)
155+
print("Processing file =", inp_pdf_file)
156+
# Compress Existing file
157+
sign_file(input_file=inp_pdf_file, signatureID=signatureID, x_coordinate=x_coordinate,
158+
y_coordinate=y_coordinate, pages=pages, output_file=None)
159+
if not recursive:
160+
break
161+
162+
163+
def is_valid_path(path):
164+
"""Validates the path inputted and checks whether it is a file path or a folder path"""
165+
if not path:
166+
raise ValueError(f"Invalid Path")
167+
if os.path.isfile(path):
168+
return path
169+
elif os.path.isdir(path):
170+
return path
171+
else:
172+
raise ValueError(f"Invalid Path {path}")
173+
174+
175+
def parse_args():
176+
"""Get user command line parameters"""
177+
parser = argparse.ArgumentParser(description="Available Options")
178+
parser.add_argument('-l', '--load', dest='load', action="store_true",
179+
help="Load the required configurations and create the certificate")
180+
parser.add_argument('-i', '--input_path', dest='input_path', type=is_valid_path,
181+
help="Enter the path of the file or the folder to process")
182+
parser.add_argument('-s', '--signatureID', dest='signatureID',
183+
type=str, help="Enter the ID of the signature")
184+
parser.add_argument('-p', '--pages', dest='pages', type=tuple,
185+
help="Enter the pages to consider e.g.: [1,3]")
186+
parser.add_argument('-x', '--x_coordinate', dest='x_coordinate',
187+
type=int, help="Enter the x coordinate.")
188+
parser.add_argument('-y', '--y_coordinate', dest='y_coordinate',
189+
type=int, help="Enter the y coordinate.")
190+
path = parser.parse_known_args()[0].input_path
191+
if path and os.path.isfile(path):
192+
parser.add_argument('-o', '--output_file', dest='output_file',
193+
type=str, help="Enter a valid output file")
194+
if path and os.path.isdir(path):
195+
parser.add_argument('-r', '--recursive', dest='recursive', default=False, type=lambda x: (
196+
str(x).lower() in ['true', '1', 'yes']), help="Process Recursively or Non-Recursively")
197+
args = vars(parser.parse_args())
198+
# To Display The Command Line Arguments
199+
print("## Command Arguments #################################################")
200+
print("\n".join("{}:{}".format(i, j) for i, j in args.items()))
201+
print("######################################################################")
202+
return args
203+
204+
205+
if __name__ == '__main__':
206+
# Parsing command line arguments entered by user
207+
args = parse_args()
208+
if args['load'] == True:
209+
load()
210+
else:
211+
# If File Path
212+
if os.path.isfile(args['input_path']):
213+
sign_file(
214+
input_file=args['input_path'], signatureID=args['signatureID'],
215+
x_coordinate=int(args['x_coordinate']), y_coordinate=int(args['y_coordinate']),
216+
pages=args['pages'], output_file=args['output_file']
217+
)
218+
# If Folder Path
219+
elif os.path.isdir(args['input_path']):
220+
# Process a folder
221+
sign_folder(
222+
input_folder=args['input_path'], signatureID=args['signatureID'],
223+
x_coordinate=int(args['x_coordinate']), y_coordinate=int(args['y_coordinate']),
224+
pages=args['pages'], recursive=args['recursive']
225+
)
Binary file not shown.
Binary file not shown.
Loading

0 commit comments

Comments
 (0)