/ pypdftools / sign.py
sign.py
  1  '''
  2  Simple tool for filling and signing pdfs
  3  '''
  4  import os
  5  import typer
  6  import PyPDF2
  7  from sys import exit
  8  
  9  from pyhanko import stamp
 10  from pyhanko.pdf_utils import text, images
 11  from pyhanko.pdf_utils.font import opentype
 12  from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
 13  from pyhanko.sign import signers
 14  from pyhanko.sign import fields
 15  
 16  
 17  FONT = os.path.join(os.path.dirname(__file__),
 18                      'resources', 'NotoSans-Regular.ttf')
 19  
 20  app = typer.Typer()
 21  
 22  
 23  def get_signature_fields(pdf: str) -> list[str]:
 24      sig_fields = []
 25      with open(pdf, 'rb') as file:
 26          reader = PyPDF2.PdfReader(file)
 27  
 28          i = 0
 29          for page in reader.pages:
 30              if '/Annots' not in page:
 31                  i += 1
 32                  continue
 33  
 34              for annotation in page['/Annots']:
 35                  annotation_dict = reader.get_object(annotation)
 36                  if annotation_dict is None:
 37                      continue
 38  
 39                  if annotation_dict.get('/Subtype') != '/Widget' \
 40                          or annotation_dict.get('/FT') != '/Sig':
 41                      continue
 42  
 43                  sig_fields.append({
 44                      'name': annotation_dict.get('/T'),
 45                      'rect': annotation_dict.get('/Rect'),
 46                      'page': i,
 47                  })
 48  
 49              i += 1
 50      return sig_fields
 51  
 52  
 53  def add_sig(w, field_name, coordinates, page):
 54      fields.append_signature_field(
 55          w, sig_field_spec=fields.SigFieldSpec(
 56              field_name, on_page=page, box=coordinates
 57          )
 58      )
 59  
 60      return w
 61  
 62  
 63  def sign(w, outf, signer, field_name: str, stamp_path: str) -> None:
 64      meta = signers.PdfSignatureMetadata(field_name=field_name)
 65      pdf_signer = signers.PdfSigner(
 66          meta, signer=signer, stamp_style=stamp.TextStampStyle(
 67              stamp_text='%(signer)s\nTime: %(ts)s',
 68              text_box_style=text.TextBoxStyle(
 69                  font=opentype.GlyphAccumulatorFactory(FONT)
 70              ),
 71              background=images.PdfImage(stamp_path),
 72              border_width=1,
 73              background_opacity=0.3
 74          ),
 75      )
 76  
 77      pdf_signer.sign_pdf(w, output=outf)
 78  
 79  
 80  @app.command('sign')
 81  def sign_pdf_from_p12(
 82      pdf: str,
 83      cert: str,
 84      pwd: str,
 85  
 86      automatic: bool = typer.Option(
 87          False, '--automagic', '-a', help='Sign the existing field'),
 88  
 89          field_name: str = typer.Option(
 90              None, '--field', '-f', help='Output file name'),
 91          out: str = typer.Option(None, '--output', '-o',
 92                                  help='Output file name'),
 93          page: int = typer.Option(
 94              0, '--page', '-p', help='page where the stamp will be applied'),
 95          dots: str = typer.Option(
 96              None, '--dots', '-d', help='Coordinates in pdf. Axis at left bottom corner'),
 97          stamp: str = typer.Option(None, '--stamp', '-s', help='Stamp image PNG')) -> None:
 98  
 99      # output name if not given
100      if out is None:
101          out = pdf[0:-4] + '_signed.pdf'
102  
103      if not os.path.exists(pdf):
104          print('File does not exist')
105          exit(1)
106  
107      if not os.path.exists(cert):
108          print('Certificate does not exist')
109          exit(1)
110  
111      if not stamp or not os.path.exists(stamp):
112          print('Stamp does not exist')
113          exit(1)
114  
115      fields = []
116      if automatic:
117          fields = get_signature_fields(pdf)
118  
119          if len(fields) == 0:
120              print('No signature fields found')
121              exit(1)
122  
123          print(fields)
124  
125          signer = signers.SimpleSigner.load_pkcs12(
126              pfx_file=cert, passphrase=bytes(pwd, 'utf-8'))
127          w = IncrementalPdfFileWriter(open(pdf, 'rb'), strict=False)
128          outf = open(out, 'wb')
129  
130          # try to sign until signed
131          for field in fields:
132              coordinates = tuple(field['rect'])
133              field_name = field['name']
134              page = field['page']
135  
136              try:
137                  w = add_sig(w, field_name, coordinates, page)
138              except Exception as e:
139                  print(e, 'Trying to sign it anyway')
140  
141              try:
142                  sign(w, outf, signer, field_name)
143                  return 0
144              except Exception as e:
145                  pass
146  
147      elif field_name is not None and page is not None and dots is not None:
148          coordinates = tuple(map(lambda x: int(x), dots.split(',')))
149  
150      else:
151          print('Not enough information given')
152          exit(1)
153  
154      # open files for rw
155      signer = signers.SimpleSigner.load_pkcs12(
156          pfx_file=cert, passphrase=bytes(pwd, 'utf-8'))
157      w = IncrementalPdfFileWriter(open(pdf, 'rb'), strict=False)
158      outf = open(out, 'wb')
159  
160      # sign the pdf
161      print(
162          f"Signing {pdf} with {cert} at {coordinates} at page {page} at {field_name}")
163  
164      try:
165          w = add_sig(w, field_name, coordinates, page)
166      except Exception as e:
167          print(e, 'Trying to sign it anyway')
168  
169      sign(w, outf, signer, field_name, stamp)
170      print(out)
171  
172  
173  if __name__ == '__main__':
174      app()