From 7612e52a638a220c551591b8a1530864a3d53936 Mon Sep 17 00:00:00 2001 From: Axel Svensson Date: Tue, 8 Aug 2023 14:50:55 +0200 Subject: [PATCH 1/6] Fixes - Allow mixed-case .pdf suffixes - Sort signature file names - Use "pdf-sign" as X11 class name --- pdf-sign | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pdf-sign b/pdf-sign index 64be231..4911b8c 100755 --- a/pdf-sign +++ b/pdf-sign @@ -7,8 +7,8 @@ import argparse, os, queue, re, subprocess, sys, tempfile, time # Inspired by https://unix.stackexchange.com/a/141496 def main(args): filePath=args.input - if not m("^.*\.(pdf|PDF)$", filePath): - die("Input file must end with .pdf or .PDF") + if not isPdfFilename(filePath): + die("Input file must end with .pdf (case insensitive)") with tempfile.TemporaryDirectory() as tempdir: intmp=lambda fileName: os.path.join(tempdir, fileName) # Maybe flatten (make forms non-editable) before signing @@ -34,9 +34,10 @@ def main(args): if not args.signature and args.batch: die('In batch mode, signature must be specified.') signatureDir=getSignatureDir() - signatures=[*filter(lambda x: m("^.*\.pdf$", x), os.listdir(signatureDir))] if not args.signature else [None] + signatures=[*filter(isPdfFilename, os.listdir(signatureDir))] if not args.signature else [None] if not signatures: die(f'No .pdf files found in {signatureDir}') + signatures.sort() signatureIndex=Cell(0) signaturePath=Cell(lambda: args.signature if args.signature else os.path.join(signatureDir, signatures[signatureIndex()])) signatureSize=Cell(lambda: pdfGetSize(signaturePath())) @@ -138,7 +139,7 @@ def main(args): # Error handling tk.Tk.report_callback_exception = lambda self, exc, val, tb: die(val) # Window and menu - root = tk.Tk() + root = tk.Tk(className="pdf-sign") rootmenu = tk.Menu(root) root.config(menu=rootmenu) filemenu = tk.Menu(rootmenu, tearoff=0) @@ -252,8 +253,7 @@ def main(args): root.mainloop() # End of GUI if doSign: - [ignored, pathPart1, pathPart2] = m("^(.*)(\.[Pp][Dd][Ff])$", filePath) - signedFilePath=args.output if args.output else f'{pathPart1}.signed{pathPart2}' + signedFilePath=args.output if args.output else f'{filePath[:-4]}.signed{filePath[-4:]}' if os.path.exists(signedFilePath): if args.existing=='backup': backupFilePath=f'{signedFilePath}.backup{time.strftime("%Y%m%d_%H%M%S")}' @@ -369,6 +369,9 @@ def m(pattern, string): ret.append(match.group(index)) return ret +def isPdfFilename(filename): + return m(r"^.*\.[Pp][Dd][Ff]$", filename) + def fromCmdOutput(cmd, pattern): sp=subprocess.run(cmd, check=True, capture_output=True) result=sp.stdout.decode('utf-8') From 41d1ab707644e7e15c58daf5c0e56fa3a055eb5a Mon Sep 17 00:00:00 2001 From: Axel Svensson Date: Tue, 8 Aug 2023 16:59:51 +0200 Subject: [PATCH 2/6] Improve error handling --- pdf-sign | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pdf-sign b/pdf-sign index 4911b8c..0f4bc5a 100755 --- a/pdf-sign +++ b/pdf-sign @@ -2,7 +2,7 @@ #Dependencies: python3.7 or later with module tkinter, gs (Ghostscript), pdftk and pdfinfo. -import argparse, os, queue, re, subprocess, sys, tempfile, time +import argparse, os, queue, re, subprocess, sys, tempfile, traceback, time # Inspired by https://unix.stackexchange.com/a/141496 def main(args): @@ -137,7 +137,10 @@ def main(args): doSign=True root.destroy() # Error handling - tk.Tk.report_callback_exception = lambda self, exc, val, tb: die(val) + def tkerror(self, exc, val, tb): + traceback.print_exception(exc, val, tb) + sys.exit(1) + tk.Tk.report_callback_exception = tkerror # Window and menu root = tk.Tk(className="pdf-sign") rootmenu = tk.Menu(root) @@ -379,7 +382,7 @@ def fromCmdOutput(cmd, pattern): def die(reason): print(reason, file=sys.stderr) - sys.exit(2) + sys.exit(1) # Monkey-patch argparse if necessary if not 'BooleanOptionalAction' in dir(argparse): From d6adc14fecbd9a0a774b029cf89b7e1a9e6f1359 Mon Sep 17 00:00:00 2001 From: Axel Svensson Date: Thu, 17 Aug 2023 20:05:24 +0200 Subject: [PATCH 3/6] Improve readme --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f88d76..78128d7 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,18 @@ The recommended way is: * Put the signed file in `~/.pdf_signatures/`. You can now use the `pdf-sign` tool interactively (or non-interactively) to sign PDF files. +The GUI is self documented and allows both keyboard-only and pointer-only operation. Run `pdf-sign -h` or `pdf-create-empty -h` for details. **Installation** -* Install dependencies: `python3.7` or later with module `tkinter`, `gs` (Ghostscript), `pdftk` and `pdfinfo`. +* Install dependencies + * `python3.7` or later + * Python module `tkinter` (only needed for interactive use) + * `gs` (Ghostscript) + * `pdftk` + * `pdfinfo` * Copy one or both tools to a directory in your `$PATH`. **Installation on Debian** From f58854ff5d220fd38408fdc73a50335b79b0b742 Mon Sep 17 00:00:00 2001 From: Axel Svensson Date: Sat, 1 Jun 2024 13:44:53 +0200 Subject: [PATCH 4/6] Fix GUI issues - Fix bug in displaySize() - Use floats and round rather than int to improve error propagation - Wait until after root window is created to bind onDocViewResize, to ignore initial problems. Then set initial root window size explicitly to trigger correct sizing. This fixes #10. - Separate threshold to change displayMaxSize into x & y direction. --- pdf-sign | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pdf-sign b/pdf-sign index 0f4bc5a..c11cb1c 100755 --- a/pdf-sign +++ b/pdf-sign @@ -74,8 +74,8 @@ def main(args): def displaySize(): (maxWidth, maxHeight)=displayMaxSize() (pageWidth, pageHeight)=pageSize() - scale=min(maxWidth/pageWidth, maxHeight/pageWidth) - return (int(pageWidth*scale), int(pageHeight*scale)) + scale=min(maxWidth/pageWidth, maxHeight/pageHeight) + return (round(pageWidth*scale), round(pageHeight*scale)) @Cell def displayPNG(): (w, h)=displaySize() @@ -218,19 +218,22 @@ def main(args): (oldMaxWidth, oldMaxHeight)=displayMaxSize() if(0', onDocViewResize) root._docView.pack(expand=1) root._docViewIndex=root._docView.create_image(0, 0, anchor=tk.NW) + root._docView.bind('', onDocViewResize) + root.geometry("800x600") @Cell def updateTitle(): root.title(f'Signing page {pageNumber()}/{pageCount} of {filePath}') From 07b9e61bf381ba055d536c8ed35ccd1fd324aabb Mon Sep 17 00:00:00 2001 From: Axel Svensson Date: Sun, 2 Jun 2024 01:02:09 +0200 Subject: [PATCH 5/6] Skip Cell calculations with unchanged inputs Add class Volatile to force recalculation after file contents change. --- pdf-sign | 104 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/pdf-sign b/pdf-sign index c11cb1c..23a0104 100755 --- a/pdf-sign +++ b/pdf-sign @@ -10,26 +10,32 @@ def main(args): if not isPdfFilename(filePath): die("Input file must end with .pdf (case insensitive)") with tempfile.TemporaryDirectory() as tempdir: - intmp=lambda fileName: os.path.join(tempdir, fileName) + intmp=lambda fileName: Volatile(os.path.join(tempdir, fileName)) # Maybe flatten (make forms non-editable) before signing - @Cell - def inputPDF(): - if args.flatten: - outFile=intmp('input.pdf') - subprocess.run([ - 'pdftk', filePath, - 'output', outFile, - 'flatten' - ], check=True) - return outFile - return filePath + if args.flatten: + inputPDF=str(intmp('input.pdf')) + subprocess.run([ + 'pdftk', filePath, + 'output', inputPDF, + 'flatten' + ], check=True) + else: + inputPDF=filePath # The chosen page - pageCount=pdfCountPages(inputPDF()) + pageCount=pdfCountPages(inputPDF) if args.page < -pageCount or args.page==0 or pageCount < args.page: die('Page number out of range') pageNumber=Cell(args.page if 0 < args.page else pageCount+args.page+1) - pagePDF=Cell(lambda: subprocess.run(['pdftk', inputPDF(), 'cat', str(pageNumber()), 'output', intmp('page.pdf')], check=True) and intmp('page.pdf')) - pageSize=Cell(lambda: pdfGetSize(pagePDF())) + @Cell + def pagePDF(): + outFile=intmp('page.pdf') + subprocess.run([ + 'pdftk', inputPDF, + 'cat', str(pageNumber()), + 'output', str(outFile) + ], check=True) + return outFile + pageSize=Cell(lambda: pdfGetSize(str(pagePDF()))) # The chosen signature if not args.signature and args.batch: die('In batch mode, signature must be specified.') @@ -62,12 +68,15 @@ def main(args): ], check=True) return outFile # The signed page - signedPagePDF=Cell(lambda: subprocess.run([ - 'pdftk', - pagePDF(), - 'stamp', signaturePositionedPDF(), - 'output', intmp('signed-page.pdf'), - ], check=True) and intmp('signed-page.pdf')) + @Cell + def signedPagePDF(): + outFile=intmp('signed-page.pdf') + subprocess.run([ + 'pdftk', str(pagePDF()), + 'stamp', str(signaturePositionedPDF()), + 'output', str(outFile) + ], check=True) + return outFile # The signed page as PNG, for GUI use displayMaxSize=Cell((400, 800)) @Cell @@ -86,7 +95,7 @@ def main(args): '-sDEVICE=pngalpha', '-dMaxBitmap=2147483647', f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}', '-dFIXEDMEDIA', '-dPDFFitPage', - '-f', signedPagePDF(), + '-f', str(signedPagePDF()), ], check=True) return outFile # GUI @@ -240,7 +249,7 @@ def main(args): @tkthrottle(100, root) def update(): (w, h) = displaySize() - root._docImg = tk.PhotoImage(file=displayPNG()) + root._docImg = tk.PhotoImage(file=str(displayPNG())) root._docView.itemconfig(root._docViewIndex, image=root._docImg) root._docView.configure(width=w, height=h) updateTitle() @@ -276,7 +285,7 @@ def main(args): pnr=pageNumber() subprocess.run([ 'pdftk', - f'A={inputPDF()}', + f'A={inputPDF}', f'B={signedPagePDF()}', 'cat', *([f'A1-{pnr-1}'] if 1 < pnr else []), @@ -288,6 +297,12 @@ def main(args): else: print(f'Aborted') +# Used for file names that don't change but represents changed content +class Volatile(): + def __init__(self, underlying): self._underlying = underlying + def __eq__(self, other): return self is other + def __str__(self): return str(self._underlying) + def getSignatureDir(): if 'PDF_SIGNATURE_DIR' in os.environ: sd=os.environ['PDF_SIGNATURE_DIR'] @@ -308,33 +323,54 @@ def getSignatureDir(): # Init with a value or function to calculate the value. # Update by calling with one argument (as in init). # To retrieve an up-to-date value, call with no arguments. +# Calculations with unchanged inputs are skipped. class Cell(): currentCell=None def __init__(self, arg): self._arg=arg self._isuptodate=False + self._needEval=True self._dependents=[] + self._precedents=[] + self._precedentvalues=[] def __call__(self, *args): if(len(args)==1): - self._arg=args[0] - self.dirty() + if self._arg != args[0]: + self._arg=args[0] + self._needEval=True + self._dirty() return assert len(args)==0 - if(Cell.currentCell): - self._dependents.append(Cell.currentCell) if not self._isuptodate: oldcell=Cell.currentCell - Cell.currentCell=self - self._value=self._arg() if callable(self._arg) else self._arg + Cell.currentCell=None + for i in range(len(self._precedents)): + p=self._precedents[i] + oldval=self._precedentvalues[i] + newval=p() + if oldval!=newval: + self._needEval=True + break + if self._needEval: + Cell.currentCell=self + for p in self._precedents: + p._dependents.remove(self) + self._precedents=[] + self._precedentvalues=[] + self._value=self._arg() if callable(self._arg) else self._arg + self._needEval=False self._isuptodate=True Cell.currentCell=oldcell + if(Cell.currentCell): + self._dependents.append(Cell.currentCell) + Cell.currentCell._precedents.append(self) + Cell.currentCell._precedentvalues.append(self._value) return self._value - def dirty(self): + def _dirty(self): if self._isuptodate: self._isuptodate=False for d in self._dependents: - d.dirty() - self._dependents=[] + d._dirty() def tkthrottle(frequency, root): wait=1/frequency @@ -358,7 +394,7 @@ def tkthrottle(frequency, root): return decorator def pdfCountPages(filePath): - return int(fromCmdOutput(["pdfinfo", filePath], "^.*\nPages: +([0-9]+)\n.*$")[1]) + return int(fromCmdOutput(["pdfinfo", str(filePath)], "^.*\nPages: +([0-9]+)\n.*$")[1]) def pdfGetSize(filePath): [ignored, w, h, *ignored2]=fromCmdOutput(['pdfinfo', filePath], '^.*\nPage size: +([0-9.]+) x ([0-9.]+) pts( \([A-Za-z0-9]+\))?\n.*$') From 2e9395cee3845d0152a85c5d00065d4de930a76c Mon Sep 17 00:00:00 2001 From: Axel Svensson Date: Sun, 2 Jun 2024 02:29:57 +0200 Subject: [PATCH 6/6] Minimize empty-3inx2in.pdf --- empty-3inx2in.pdf | Bin 2190 -> 196 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/empty-3inx2in.pdf b/empty-3inx2in.pdf index 21cba6c4afaaa3933a938b8f56b81a3f458be77d..301d503a0e713b8e4fa547ebb89b866fc9047142 100644 GIT binary patch delta 132 zcmeAZJi;ib8sOrlYp7?=Wujo9ke`&LFqxZCS;WpxAvG@r$mKGI3ftIBZeSEqRsbmr zQixU1cg`=(D^V~+s5FEbu!B)n10h^el$e>5TBKlOqaT!?57YrNz|M|KRn^tsjSB$d C;3EzI literal 2190 zcmb_e&u*he7~eyC;okcwRYa;So*6KdpHn@IE`A^e$!tpk}?{U<&UaFGhno(u(s^WThC!33CqN(;NIW>JOYO zjMjN97zl1TZ${qyWO6<_o6j7bw(j{SgVFKHygs1KreY$3`1L!b+r_6WU5Fdh0?rhJ z2f=cLhlwwk#{wQQ*HsFUGk>LQ-oKuce~dYK)jRn0{omjG_4O-r^c7=iSPK9pjY?DU zts6?sX+F4{-)0O?`~|~f7RLS&-=Hf3$1+@q?5zTaIZFjHWIzg@vVxa+zzPU;lJj83 z#1#fa-zpFo?(wAKLmU?9N|6ywmA42il;Dg$Ql?!6Lnx?Cj1aiQ2U8%`CVgMCA$+;@T|NRGX2Z(=?->EJG$%E_;n9AnI9~f zK-X*$r;hsH?|-N$4juK4IVR(*&u-$AyPVDL&gQ}0GO$AR&{evJKETejjh|^zb$Da`DX_6Ot84}FqRlEv3wz_Jm5L3tWMdUoB4X*Nm*WW%oF~gzAiZHVs78(N zQIz{Dw&wW~(MC%JlX)1~Q*T%sgfBWf_O|wLv?k6&PA8Xa*Xk zk);uMG|OlM9Y)L#te4(L7~IE^m3dNCUKn7OFzK>@E1?ib7}ycdSH6JSf4~DnVW@5w z4n-^y=2msxOT?B)Vp(B!;-?Eo-Dn|;{4xj}TfS)>ruQDot4AJj5U!$c9<@tOvNC?AyWc(iT8?EjEQj5ZAQ zCp_J>KH|}4=OdopgoW`$w`su+-0?*2!`{we*CITN?-=SJJmnmgj$9lFkJ5-E2_UEBcL9+>MVbXr@NH@)vuDhR?sJ3Gv{9IHp(N5cfACJNLQ28JH Ccz34&