Commits vergleichen
17 Commits
Autor | SHA1 | Datum | |
---|---|---|---|
|
2d0ef9c085 | ||
|
8e83ba1a2d | ||
|
6f3e268672 | ||
|
bfab36ece4 | ||
|
b2531a89e4 | ||
|
fffac1c147 | ||
|
823c67bbee | ||
|
74ad2b009c | ||
|
d6f52c917e | ||
|
91cc247154 | ||
|
80ca4898ca | ||
|
939a6c96f8 | ||
|
89c808fb32 | ||
|
a8df5d398c | ||
|
2025f66620 | ||
|
4727a65f08 | ||
|
71b1a84970 |
3 geänderte Dateien mit 217 neuen und 50 gelöschten Zeilen
|
@ -29,7 +29,7 @@ Run `pdf-sign -h` or `pdf-create-empty -h` for details.
|
|||
* `python3.7` or later
|
||||
* Python module `tkinter` (only needed for interactive use)
|
||||
* `gs` (Ghostscript)
|
||||
* `pdftk`
|
||||
* `qpdf` or `pdftk` (at least one of them)
|
||||
* `pdfinfo`
|
||||
* Copy one or both tools to a directory in your `$PATH`.
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
#Dependencies: python3, gs
|
||||
# Dependencies:
|
||||
# - python3.7 or later
|
||||
# - gs (Ghostscript)
|
||||
|
||||
import argparse, os, re, subprocess, sys
|
||||
|
||||
|
|
261
pdf-sign
261
pdf-sign
|
@ -1,42 +1,69 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
#Dependencies: python3.7 or later with module tkinter, gs (Ghostscript), pdftk and pdfinfo.
|
||||
# Dependencies:
|
||||
# - python3.7 or later with module tkinter
|
||||
# - gs (Ghostscript)
|
||||
# - qpdf or pdftk (pdf-sign will use pdftk if qpdf cannot be found)
|
||||
# - pdfinfo.
|
||||
|
||||
import argparse, os, queue, re, subprocess, sys, tempfile, traceback, time
|
||||
|
||||
from inspect import currentframe
|
||||
starttime=time.time()
|
||||
def tdbg():
|
||||
print(f"========== DBG: line {currentframe().f_back.f_lineno}, time after start={time.time()-starttime:.3f} ==========", flush=True)
|
||||
|
||||
# Inspired by https://unix.stackexchange.com/a/141496
|
||||
def main(args):
|
||||
tdbg()
|
||||
if not hasQpdf and not has("pdftk"):
|
||||
die("Needs either qpdf or pdftk installed")
|
||||
if not has("gs"):
|
||||
die("Needs Ghostscript installed")
|
||||
if not has("pdfinfo"):
|
||||
die("Needs pdfinfo installed")
|
||||
filePath=args.input
|
||||
if not isPdfFilename(filePath):
|
||||
die("Input file must end with .pdf (case insensitive)")
|
||||
tdbg()
|
||||
with tempfile.TemporaryDirectory() as tempdir:
|
||||
tdbg()
|
||||
intmp=lambda fileName: Volatile(os.path.join(tempdir, fileName))
|
||||
# Maybe flatten (make forms non-editable) before signing
|
||||
if args.flatten:
|
||||
inputPDF=str(intmp('input.pdf'))
|
||||
subprocess.run([
|
||||
qpdfOrPdftk([
|
||||
'qpdf',
|
||||
'--flatten-annotations=all',
|
||||
'--generate-appearances',
|
||||
filePath, inputPDF,
|
||||
],[
|
||||
'pdftk', filePath,
|
||||
'output', inputPDF,
|
||||
'flatten'
|
||||
], check=True)
|
||||
])
|
||||
else:
|
||||
inputPDF=filePath
|
||||
tdbg()
|
||||
# The chosen page
|
||||
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)
|
||||
pageNumber=Cell(args.page if 0 < args.page else pageCount+args.page+1, 'pageNumber')
|
||||
@Cell
|
||||
def pagePDF():
|
||||
outFile=intmp('page.pdf')
|
||||
subprocess.run([
|
||||
qpdfOrPdftk([
|
||||
'qpdf', '--pages', '.', f'{pageNumber()}', '--',
|
||||
inputPDF, str(outFile)
|
||||
],[
|
||||
'pdftk', inputPDF,
|
||||
'cat', str(pageNumber()),
|
||||
'output', str(outFile)
|
||||
], check=True)
|
||||
'output', str(outFile)])
|
||||
return outFile
|
||||
pageSize=Cell(lambda: pdfGetSize(str(pagePDF())))
|
||||
pageSize=Cell(lambda: pdfGetSize(str(pagePDF())), 'pageSize')
|
||||
# The chosen signature
|
||||
tdbg()
|
||||
if not args.signature and args.batch:
|
||||
die('In batch mode, signature must be specified.')
|
||||
signatureDir=getSignatureDir()
|
||||
|
@ -44,12 +71,57 @@ def main(args):
|
|||
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()))
|
||||
signaturePositionX=Cell(args.x_coordinate)
|
||||
signaturePositionY=Cell(args.y_coordinate)
|
||||
signatureScale=Cell(0)
|
||||
signatureIndex=Cell(0, 'signatureIndex')
|
||||
signaturePath=Cell(lambda: args.signature if args.signature else os.path.join(signatureDir, signatures[signatureIndex()]), 'signaturePath')
|
||||
signatureSize=Cell(lambda: pdfGetSize(signaturePath()), 'signatureSize')
|
||||
signaturePositionX=Cell(args.x_coordinate, 'signaturePositionX')
|
||||
signaturePositionY=Cell(args.y_coordinate, 'signaturePositionY')
|
||||
signatureScale=Cell(0, 'signatureScale')
|
||||
tdbg()
|
||||
def translatablePDF(path):
|
||||
cache = translatablePDF._cache
|
||||
if path not in cache:
|
||||
(w, h) = pdfGetSize(path)
|
||||
(double_w, double_h) = (2*w, 2*h)
|
||||
testFile = os.path.join(tempdir, 'translateTest.pdf')
|
||||
subprocess.run([
|
||||
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||
f'-sOutputFile={testFile}',
|
||||
'-sDEVICE=pdfwrite',
|
||||
f'-dDEVICEWIDTHPOINTS={double_w}', f'-dDEVICEHEIGHTPOINTS={double_h}', '-dFIXEDMEDIA',
|
||||
'-c', f'<</BeginPage{{{w} {h} translate}}>> setpagedevice',
|
||||
'-f', path,
|
||||
], check=True)
|
||||
(test_w, test_h) = pdfGetSize(testFile)
|
||||
if (test_w, test_h) == (double_w, double_h):
|
||||
# The pdf file at path can be translated correctly
|
||||
cache[path] = path
|
||||
elif (test_w, test_h) == (w, h):
|
||||
# The pdf file at path cannot be translated correctly.
|
||||
# Rather, the size is unchanged. This can happen if the PDF
|
||||
# has an explicit CropBox set. We have to remove it to make
|
||||
# the PDF translatable and usable as a signature.
|
||||
translatableFileName = os.path.join(tempdir, f'translatable{len(cache)}.pdf')
|
||||
emptyFileName = os.path.join(tempdir, f'empty{len(cache)}.pdf')
|
||||
subprocess.run([
|
||||
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||
f'-sOutputFile={emptyFileName}',
|
||||
'-sDEVICE=pdfwrite',
|
||||
f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}',
|
||||
], check=True)
|
||||
qpdfOrPdftk([
|
||||
'qpdf', '--overlay', path, '--',
|
||||
emptyFileName, translatableFileName,
|
||||
],[
|
||||
'pdftk', emptyFileName,
|
||||
'stamp', path,
|
||||
'output', translatableFileName
|
||||
])
|
||||
cache[path] = translatableFileName
|
||||
else:
|
||||
die(f"The PDF at {path} is unusable as a signature. Reason unknown.")
|
||||
return cache[path]
|
||||
translatablePDF._cache={}
|
||||
@Cell
|
||||
def signaturePositionedPDF():
|
||||
(w, h)=pageSize()
|
||||
|
@ -64,21 +136,24 @@ def main(args):
|
|||
'-sDEVICE=pdfwrite',
|
||||
f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}', '-dFIXEDMEDIA',
|
||||
'-c', f'<</BeginPage{{{resize} {resize} scale {dx} {dy} translate}}>> setpagedevice',
|
||||
'-f', signaturePath(),
|
||||
'-f', translatablePDF(signaturePath()),
|
||||
], check=True)
|
||||
return outFile
|
||||
# The signed page
|
||||
@Cell
|
||||
def signedPagePDF():
|
||||
outFile=intmp('signed-page.pdf')
|
||||
subprocess.run([
|
||||
qpdfOrPdftk([
|
||||
'qpdf', '--overlay', str(signaturePositionedPDF()), '--',
|
||||
str(pagePDF()), str(outFile),
|
||||
],[
|
||||
'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))
|
||||
displayMaxSize=Cell((400, 800), 'displayMaxSize')
|
||||
@Cell
|
||||
def displaySize():
|
||||
(maxWidth, maxHeight)=displayMaxSize()
|
||||
|
@ -89,6 +164,7 @@ def main(args):
|
|||
def displayPNG():
|
||||
(w, h)=displaySize()
|
||||
outFile=intmp('display.png')
|
||||
tdbg()
|
||||
subprocess.run([
|
||||
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||
f'-sOutputFile={outFile}',
|
||||
|
@ -97,11 +173,13 @@ def main(args):
|
|||
f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}', '-dFIXEDMEDIA', '-dPDFFitPage',
|
||||
'-f', str(signedPagePDF()),
|
||||
], check=True)
|
||||
tdbg()
|
||||
return outFile
|
||||
# GUI
|
||||
doSign=True
|
||||
gui=not args.batch
|
||||
if gui:
|
||||
tdbg()
|
||||
try:
|
||||
import tkinter as tk
|
||||
except ModuleNotFoundError:
|
||||
|
@ -151,7 +229,9 @@ def main(args):
|
|||
sys.exit(1)
|
||||
tk.Tk.report_callback_exception = tkerror
|
||||
# Window and menu
|
||||
tdbg()
|
||||
root = tk.Tk(className="pdf-sign")
|
||||
tdbg()
|
||||
rootmenu = tk.Menu(root)
|
||||
root.config(menu=rootmenu)
|
||||
filemenu = tk.Menu(rootmenu, tearoff=0)
|
||||
|
@ -208,9 +288,13 @@ def main(args):
|
|||
'space': cmd_sign,
|
||||
}
|
||||
def onkey(event):
|
||||
print(f"Debug: in onkey(event): char={event.char}, delta={event.delta}, height={event.height}, keycode={event.keycode}, keysym={event.keysym}, keysym_num={event.keysym_num}, num={event.num}, send_event={event.send_event}, serial={event.serial}, state={event.state}, time={event.time}, type={event.type}, widget={event.widget}, width={event.width}, x={event.x}, x_root={event.x_root}, y={event.y}, y_root={event.y_root}")
|
||||
key=('C-' if event.state in [4, 5] else '')+event.keysym
|
||||
if key in keyToFunction:
|
||||
print(f"Debug: Calling function for {key}")
|
||||
keyToFunction[key]()
|
||||
else:
|
||||
print(f"Debug: No function for {key}")
|
||||
for key in keyToFunction.keys():
|
||||
root.bind(f'<{key.split("-")[-1]}>', onkey)
|
||||
def bindDigit(i, char):
|
||||
|
@ -218,54 +302,91 @@ def main(args):
|
|||
root.bind(f'{char}', onkey)
|
||||
for i, char in enumerate("123456789"): bindDigit(i, char)
|
||||
# Canvas and click binding
|
||||
root._docView=tk.Canvas(root, borderwidth=0, background='#ffffff', confine=True)
|
||||
def onDocViewResize(event):
|
||||
canvasMarginX=event.x
|
||||
canvasMarginY=event.y
|
||||
canvasWidth=event.width
|
||||
canvasHeight=event.height
|
||||
(oldMaxWidth, oldMaxHeight)=displayMaxSize()
|
||||
if(0<canvasMarginX and 0<canvasMarginY):
|
||||
apparentScale=max(canvasHeight/oldMaxHeight, canvasWidth/oldMaxWidth, 0.5)
|
||||
newMaxWidth=(canvasWidth+2*canvasMarginX)/apparentScale-10
|
||||
newMaxHeight=(canvasHeight+2*canvasMarginY)/apparentScale-10
|
||||
else:
|
||||
newMaxWidth=oldMaxWidth/2
|
||||
newMaxHeight=oldMaxHeight/2
|
||||
newMaxWidth=max(newMaxWidth, 10)
|
||||
newMaxHeight=max(newMaxHeight, 10)
|
||||
if abs(oldMaxWidth-newMaxWidth) < 5: newMaxWidth=oldMaxWidth
|
||||
if abs(oldMaxHeight-newMaxHeight) < 5: newMaxHeight=oldMaxHeight
|
||||
if oldMaxWidth != newMaxWidth or oldMaxHeight != newMaxHeight:
|
||||
displayMaxSize((newMaxWidth, newMaxHeight))
|
||||
tdbg()
|
||||
root._docView=tk.Canvas(root, borderwidth=0, background='#ffffff')
|
||||
tdbg()
|
||||
docViewMargin=5
|
||||
docViewMinDimension=50
|
||||
tdbg()
|
||||
def onRootResize(event):
|
||||
rootWidth=root.winfo_width()
|
||||
rootHeight=root.winfo_height()
|
||||
newMaxDisplayDimensions=(max(rootWidth - 2 * docViewMargin, docViewMinDimension),
|
||||
max(rootHeight - 2 * docViewMargin, docViewMinDimension))
|
||||
if displayMaxSize() != newMaxDisplayDimensions:
|
||||
displayMaxSize(newMaxDisplayDimensions)
|
||||
update()
|
||||
root._docView.pack(expand=1)
|
||||
root._docView.place(x=docViewMargin, y=docViewMargin)
|
||||
root._docViewIndex=root._docView.create_image(0, 0, anchor=tk.NW)
|
||||
root._docView.bind('<Configure>', onDocViewResize)
|
||||
root.geometry("800x600")
|
||||
root.bind('<Configure>', onRootResize)
|
||||
initWinSize=(root.winfo_screenwidth() * 0.8, root.winfo_screenheight() * 0.8)
|
||||
initWinSize=(min(initWinSize[0], initWinSize[1] * pageSize()[0] / pageSize()[1]),
|
||||
min(initWinSize[1], initWinSize[0] * pageSize()[1] / pageSize()[0]))
|
||||
root.geometry(f"{int(initWinSize[0])}x{int(initWinSize[1])}")
|
||||
tdbg()
|
||||
@Cell
|
||||
def updateTitle():
|
||||
root.title(f'Signing page {pageNumber()}/{pageCount} of {filePath}')
|
||||
# The update function triggers heavy PDF file operations, so we try
|
||||
# to avoid calling it too much. In particular,
|
||||
# 1) Depending on desktop environment and window manager, one or
|
||||
# more resizes can happen soon after startup, triggering an
|
||||
# update. We use the updateActive flag to avoid these, then
|
||||
# instead update once 100 ms after startup.
|
||||
# 2) An interactive resizing process using the pointer can produce a
|
||||
# lot of resizing events. We use the @tkthrottle decorator to
|
||||
# reduce them.
|
||||
updateActive = False
|
||||
def soonAfterStart():
|
||||
nonlocal updateActive
|
||||
updateActive = True
|
||||
update()
|
||||
root.after(100, soonAfterStart)
|
||||
tdbg()
|
||||
@tkthrottle(100, root)
|
||||
def update():
|
||||
tdbg()
|
||||
if not updateActive:
|
||||
return
|
||||
(w, h) = displaySize()
|
||||
root._docImg = tk.PhotoImage(file=str(displayPNG()))
|
||||
filename = str(displayPNG())
|
||||
before = time.time()
|
||||
tdbg()
|
||||
root._docImg = tk.PhotoImage(file=filename)
|
||||
tdbg()
|
||||
print(f"Debug: Loading photo image took {time.time()-before:.2f} seconds", flush=True)
|
||||
root._docView.itemconfig(root._docViewIndex, image=root._docImg)
|
||||
root._docView.configure(width=w, height=h)
|
||||
updateTitle()
|
||||
if not args.signature:
|
||||
if root.signatureControlVar.get() != signatureIndex():
|
||||
root.signatureControlVar.set(signatureIndex())
|
||||
tdbg()
|
||||
def dbg(event, x):
|
||||
print(f"Debug: in dbg(event) for {x}: char={event.char}, delta={event.delta}, height={event.height}, keycode={event.keycode}, keysym={event.keysym}, keysym_num={event.keysym_num}, num={event.num}, send_event={event.send_event}, serial={event.serial}, state={event.state}, time={event.time}, type={event.type}, widget={event.widget}, width={event.width}, x={event.x}, x_root={event.x_root}, y={event.y}, y_root={event.y_root}")
|
||||
def dbgfun(x):
|
||||
def fun(event):
|
||||
dbg(event, x)
|
||||
return fun
|
||||
root._docView.bind('<KeyPress>', dbgfun("root._docView <KeyPress>"))
|
||||
root._docView.bind('<Button>', dbgfun("root._docView <Button>"))
|
||||
root.bind('<KeyPress>', dbgfun("root <KeyPress>"))
|
||||
root.bind('<Button>', dbgfun("root <Button>"))
|
||||
tdbg()
|
||||
def onclick(event):
|
||||
print(f"Debug: in onclick(event): char={event.char}, delta={event.delta}, height={event.height}, keycode={event.keycode}, keysym={event.keysym}, keysym_num={event.keysym_num}, num={event.num}, send_event={event.send_event}, serial={event.serial}, state={event.state}, time={event.time}, type={event.type}, widget={event.widget}, width={event.width}, x={event.x}, x_root={event.x_root}, y={event.y}, y_root={event.y_root}")
|
||||
x=event.x
|
||||
y=event.y
|
||||
canvasConfig=root._docView.config()
|
||||
canvasWidth=int(canvasConfig['width'][4])
|
||||
canvasHeight=int(canvasConfig['height'][4])
|
||||
print(f"Debug: in onclick(event): canvasConfig={repr(canvasConfig)}, canvasWidth={canvasWidth}, canvasHeight={canvasHeight}, calling cmd_positionSignature({x/canvasWidth}, {y/canvasHeight})")
|
||||
cmd_positionSignature(x/canvasWidth, y/canvasHeight)
|
||||
root._docView.bind('<Button-1>', onclick)
|
||||
# Run GUI
|
||||
tdbg()
|
||||
root.mainloop()
|
||||
tdbg()
|
||||
# End of GUI
|
||||
if doSign:
|
||||
signedFilePath=args.output if args.output else f'{filePath[:-4]}.signed{filePath[-4:]}'
|
||||
|
@ -283,7 +404,15 @@ def main(args):
|
|||
else:
|
||||
assert args.existing=='overwrite'
|
||||
pnr=pageNumber()
|
||||
subprocess.run([
|
||||
qpdfOrPdftk([
|
||||
'qpdf', '--pages',
|
||||
*(['.', f'1-{pnr-1}'] if 1 < pnr else []),
|
||||
str(signedPagePDF()), '1',
|
||||
*(['.', f'{pnr+1}-z'] if pnr < pageCount else []),
|
||||
'--',
|
||||
inputPDF,
|
||||
signedFilePath,
|
||||
],[
|
||||
'pdftk',
|
||||
f'A={inputPDF}',
|
||||
f'B={signedPagePDF()}',
|
||||
|
@ -292,7 +421,7 @@ def main(args):
|
|||
'B',
|
||||
*([f'A{pnr+1}-end'] if pnr < pageCount else []),
|
||||
'output', signedFilePath,
|
||||
], check=True)
|
||||
])
|
||||
print(f'Signed document saved as {signedFilePath}')
|
||||
else:
|
||||
print(f'Aborted')
|
||||
|
@ -303,6 +432,15 @@ class Volatile():
|
|||
def __eq__(self, other): return self is other
|
||||
def __str__(self): return str(self._underlying)
|
||||
|
||||
def has(cmd):
|
||||
return subprocess.run(["which", cmd], check=False, capture_output=True).returncode == 0
|
||||
hasQpdf = has("qpdf")
|
||||
def qpdfOrPdftk(qpdfCmd, pdftkCmd):
|
||||
assert qpdfCmd[0] == "qpdf" and pdftkCmd[0] == "pdftk"
|
||||
cmd = qpdfCmd if hasQpdf else pdftkCmd
|
||||
subprocess.run(cmd, check=True)
|
||||
return True # Some lambdas above rely on this
|
||||
|
||||
def getSignatureDir():
|
||||
if 'PDF_SIGNATURE_DIR' in os.environ:
|
||||
sd=os.environ['PDF_SIGNATURE_DIR']
|
||||
|
@ -326,22 +464,32 @@ def getSignatureDir():
|
|||
# Calculations with unchanged inputs are skipped.
|
||||
class Cell():
|
||||
currentCell=None
|
||||
def __init__(self, arg):
|
||||
def __init__(self, arg, dbgname=None):
|
||||
self._dbgname=dbgname
|
||||
if dbgname==None and callable(arg): self._dbgname=arg.__name__
|
||||
self._arg=arg
|
||||
self._isuptodate=False
|
||||
self._needEval=True
|
||||
self._dependents=[]
|
||||
self._precedents=[]
|
||||
self._precedentvalues=[]
|
||||
print(f"Debug: Init cell {dbgname}{repr(arg)}")
|
||||
def __call__(self, *args):
|
||||
if(len(args)==1):
|
||||
if self._arg != args[0]:
|
||||
print(f"{Cell._dbgprefix}Debug: Called cell {self._dbgname}{repr(args)}, changing value from {self._arg} to {args[0]}")
|
||||
self._arg=args[0]
|
||||
self._needEval=True
|
||||
self._dirty()
|
||||
else:
|
||||
print(f"Debug: Called cell {self._dbgname}{repr(args)}, not changing value.")
|
||||
return
|
||||
assert len(args)==0
|
||||
print(f"{Cell._dbgprefix}Debug: Called cell {self._dbgname}{repr(args)}...")
|
||||
dbgtext=f"{Cell._dbgprefix}.."
|
||||
Cell._dbgprefix+=' '
|
||||
if not self._isuptodate:
|
||||
dbgtext+=", updating it"
|
||||
oldcell=Cell.currentCell
|
||||
Cell.currentCell=None
|
||||
for i in range(len(self._precedents)):
|
||||
|
@ -349,9 +497,11 @@ class Cell():
|
|||
oldval=self._precedentvalues[i]
|
||||
newval=p()
|
||||
if oldval!=newval:
|
||||
dbgtext+=f", detected change in precedent {p._dbgname}"
|
||||
self._needEval=True
|
||||
break
|
||||
if self._needEval:
|
||||
dbgtext+=", evaluating"
|
||||
Cell.currentCell=self
|
||||
for p in self._precedents:
|
||||
p._dependents.remove(self)
|
||||
|
@ -365,12 +515,16 @@ class Cell():
|
|||
self._dependents.append(Cell.currentCell)
|
||||
Cell.currentCell._precedents.append(self)
|
||||
Cell.currentCell._precedentvalues.append(self._value)
|
||||
dbgtext+=f", returning {repr(self._value)}"
|
||||
Cell._dbgprefix=Cell._dbgprefix[:-2]
|
||||
print(dbgtext)
|
||||
return self._value
|
||||
def _dirty(self):
|
||||
if self._isuptodate:
|
||||
self._isuptodate=False
|
||||
for d in self._dependents:
|
||||
d._dirty()
|
||||
Cell._dbgprefix=''
|
||||
|
||||
def tkthrottle(frequency, root):
|
||||
wait=1/frequency
|
||||
|
@ -397,7 +551,7 @@ def pdfCountPages(filePath):
|
|||
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.*$')
|
||||
[ignored, w, h, *ignored2]=fromCmdOutput(['pdfinfo', filePath], '^.*\nPage size: +([0-9.]+) x ([0-9.]+) pts( \\([A-Za-z0-9]+\\))?\n.*$')
|
||||
return (float(w), float(h))
|
||||
|
||||
def m(pattern, string):
|
||||
|
@ -452,8 +606,19 @@ parser.add_argument('-y', '--y-coordinate', type=float, default=0.75, help='Vert
|
|||
parser.add_argument('-W', '--width', type=float, default=0.28, help='Width of box to fit signature to, in page width units. (default: 0.28)')
|
||||
parser.add_argument('-H', '--height', type=float, default=0.28, help='Height of box to fit signature to, in page height units. (default: 0.28)')
|
||||
parser.add_argument('-o', '--output', type=str, help='Output file. (default: Add ".signed" before the extension)')
|
||||
parser.add_argument('-b', '--batch', action=argparse.BooleanOptionalAction, default=False, help='Batch mode: do not show GUI.')
|
||||
parser.add_argument('-f', '--flatten', action=argparse.BooleanOptionalAction, default=True, help='Flatten before signing.')
|
||||
parser.add_argument('-b', '--batch', action=argparse.BooleanOptionalAction, default=False, help='Batch mode: do not show GUI. (default: False)')
|
||||
parser.add_argument('-f', '--flatten', action=argparse.BooleanOptionalAction, default=True, help='Flatten before signing, preventing subsequent changes in PDF forms. (default: True)')
|
||||
parser.add_argument('-e', '--existing', type=str, choices=['backup', 'overwrite', 'fail'], default='backup', help='What to do if output file exists. (default: backup)')
|
||||
|
||||
# debug subprocess runs
|
||||
orig_subprocess_run = subprocess.run
|
||||
def debug_subprocess_run(*args, **kwargs):
|
||||
before=time.time()
|
||||
ret = orig_subprocess_run(*args, **kwargs)
|
||||
after=time.time()
|
||||
duration=after-before
|
||||
print(f"{Cell._dbgprefix}Subprocess [{duration:.2f}s]: {repr(args)} {repr(kwargs)}", flush=True)
|
||||
return ret
|
||||
subprocess.run = debug_subprocess_run
|
||||
|
||||
main(parser.parse_args(sys.argv[1:] or ['-h']))
|
||||
|
|
Laden …
Tabelle hinzufügen
In neuem Issue referenzieren