Commits vergleichen
24 Commits
Autor | SHA1 | Datum | |
---|---|---|---|
4d40254f62 | |||
|
48cbf7830a | ||
|
f63c07f37b | ||
|
8a6c7d5f2f | ||
|
7b5a699060 | ||
|
b3a9474e4e | ||
|
9c7ba6da50 | ||
|
bcc37b0929 | ||
|
17ccf8b527 | ||
|
6c373e3df2 | ||
|
1a69e20946 | ||
|
9e0ef1a65c | ||
|
21aad7d1b9 | ||
|
ed54c79aa7 | ||
|
c0dfb3ec4c | ||
|
c61747cf7a | ||
|
3eed74e1ef | ||
|
0a0c7c9e13 | ||
|
0fbba14076 | ||
|
17ec3c5e56 | ||
|
a3d25a566f | ||
|
34fdeb1043 | ||
|
8f299dda2e | ||
|
6b80860a9b |
10 geänderte Dateien mit 530 neuen und 163 gelöschten Zeilen
23
.SRCINFO
Normale Datei
23
.SRCINFO
Normale Datei
|
@ -0,0 +1,23 @@
|
||||||
|
pkgbase = pdf-sign
|
||||||
|
pkgdesc = Simple tool to sign PDFs using an saved signature
|
||||||
|
pkgver = 1.0
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://github.com/svenssonaxel/pdf-sign
|
||||||
|
arch = x86_64
|
||||||
|
license = MIT
|
||||||
|
depends = ghostscript
|
||||||
|
depends = python
|
||||||
|
depends = qpdf
|
||||||
|
depends = poppler
|
||||||
|
optdepends = tk: for interactive usage
|
||||||
|
optdepends = pdftk: an alterative tool to process PDF for qpdf
|
||||||
|
source = pdf-create-empty
|
||||||
|
source = pdf-from-text
|
||||||
|
source = pdf-sign
|
||||||
|
source = empty-3inx2in.pdf
|
||||||
|
sha256sums = d6b3bd23a3fcf28561e152690332738babf5e71afde40e5727c643a5805ae76b
|
||||||
|
sha256sums = 4a509aa3e787c3e617f151bcd25dd93adf9af3af477562c64988f7784395d40d
|
||||||
|
sha256sums = 7b52792ead60257c57f4a12d727a882936f24b9cb3ae57d73beb90cdd7960707
|
||||||
|
sha256sums = a2fd2476c1d8364853c2382a8090d8fbc2f11fb4a535714e27a3069ce009eef3
|
||||||
|
|
||||||
|
pkgname = pdf-sign
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright © 2021 Axel Svensson
|
Copyright © 2021-2024 Axel Svensson
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
|
39
PKGBUILD
Normale Datei
39
PKGBUILD
Normale Datei
|
@ -0,0 +1,39 @@
|
||||||
|
# Maintainer: Sebastian Tobie <archlinux@sebastian-tobie.de>
|
||||||
|
# based on the PKGBUILD of sulis
|
||||||
|
|
||||||
|
pkgname=pdf-sign
|
||||||
|
pkgver=1.0
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc='Simple tool to sign PDFs using an saved signature'
|
||||||
|
arch=('x86_64')
|
||||||
|
url='https://github.com/svenssonaxel/pdf-sign'
|
||||||
|
license=('MIT')
|
||||||
|
depends=(
|
||||||
|
'ghostscript'
|
||||||
|
'python'
|
||||||
|
'qpdf'
|
||||||
|
'poppler'
|
||||||
|
)
|
||||||
|
optdepends=(
|
||||||
|
'tk: for interactive usage'
|
||||||
|
'pdftk: an alterative tool to process PDF for qpdf'
|
||||||
|
)
|
||||||
|
source=(
|
||||||
|
'pdf-create-empty'
|
||||||
|
'pdf-from-text'
|
||||||
|
'pdf-sign'
|
||||||
|
'empty-3inx2in.pdf'
|
||||||
|
)
|
||||||
|
sha256sums=(
|
||||||
|
'd6b3bd23a3fcf28561e152690332738babf5e71afde40e5727c643a5805ae76b'
|
||||||
|
'4a509aa3e787c3e617f151bcd25dd93adf9af3af477562c64988f7784395d40d'
|
||||||
|
'7b52792ead60257c57f4a12d727a882936f24b9cb3ae57d73beb90cdd7960707'
|
||||||
|
'a2fd2476c1d8364853c2382a8090d8fbc2f11fb4a535714e27a3069ce009eef3'
|
||||||
|
)
|
||||||
|
|
||||||
|
package() {
|
||||||
|
install -Dm755 -T pdf-create-empty "${pkgdir}/usr/bin/pdf-create-empty"
|
||||||
|
install -Dm755 -T pdf-from-text "${pkgdir}/usr/bin/pdf-from-text"
|
||||||
|
install -Dm755 -T pdf-sign "${pkgdir}/usr/bin/pdf-sign"
|
||||||
|
install -Dm644 -T empty-3inx2in.pdf "${pkgdir}/usr/share/doc/pdf-sign/examples/empty-3inx2in.pdf"
|
||||||
|
}
|
BIN
README-example-signature.gif
Normale Datei
BIN
README-example-signature.gif
Normale Datei
Binäre Datei nicht angezeigt.
Nachher Breite: | Höhe: | Größe: 22 KiB |
Vorher Breite: | Höhe: | Größe: 342 KiB Nachher Breite: | Höhe: | Größe: 342 KiB |
27
README.md
27
README.md
|
@ -5,7 +5,7 @@
|
||||||
A tool to sign PDF files, with Linux support.
|
A tool to sign PDF files, with Linux support.
|
||||||
We are here referring to the visible, non-cryptographic squiggles.
|
We are here referring to the visible, non-cryptographic squiggles.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## How
|
## How
|
||||||
|
|
||||||
|
@ -16,12 +16,18 @@ The recommended way is:
|
||||||
* Use an application of your choice to sign it.
|
* Use an application of your choice to sign it.
|
||||||
You can for example use Okular's Freehand Line, or transfer it to your smartphone and use Adobe Acrobat Reader.
|
You can for example use Okular's Freehand Line, or transfer it to your smartphone and use Adobe Acrobat Reader.
|
||||||
Keep in mind that it's the center of this mini-page that will be used for positioning the signature.
|
Keep in mind that it's the center of this mini-page that will be used for positioning the signature.
|
||||||
* Put the signed file in `~/.pdf_signatures/`.
|
|
||||||
|
<img src="README-example-signature.gif" width="250"/>
|
||||||
|
|
||||||
|
It's a good idea to write your signature on an imagined line through the center of the mini-page.
|
||||||
|
That way, it can be positioned correctly by clicking on the signature line.
|
||||||
|
* Put the signed file in your signature directory.
|
||||||
|
The signature directory is `$PDF_SIGNATURE_DIR`, `$XDG_CONFIG_HOME/pdf_signatures`, `$HOME/.config/pdf_signatures` or `$HOME/.pdf_signatures/`; the first one that exists. Use `pdf-sign -h` to confirm which one will be used on your system.
|
||||||
|
|
||||||
You can now use the `pdf-sign` tool interactively (or non-interactively) to sign PDF files.
|
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.
|
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.
|
Run `pdf-sign -h`, `pdf-create-empty -h` or `pdf-from-text -h` for details.
|
||||||
|
|
||||||
**Installation**
|
**Installation**
|
||||||
|
|
||||||
|
@ -31,7 +37,7 @@ Run `pdf-sign -h` or `pdf-create-empty -h` for details.
|
||||||
* `gs` (Ghostscript)
|
* `gs` (Ghostscript)
|
||||||
* `qpdf` or `pdftk` (at least one of them)
|
* `qpdf` or `pdftk` (at least one of them)
|
||||||
* `pdfinfo`
|
* `pdfinfo`
|
||||||
* Copy one or both tools to a directory in your `$PATH`.
|
* Copy the tools to a directory in your `$PATH`.
|
||||||
|
|
||||||
**Installation on Debian**
|
**Installation on Debian**
|
||||||
|
|
||||||
|
@ -40,9 +46,20 @@ apt-get update
|
||||||
apt-get install -y coreutils git python3 python3-tk ghostscript pdftk poppler-utils
|
apt-get install -y coreutils git python3 python3-tk ghostscript pdftk poppler-utils
|
||||||
git clone https://github.com/svenssonaxel/pdf-sign.git
|
git clone https://github.com/svenssonaxel/pdf-sign.git
|
||||||
cd pdf-sign
|
cd pdf-sign
|
||||||
cp pdf-sign pdf-create-empty /usr/local/bin/
|
cp pdf-* /usr/local/bin/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Related use cases
|
||||||
|
|
||||||
|
* You can add the date or other pieces of text using the `--text` CLI option or `Signature -> Custom text` menu option.
|
||||||
|
If you want both a date and a signature, you have to invoke `pdf-sign` twice.
|
||||||
|
* You can convert SVG stamps/marks and add them to your signature directory. Example:
|
||||||
|
```
|
||||||
|
curl -LO https://www.svgrepo.com/download/438371/checkmark-round.svg
|
||||||
|
sudo apt-get install librsvg2-bin
|
||||||
|
rsvg-convert -f pdf -o ~/.config/pdf_signatures/check.pdf checkmark-round.svg
|
||||||
|
```
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
There appears to be a lack of applications that run on Linux and allow for attaching free-hand signatures to PDF files in a good way.
|
There appears to be a lack of applications that run on Linux and allow for attaching free-hand signatures to PDF files in a good way.
|
||||||
|
|
Binäre Datei nicht angezeigt.
|
@ -44,6 +44,10 @@ def die(reason):
|
||||||
print(reason, file=sys.stderr)
|
print(reason, file=sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
pyver = sys.version_info
|
||||||
|
if not (pyver.major == 3 and pyver.minor >= 7):
|
||||||
|
die("Requires python 3.7 or later")
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Create an empty, transparent PDF file.')
|
parser = argparse.ArgumentParser(description='Create an empty, transparent PDF file.')
|
||||||
parser.add_argument('-o', '--output', metavar='FILE', type=str, help='Output PDF file. (default: empty-DIMENSIONS.pdf)')
|
parser.add_argument('-o', '--output', metavar='FILE', type=str, help='Output PDF file. (default: empty-DIMENSIONS.pdf)')
|
||||||
parser.add_argument('-d', '--dimensions', type=str, default='3inx2in', help='The page dimensions of the file to create. Supports units pts, in, cm, and mm. (default: 3inx2in)')
|
parser.add_argument('-d', '--dimensions', type=str, default='3inx2in', help='The page dimensions of the file to create. Supports units pts, in, cm, and mm. (default: 3inx2in)')
|
||||||
|
|
102
pdf-from-text
Ausführbare Datei
102
pdf-from-text
Ausführbare Datei
|
@ -0,0 +1,102 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
#Dependencies: python3.7 or later and gs (Ghostscript)
|
||||||
|
|
||||||
|
import argparse, os, re, subprocess, sys, tempfile
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Create a correctly cropped PDF from one line of text. Only Latin-1 character set is supported.")
|
||||||
|
parser.add_argument("-t", "--text", required=True, help="Text to be converted to PDF.")
|
||||||
|
parser.add_argument("-o", "--output", required=True, help="Output PDF file.")
|
||||||
|
parser.add_argument("-s", "--size", type=int, default=12, help="Font size in points (default 12).")
|
||||||
|
parser.add_argument("-m", "--margin", type=int, default=1, help="Margin in points (default 1).")
|
||||||
|
args = parser.parse_args()
|
||||||
|
text_to_pdf(args.text, args.output, args.size, args.margin)
|
||||||
|
|
||||||
|
# Keep this function in sync with pdf-sign
|
||||||
|
def text_to_pdf(text, output, size, margin):
|
||||||
|
# Validate text
|
||||||
|
text_chars=set(map(ord, text))
|
||||||
|
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
||||||
|
if not text_chars.issubset(latin1_chars):
|
||||||
|
die(f"Error: Only non-control latin-1 characters are supported. Unsupported characters: {', '.join(map(hex, text_chars - latin1_chars))}")
|
||||||
|
text.encode('latin-1').decode('latin-1') # Assertion. E.i., an exception here indicates a bug.
|
||||||
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
|
# Write postscript file
|
||||||
|
ps_file=os.path.join(tempdir, "file.ps")
|
||||||
|
text_len=len(text)
|
||||||
|
w=text_len * size + margin * 2
|
||||||
|
h=size * 3 + margin * 2
|
||||||
|
x=size + margin
|
||||||
|
y=size + margin
|
||||||
|
ps_text = (text
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("(", "\\(")
|
||||||
|
.replace(")", "\\)")
|
||||||
|
)
|
||||||
|
def write_ps():
|
||||||
|
with open(ps_file, "w", encoding="latin-1") as f:
|
||||||
|
f.write('\n'.join([
|
||||||
|
"%!PS-Adobe-3.0",
|
||||||
|
f"%%BoundingBox: 0 0 {w} {h}",
|
||||||
|
"%%Pages: 1",
|
||||||
|
"%%EndComments",
|
||||||
|
"%%Page: 1 1",
|
||||||
|
f"/DejaVuSansMono findfont {size} scalefont setfont",
|
||||||
|
f"{x} {y} moveto",
|
||||||
|
f"({ps_text}) show",
|
||||||
|
"showpage"]))
|
||||||
|
# Write postscript file with too big bounding box
|
||||||
|
write_ps()
|
||||||
|
# Get correct bounding box
|
||||||
|
bbox_result = subprocess.run([
|
||||||
|
"gs", '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||||
|
"-sDEVICE=bbox",
|
||||||
|
ps_file
|
||||||
|
], stderr=subprocess.PIPE, text=True, check=True)
|
||||||
|
bbox = m(r'.*%%HiResBoundingBox: (\d+(\.\d+)?) (\d+(\.\d+)?) (\d+(\.\d+)?) (\d+(\.\d+)?)\n.*', bbox_result.stderr)
|
||||||
|
if not bbox:
|
||||||
|
die("Error: Unable to extract bounding box.")
|
||||||
|
# Adjust variables for bounding box
|
||||||
|
llx, lly, urx, ury = float(bbox[1]), float(bbox[3]), float(bbox[5]), float(bbox[7])
|
||||||
|
llx, lly, urx, ury = llx - margin, lly - margin, urx + margin, ury + margin
|
||||||
|
w=urx - llx
|
||||||
|
h=ury - lly
|
||||||
|
x-=llx
|
||||||
|
y-=lly
|
||||||
|
# Write postscript file with correct bounding box
|
||||||
|
write_ps()
|
||||||
|
# Convert to PDF
|
||||||
|
gs_cmd = [
|
||||||
|
"gs", '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||||
|
"-o", output,
|
||||||
|
"-sDEVICE=pdfwrite",
|
||||||
|
f"-dDEVICEWIDTHPOINTS={w}",
|
||||||
|
f"-dDEVICEHEIGHTPOINTS={h}",
|
||||||
|
"-dFIXEDMEDIA",
|
||||||
|
"-c", "[ /PAGES pdfmark",
|
||||||
|
"-f", ps_file
|
||||||
|
]
|
||||||
|
subprocess.run(gs_cmd, check=True, stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
|
def m(pattern, string):
|
||||||
|
match = re.match(pattern, string, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
if(match.group(0) != string):
|
||||||
|
die(f'Pattern /${pattern}/ matched only part of string')
|
||||||
|
ret = []
|
||||||
|
for index in range((match.lastindex or 0)+1):
|
||||||
|
ret.append(match.group(index))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def die(reason):
|
||||||
|
print(reason, file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
pyver = sys.version_info
|
||||||
|
if not (pyver.major == 3 and pyver.minor >= 7):
|
||||||
|
die("Requires python 3.7 or later")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
496
pdf-sign
496
pdf-sign
|
@ -8,14 +8,16 @@
|
||||||
|
|
||||||
import argparse, os, queue, re, subprocess, sys, tempfile, traceback, time
|
import argparse, os, queue, re, subprocess, sys, tempfile, traceback, time
|
||||||
|
|
||||||
from inspect import currentframe
|
def die(reason):
|
||||||
starttime=time.time()
|
print(reason, file=sys.stderr)
|
||||||
def tdbg():
|
sys.exit(1)
|
||||||
print(f"========== DBG: line {currentframe().f_back.f_lineno}, time after start={time.time()-starttime:.3f} ==========", flush=True)
|
|
||||||
|
pyver = sys.version_info
|
||||||
|
if not (pyver.major == 3 and pyver.minor >= 7):
|
||||||
|
die("Requires python 3.7 or later")
|
||||||
|
|
||||||
# Inspired by https://unix.stackexchange.com/a/141496
|
# Inspired by https://unix.stackexchange.com/a/141496
|
||||||
def main(args):
|
def main(args):
|
||||||
tdbg()
|
|
||||||
if not hasQpdf and not has("pdftk"):
|
if not hasQpdf and not has("pdftk"):
|
||||||
die("Needs either qpdf or pdftk installed")
|
die("Needs either qpdf or pdftk installed")
|
||||||
if not has("gs"):
|
if not has("gs"):
|
||||||
|
@ -25,14 +27,15 @@ def main(args):
|
||||||
filePath=args.input
|
filePath=args.input
|
||||||
if not isPdfFilename(filePath):
|
if not isPdfFilename(filePath):
|
||||||
die("Input file must end with .pdf (case insensitive)")
|
die("Input file must end with .pdf (case insensitive)")
|
||||||
tdbg()
|
if filePath.startswith('-'):
|
||||||
|
# A cheap solution to a rare problem
|
||||||
|
die("Input file may not start with a dash (-)")
|
||||||
with tempfile.TemporaryDirectory() as tempdir:
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
tdbg()
|
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
|
# Maybe flatten (make forms non-editable) before signing
|
||||||
if args.flatten:
|
if args.flatten:
|
||||||
inputPDF=str(intmp('input.pdf'))
|
inputPDF=intmp('input.pdf')
|
||||||
qpdfOrPdftk([
|
qpdfOrPdftk(args, [
|
||||||
'qpdf',
|
'qpdf',
|
||||||
'--flatten-annotations=all',
|
'--flatten-annotations=all',
|
||||||
'--generate-appearances',
|
'--generate-appearances',
|
||||||
|
@ -44,46 +47,93 @@ def main(args):
|
||||||
])
|
])
|
||||||
else:
|
else:
|
||||||
inputPDF=filePath
|
inputPDF=filePath
|
||||||
tdbg()
|
|
||||||
# The chosen page
|
# The chosen page
|
||||||
pageCount=pdfCountPages(inputPDF)
|
pageCount=pdfCountPages(inputPDF)
|
||||||
if args.page < -pageCount or args.page==0 or pageCount < args.page:
|
if args.page < -pageCount or args.page==0 or pageCount < args.page:
|
||||||
die('Page number out of range')
|
die('Page number out of range')
|
||||||
pageNumber=Cell(args.page if 0 < args.page else pageCount+args.page+1, 'pageNumber')
|
pageNumber=Cell(args.page if 0 < args.page else pageCount+args.page+1)
|
||||||
@Cell
|
@VolatileCell
|
||||||
def pagePDF():
|
def pagePDF():
|
||||||
outFile=intmp('page.pdf')
|
outFile=intmp('page.pdf')
|
||||||
qpdfOrPdftk([
|
qpdfOrPdftk(args, [
|
||||||
'qpdf', '--pages', '.', f'{pageNumber()}', '--',
|
'qpdf', '--pages', '.', f'{pageNumber()}', '--',
|
||||||
inputPDF, str(outFile)
|
inputPDF, outFile
|
||||||
],[
|
],[
|
||||||
'pdftk', inputPDF,
|
'pdftk', inputPDF,
|
||||||
'cat', str(pageNumber()),
|
'cat', str(pageNumber()),
|
||||||
'output', str(outFile)])
|
'output', outFile])
|
||||||
return outFile
|
return outFile
|
||||||
pageSize=Cell(lambda: pdfGetSize(str(pagePDF())), 'pageSize')
|
pageSize=Cell(lambda: pdfGetSize(pagePDF()))
|
||||||
# The chosen signature
|
# The chosen signature
|
||||||
tdbg()
|
if args.signature:
|
||||||
if not args.signature and args.batch:
|
if args.text:
|
||||||
die('In batch mode, signature must be specified.')
|
die('--signature and --text cannot be specified together.')
|
||||||
signatureDir=getSignatureDir()
|
if not os.path.exists(args.signature):
|
||||||
signatures=[*filter(isPdfFilename, os.listdir(signatureDir))] if not args.signature else [None]
|
die(f'File not found: {args.signature}')
|
||||||
if not signatures:
|
signatures=[('file', args.signature, args.signature)]
|
||||||
die(f'No .pdf files found in {signatureDir}')
|
elif args.batch:
|
||||||
signatures.sort()
|
assert not args.signature
|
||||||
signatureIndex=Cell(0, 'signatureIndex')
|
if not args.text:
|
||||||
signaturePath=Cell(lambda: args.signature if args.signature else os.path.join(signatureDir, signatures[signatureIndex()]), 'signaturePath')
|
die('In batch mode, --signature or --text must be specified.')
|
||||||
signatureSize=Cell(lambda: pdfGetSize(signaturePath()), 'signatureSize')
|
if len(args.text) > 1:
|
||||||
signaturePositionX=Cell(args.x_coordinate, 'signaturePositionX')
|
die('In batch mode, --text must be given only once.')
|
||||||
signaturePositionY=Cell(args.y_coordinate, 'signaturePositionY')
|
validateText(args.text[0], True)
|
||||||
signatureScale=Cell(0, 'signatureScale')
|
signatures=[('text', textLabel(args.text[0]), args.text[0])]
|
||||||
tdbg()
|
else:
|
||||||
|
(signatureDir, signatureDirHelp)=getSignatureDirAndHelp()
|
||||||
|
signatures=([('file', x, os.path.join(signatureDir, x))
|
||||||
|
for x in filter(isPdfFilename, os.listdir(signatureDir))]
|
||||||
|
if signatureDir else [])
|
||||||
|
signatures.sort()
|
||||||
|
if not signatures and not args.text:
|
||||||
|
die('Could not find any usable signatures. No --signature, no --text, and ' +
|
||||||
|
('no .pdf files found in {signatureDir}.'
|
||||||
|
if signatureDir else
|
||||||
|
f'no signature directory. The options considered for signature directory are: {signatureDirHelp}'))
|
||||||
|
if args.text:
|
||||||
|
assert(len(args.text) > 0)
|
||||||
|
for text in args.text:
|
||||||
|
validateText(text, True)
|
||||||
|
signatures=[('text', textLabel(x), x) for x in args.text] + signatures
|
||||||
|
signatureIndex=Cell(0)
|
||||||
|
@Cell
|
||||||
|
def signaturePath():
|
||||||
|
(signType, _, content)=signatures[signatureIndex()]
|
||||||
|
if signType=='cell':
|
||||||
|
(signType, _, content)=content()
|
||||||
|
if signType=='file':
|
||||||
|
return content
|
||||||
|
assert signType=='text'
|
||||||
|
cache = signaturePath._cache
|
||||||
|
if content in cache:
|
||||||
|
return cache[content]
|
||||||
|
fileName=intmp(f"text{len(cache)}.pdf")
|
||||||
|
text_to_pdf(content, fileName, 12, 1)
|
||||||
|
cache[content]=fileName
|
||||||
|
return fileName
|
||||||
|
signaturePath._cache={}
|
||||||
|
signatureSize=Cell(lambda: pdfGetSize(signaturePath()))
|
||||||
|
try:
|
||||||
|
xm=m("^(L\\+|L-|R\\+|R-|-|)([0-9.]+)%$", args.x_coordinate)
|
||||||
|
default_x=(lambda x:{'L+':x,'L-':-x,'R+':1+x,'R-':1-x,'-':-x,'':x}[xm[1]])(float(xm[2])/100)
|
||||||
|
except:
|
||||||
|
die('Invalid -x option')
|
||||||
|
try:
|
||||||
|
ym=m("^(T\\+|T-|B\\+|B-|-|)([0-9.]+)%$", args.y_coordinate)
|
||||||
|
default_y=(lambda y:{'T+':y,'T-':-y,'B+':1+y,'B-':1-y,'-':-y,'':y}[ym[1]])(float(ym[2])/100)
|
||||||
|
except:
|
||||||
|
die('Invalid -y option')
|
||||||
|
signaturePositionX=Cell(default_x)
|
||||||
|
signaturePositionY=Cell(default_y)
|
||||||
|
signatureScale=Cell(0)
|
||||||
def translatablePDF(path):
|
def translatablePDF(path):
|
||||||
cache = translatablePDF._cache
|
cache = translatablePDF._cache
|
||||||
if path not in cache:
|
if path not in cache:
|
||||||
(w, h) = pdfGetSize(path)
|
(w, h) = pdfGetSize(path)
|
||||||
|
if not (1 < w and 1 < h):
|
||||||
|
die(f"The PDF at {path} is unusable as a signature due to too small dimensions.")
|
||||||
(double_w, double_h) = (2*w, 2*h)
|
(double_w, double_h) = (2*w, 2*h)
|
||||||
testFile = os.path.join(tempdir, 'translateTest.pdf')
|
testFile = intmp('translateTest.pdf')
|
||||||
subprocess.run([
|
subprocess.run([
|
||||||
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||||
f'-sOutputFile={testFile}',
|
f'-sOutputFile={testFile}',
|
||||||
|
@ -93,23 +143,25 @@ def main(args):
|
||||||
'-f', path,
|
'-f', path,
|
||||||
], check=True)
|
], check=True)
|
||||||
(test_w, test_h) = pdfGetSize(testFile)
|
(test_w, test_h) = pdfGetSize(testFile)
|
||||||
if (test_w, test_h) == (double_w, double_h):
|
if abs(test_w - double_w) < 0.01 and abs(test_h - double_h) < 0.01:
|
||||||
# The pdf file at path can be translated correctly
|
# The pdf file at path can be translated correctly
|
||||||
cache[path] = path
|
cache[path] = path
|
||||||
elif (test_w, test_h) == (w, h):
|
elif (test_w, test_h) == (w, h):
|
||||||
# The pdf file at path cannot be translated correctly.
|
# The pdf file at path cannot be translated correctly.
|
||||||
# Rather, the size is unchanged. This can happen if the PDF
|
# Rather, the size is unchanged. This can happen if the PDF
|
||||||
# has an explicit CropBox set. We have to remove it to make
|
# has an explicit CropBox set, e.g. is created with
|
||||||
# the PDF translatable and usable as a signature.
|
# gs -c '[/CropBox [0 0 100 50] /PAGES pdfmark'. We have to
|
||||||
translatableFileName = os.path.join(tempdir, f'translatable{len(cache)}.pdf')
|
# remove it to make the PDF translatable and usable as a
|
||||||
emptyFileName = os.path.join(tempdir, f'empty{len(cache)}.pdf')
|
# signature.
|
||||||
|
translatableFileName = intmp(f'translatable{len(cache)}.pdf')
|
||||||
|
emptyFileName = intmp(f'empty{len(cache)}.pdf')
|
||||||
subprocess.run([
|
subprocess.run([
|
||||||
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||||
f'-sOutputFile={emptyFileName}',
|
f'-sOutputFile={emptyFileName}',
|
||||||
'-sDEVICE=pdfwrite',
|
'-sDEVICE=pdfwrite',
|
||||||
f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}',
|
f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}',
|
||||||
], check=True)
|
], check=True)
|
||||||
qpdfOrPdftk([
|
qpdfOrPdftk(args, [
|
||||||
'qpdf', '--overlay', path, '--',
|
'qpdf', '--overlay', path, '--',
|
||||||
emptyFileName, translatableFileName,
|
emptyFileName, translatableFileName,
|
||||||
],[
|
],[
|
||||||
|
@ -123,10 +175,27 @@ def main(args):
|
||||||
return cache[path]
|
return cache[path]
|
||||||
translatablePDF._cache={}
|
translatablePDF._cache={}
|
||||||
@Cell
|
@Cell
|
||||||
|
def defaultResizeFactor():
|
||||||
|
(pageWidth, pageHeight)=pageSize()
|
||||||
|
try:
|
||||||
|
wm=m("^([0-9.]+)(pts|pt|in|cm|mm|%)$", args.width)
|
||||||
|
maxSignatureWidth=int(float(wm[1])*({'pts':1,'pt':1,'in':72,'cm':28.3,'mm':2.83,'%':pageWidth/100}[wm[2]]))
|
||||||
|
except:
|
||||||
|
die('Invalid -W option')
|
||||||
|
try:
|
||||||
|
hm=m("^([0-9.]+)(pts|pt|in|cm|mm|%)$", args.height)
|
||||||
|
maxSignatureHeight=int(float(hm[1])*({'pts':1,'pt':1,'in':72,'cm':28.3,'mm':2.83,'%':pageHeight/100}[hm[2]]))
|
||||||
|
except:
|
||||||
|
die('Invalid -H option')
|
||||||
|
(signatureWidth, signatureHeight)=signatureSize()
|
||||||
|
return min(args.resize_factor,
|
||||||
|
maxSignatureWidth / signatureWidth,
|
||||||
|
maxSignatureHeight / signatureHeight)
|
||||||
|
@VolatileCell
|
||||||
def signaturePositionedPDF():
|
def signaturePositionedPDF():
|
||||||
(w, h)=pageSize()
|
(w, h)=pageSize()
|
||||||
(sw, sh)=signatureSize()
|
(sw, sh)=signatureSize()
|
||||||
resize=1.1**signatureScale()*min(args.width*w/sw, args.height*h/sh)
|
resize=1.1**signatureScale()*defaultResizeFactor()
|
||||||
dx=w*signaturePositionX()/resize - sw/2
|
dx=w*signaturePositionX()/resize - sw/2
|
||||||
dy=h*(1-signaturePositionY())/resize - sh/2
|
dy=h*(1-signaturePositionY())/resize - sh/2
|
||||||
outFile=intmp('signature-positioned.pdf')
|
outFile=intmp('signature-positioned.pdf')
|
||||||
|
@ -140,51 +209,51 @@ def main(args):
|
||||||
], check=True)
|
], check=True)
|
||||||
return outFile
|
return outFile
|
||||||
# The signed page
|
# The signed page
|
||||||
@Cell
|
@VolatileCell
|
||||||
def signedPagePDF():
|
def signedPagePDF():
|
||||||
outFile=intmp('signed-page.pdf')
|
outFile=intmp('signed-page.pdf')
|
||||||
qpdfOrPdftk([
|
qpdfOrPdftk(args, [
|
||||||
'qpdf', '--overlay', str(signaturePositionedPDF()), '--',
|
'qpdf', '--overlay', signaturePositionedPDF(), '--',
|
||||||
str(pagePDF()), str(outFile),
|
pagePDF(), outFile,
|
||||||
],[
|
],[
|
||||||
'pdftk', str(pagePDF()),
|
'pdftk', pagePDF(),
|
||||||
'stamp', str(signaturePositionedPDF()),
|
'stamp', signaturePositionedPDF(),
|
||||||
'output', str(outFile)
|
'output', outFile
|
||||||
])
|
])
|
||||||
return outFile
|
return outFile
|
||||||
# The signed page as PNG, for GUI use
|
# The signed page as PNG, for GUI use
|
||||||
displayMaxSize=Cell((400, 800), 'displayMaxSize')
|
displayMaxSize=Cell((400, 800))
|
||||||
@Cell
|
@Cell
|
||||||
def displaySize():
|
def displaySize():
|
||||||
(maxWidth, maxHeight)=displayMaxSize()
|
(maxWidth, maxHeight)=displayMaxSize()
|
||||||
(pageWidth, pageHeight)=pageSize()
|
(pageWidth, pageHeight)=pageSize()
|
||||||
scale=min(maxWidth/pageWidth, maxHeight/pageHeight)
|
scale=min(maxWidth/pageWidth, maxHeight/pageHeight)
|
||||||
return (round(pageWidth*scale), round(pageHeight*scale))
|
return (round(pageWidth*scale), round(pageHeight*scale))
|
||||||
@Cell
|
@VolatileCell
|
||||||
def displayPNG():
|
def displayPNG():
|
||||||
(w, h)=displaySize()
|
(w, h)=displaySize()
|
||||||
outFile=intmp('display.png')
|
outFile=intmp('display.png')
|
||||||
tdbg()
|
|
||||||
subprocess.run([
|
subprocess.run([
|
||||||
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
'gs', '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||||
f'-sOutputFile={outFile}',
|
f'-sOutputFile={outFile}',
|
||||||
'-sDEVICE=pngalpha',
|
'-sDEVICE=pngalpha',
|
||||||
'-dMaxBitmap=2147483647',
|
'-dMaxBitmap=2147483647',
|
||||||
f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}', '-dFIXEDMEDIA', '-dPDFFitPage',
|
f'-dDEVICEWIDTHPOINTS={w}', f'-dDEVICEHEIGHTPOINTS={h}', '-dFIXEDMEDIA', '-dPDFFitPage',
|
||||||
'-f', str(signedPagePDF()),
|
'-f', signedPagePDF(),
|
||||||
], check=True)
|
], check=True)
|
||||||
tdbg()
|
|
||||||
return outFile
|
return outFile
|
||||||
# GUI
|
# GUI
|
||||||
doSign=True
|
doSign=True
|
||||||
gui=not args.batch
|
gui=not args.batch
|
||||||
if gui:
|
if gui:
|
||||||
tdbg()
|
|
||||||
try:
|
try:
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
|
from tkinter import simpledialog
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
die('Cannot find Python module `tkinter`, which is needed for interactive use.')
|
die('Cannot find Python module `tkinter`, which is needed for interactive use.')
|
||||||
doSign=False
|
doSign=False
|
||||||
|
customText=Cell(('text', None, time.strftime('%Y-%m-%d')))
|
||||||
|
customTextIndex = len(signatures)
|
||||||
# Commands
|
# Commands
|
||||||
def uf(fun):
|
def uf(fun):
|
||||||
def ret():
|
def ret():
|
||||||
|
@ -217,6 +286,32 @@ def main(args):
|
||||||
if i<len(signatures):
|
if i<len(signatures):
|
||||||
signatureIndex(i)
|
signatureIndex(i)
|
||||||
update()
|
update()
|
||||||
|
def cmd_customText():
|
||||||
|
if args.signature:
|
||||||
|
return
|
||||||
|
# Get text from user
|
||||||
|
text = simpledialog.askstring(
|
||||||
|
"Custom Text",
|
||||||
|
"Input the text you want to stamp this PDF file with",
|
||||||
|
initialvalue=customText()[2])
|
||||||
|
if text == None:
|
||||||
|
return
|
||||||
|
# Validate text
|
||||||
|
validateMsg = validateText(text, False)
|
||||||
|
if validateMsg:
|
||||||
|
simpledialog.messagebox.showerror(
|
||||||
|
parent=root,
|
||||||
|
title="Invalid text",
|
||||||
|
message=validateMsg
|
||||||
|
)
|
||||||
|
return
|
||||||
|
# Set text
|
||||||
|
customText(('text', None, text))
|
||||||
|
signatureIndex(customTextIndex)
|
||||||
|
label='Custom text: ' + textLabel(text)
|
||||||
|
placemenu.entryconfig(len(signatures) + 2, label=label)
|
||||||
|
update()
|
||||||
|
def textLabel(text): return f'"{text}"' if len(text) <= 20 else f'Custom text: "{text[:17]}"...'
|
||||||
def cmd_abort():
|
def cmd_abort():
|
||||||
root.destroy()
|
root.destroy()
|
||||||
def cmd_sign():
|
def cmd_sign():
|
||||||
|
@ -229,9 +324,7 @@ def main(args):
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
tk.Tk.report_callback_exception = tkerror
|
tk.Tk.report_callback_exception = tkerror
|
||||||
# Window and menu
|
# Window and menu
|
||||||
tdbg()
|
|
||||||
root = tk.Tk(className="pdf-sign")
|
root = tk.Tk(className="pdf-sign")
|
||||||
tdbg()
|
|
||||||
rootmenu = tk.Menu(root)
|
rootmenu = tk.Menu(root)
|
||||||
root.config(menu=rootmenu)
|
root.config(menu=rootmenu)
|
||||||
filemenu = tk.Menu(rootmenu, tearoff=0)
|
filemenu = tk.Menu(rootmenu, tearoff=0)
|
||||||
|
@ -256,11 +349,27 @@ def main(args):
|
||||||
if root.signatureControlVar.get() != signatureIndex():
|
if root.signatureControlVar.get() != signatureIndex():
|
||||||
signatureIndex(root.signatureControlVar.get())
|
signatureIndex(root.signatureControlVar.get())
|
||||||
update()
|
update()
|
||||||
for index, filename in enumerate(signatures):
|
for index, (_, signatureText, _) in enumerate(signatures):
|
||||||
placemenu.add_radiobutton(value=index, label=f'{index+1}: {filename}', underline=0 if index<9 else None, variable=root.signatureControlVar, accelerator=(str(index+1) if index<9 else None), command=updateFromSignatureRadio)
|
placemenu.add_radiobutton(
|
||||||
|
value=index,
|
||||||
|
label=f'{index+1}: {signatureText}',
|
||||||
|
underline=0 if index<9 else None,
|
||||||
|
variable=root.signatureControlVar,
|
||||||
|
accelerator=(str(index+1) if index<9 else None),
|
||||||
|
command=updateFromSignatureRadio)
|
||||||
|
signatures.append(('cell', None, customText))
|
||||||
|
placemenu.add_radiobutton(
|
||||||
|
value=customTextIndex,
|
||||||
|
label='Custom text: ' + textLabel(customText()[2]),
|
||||||
|
underline=7,
|
||||||
|
variable=root.signatureControlVar,
|
||||||
|
accelerator='T',
|
||||||
|
command=cmd_customText)
|
||||||
placemenu.add_separator()
|
placemenu.add_separator()
|
||||||
placemenu.add_command(label='Enlarge signature', underline=0, accelerator='+', command=cmd_enlargeSignature)
|
placemenu.add_command(label='Enlarge signature', underline=0, accelerator='+', command=cmd_enlargeSignature)
|
||||||
|
root.bind_all('<KP_Add>', lambda e: cmd_enlargeSignature())
|
||||||
placemenu.add_command(label='Shrink signature', underline=0, accelerator='-', command=cmd_shrinkSignature)
|
placemenu.add_command(label='Shrink signature', underline=0, accelerator='-', command=cmd_shrinkSignature)
|
||||||
|
root.bind_all('<KP_Subtract>', lambda e: cmd_shrinkSignature())
|
||||||
placemenu.add_separator()
|
placemenu.add_separator()
|
||||||
placemenu.add_command(label='Move signature left', underline=15, accelerator='Click / Left', command=cmd_moveSignatureLeft)
|
placemenu.add_command(label='Move signature left', underline=15, accelerator='Click / Left', command=cmd_moveSignatureLeft)
|
||||||
placemenu.add_command(label='Move signature down', underline=15, accelerator='Click / Down', command=cmd_moveSignatureDown)
|
placemenu.add_command(label='Move signature down', underline=15, accelerator='Click / Down', command=cmd_moveSignatureDown)
|
||||||
|
@ -285,16 +394,14 @@ def main(args):
|
||||||
'q': cmd_abort,
|
'q': cmd_abort,
|
||||||
'S': cmd_sign,
|
'S': cmd_sign,
|
||||||
's': cmd_sign,
|
's': cmd_sign,
|
||||||
|
'T': cmd_customText,
|
||||||
|
't': cmd_customText,
|
||||||
'space': cmd_sign,
|
'space': cmd_sign,
|
||||||
}
|
}
|
||||||
def onkey(event):
|
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
|
key=('C-' if event.state in [4, 5] else '')+event.keysym
|
||||||
if key in keyToFunction:
|
if key in keyToFunction:
|
||||||
print(f"Debug: Calling function for {key}")
|
|
||||||
keyToFunction[key]()
|
keyToFunction[key]()
|
||||||
else:
|
|
||||||
print(f"Debug: No function for {key}")
|
|
||||||
for key in keyToFunction.keys():
|
for key in keyToFunction.keys():
|
||||||
root.bind(f'<{key.split("-")[-1]}>', onkey)
|
root.bind(f'<{key.split("-")[-1]}>', onkey)
|
||||||
def bindDigit(i, char):
|
def bindDigit(i, char):
|
||||||
|
@ -302,12 +409,9 @@ def main(args):
|
||||||
root.bind(f'{char}', onkey)
|
root.bind(f'{char}', onkey)
|
||||||
for i, char in enumerate("123456789"): bindDigit(i, char)
|
for i, char in enumerate("123456789"): bindDigit(i, char)
|
||||||
# Canvas and click binding
|
# Canvas and click binding
|
||||||
tdbg()
|
|
||||||
root._docView=tk.Canvas(root, borderwidth=0, background='#ffffff')
|
root._docView=tk.Canvas(root, borderwidth=0, background='#ffffff')
|
||||||
tdbg()
|
|
||||||
docViewMargin=5
|
docViewMargin=5
|
||||||
docViewMinDimension=50
|
docViewMinDimension=50
|
||||||
tdbg()
|
|
||||||
def onRootResize(event):
|
def onRootResize(event):
|
||||||
rootWidth=root.winfo_width()
|
rootWidth=root.winfo_width()
|
||||||
rootHeight=root.winfo_height()
|
rootHeight=root.winfo_height()
|
||||||
|
@ -323,7 +427,6 @@ def main(args):
|
||||||
initWinSize=(min(initWinSize[0], initWinSize[1] * pageSize()[0] / pageSize()[1]),
|
initWinSize=(min(initWinSize[0], initWinSize[1] * pageSize()[0] / pageSize()[1]),
|
||||||
min(initWinSize[1], initWinSize[0] * pageSize()[1] / pageSize()[0]))
|
min(initWinSize[1], initWinSize[0] * pageSize()[1] / pageSize()[0]))
|
||||||
root.geometry(f"{int(initWinSize[0])}x{int(initWinSize[1])}")
|
root.geometry(f"{int(initWinSize[0])}x{int(initWinSize[1])}")
|
||||||
tdbg()
|
|
||||||
@Cell
|
@Cell
|
||||||
def updateTitle():
|
def updateTitle():
|
||||||
root.title(f'Signing page {pageNumber()}/{pageCount} of {filePath}')
|
root.title(f'Signing page {pageNumber()}/{pageCount} of {filePath}')
|
||||||
|
@ -342,51 +445,28 @@ def main(args):
|
||||||
updateActive = True
|
updateActive = True
|
||||||
update()
|
update()
|
||||||
root.after(100, soonAfterStart)
|
root.after(100, soonAfterStart)
|
||||||
tdbg()
|
|
||||||
@tkthrottle(100, root)
|
@tkthrottle(100, root)
|
||||||
def update():
|
def update():
|
||||||
tdbg()
|
|
||||||
if not updateActive:
|
if not updateActive:
|
||||||
return
|
return
|
||||||
(w, h) = displaySize()
|
(w, h) = displaySize()
|
||||||
filename = str(displayPNG())
|
root._docImg = tk.PhotoImage(file=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.itemconfig(root._docViewIndex, image=root._docImg)
|
||||||
root._docView.configure(width=w, height=h)
|
root._docView.configure(width=w, height=h)
|
||||||
updateTitle()
|
updateTitle()
|
||||||
if not args.signature:
|
if not args.signature:
|
||||||
if root.signatureControlVar.get() != signatureIndex():
|
if root.signatureControlVar.get() != signatureIndex():
|
||||||
root.signatureControlVar.set(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):
|
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
|
x=event.x
|
||||||
y=event.y
|
y=event.y
|
||||||
canvasConfig=root._docView.config()
|
canvasConfig=root._docView.config()
|
||||||
canvasWidth=int(canvasConfig['width'][4])
|
canvasWidth=int(canvasConfig['width'][4])
|
||||||
canvasHeight=int(canvasConfig['height'][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)
|
cmd_positionSignature(x/canvasWidth, y/canvasHeight)
|
||||||
root._docView.bind('<Button-1>', onclick)
|
root._docView.bind('<Button-1>', onclick)
|
||||||
# Run GUI
|
# Run GUI
|
||||||
tdbg()
|
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
tdbg()
|
|
||||||
# End of GUI
|
# End of GUI
|
||||||
if doSign:
|
if doSign:
|
||||||
signedFilePath=args.output if args.output else f'{filePath[:-4]}.signed{filePath[-4:]}'
|
signedFilePath=args.output if args.output else f'{filePath[:-4]}.signed{filePath[-4:]}'
|
||||||
|
@ -404,10 +484,10 @@ def main(args):
|
||||||
else:
|
else:
|
||||||
assert args.existing=='overwrite'
|
assert args.existing=='overwrite'
|
||||||
pnr=pageNumber()
|
pnr=pageNumber()
|
||||||
qpdfOrPdftk([
|
qpdfOrPdftk(args, [
|
||||||
'qpdf', '--pages',
|
'qpdf', '--pages',
|
||||||
*(['.', f'1-{pnr-1}'] if 1 < pnr else []),
|
*(['.', f'1-{pnr-1}'] if 1 < pnr else []),
|
||||||
str(signedPagePDF()), '1',
|
signedPagePDF(), '1',
|
||||||
*(['.', f'{pnr+1}-z'] if pnr < pageCount else []),
|
*(['.', f'{pnr+1}-z'] if pnr < pageCount else []),
|
||||||
'--',
|
'--',
|
||||||
inputPDF,
|
inputPDF,
|
||||||
|
@ -426,36 +506,75 @@ def main(args):
|
||||||
else:
|
else:
|
||||||
print(f'Aborted')
|
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 has(cmd):
|
def has(cmd):
|
||||||
return subprocess.run(["which", cmd], check=False, capture_output=True).returncode == 0
|
return subprocess.run(["which", cmd], check=False, capture_output=True).returncode == 0
|
||||||
hasQpdf = has("qpdf")
|
hasQpdf = has("qpdf")
|
||||||
def qpdfOrPdftk(qpdfCmd, pdftkCmd):
|
def qpdfOrPdftk(args, qpdfCmd, pdftkCmd):
|
||||||
assert qpdfCmd[0] == "qpdf" and pdftkCmd[0] == "pdftk"
|
assert qpdfCmd[0] == "qpdf" and pdftkCmd[0] == "pdftk"
|
||||||
cmd = qpdfCmd if hasQpdf else pdftkCmd
|
if hasQpdf:
|
||||||
subprocess.run(cmd, check=True)
|
if args.continue_on_warnings:
|
||||||
|
subprocess.run(['qpdf', '--warning-exit-0', *qpdfCmd[1:]], check=True)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
subprocess.run(qpdfCmd, check=True)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
if e.returncode == 3:
|
||||||
|
# https://qpdf.readthedocs.io/en/stable/cli.html#exit-status
|
||||||
|
die("qpdf exited with code 3, indicating warnings. Run pdf-sign with flag --continue-on-warnings to ignore.")
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
subprocess.run(pdftkCmd, check=True)
|
||||||
return True # Some lambdas above rely on this
|
return True # Some lambdas above rely on this
|
||||||
|
|
||||||
def getSignatureDir():
|
def validateText(text, do_die):
|
||||||
if 'PDF_SIGNATURE_DIR' in os.environ:
|
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
||||||
sd=os.environ['PDF_SIGNATURE_DIR']
|
text_chars=set(map(ord, text))
|
||||||
elif 'XDG_CONFIG_HOME' in os.environ:
|
msg = ""
|
||||||
sd=os.path.join(os.environ['XDG_CONFIG_HOME'], 'pdf_signatures')
|
if not text_chars.issubset(latin1_chars):
|
||||||
elif os.path.exists(os.path.expanduser("~/.config/pdf_signatures")):
|
msg = ("Error: Only non-control latin-1 characters are supported" +
|
||||||
sd="~/.config/pdf_signatures"
|
(" in --text" if do_die else "") +
|
||||||
else:
|
". Unsupported characters: " + ', '.join(map(hex, text_chars - latin1_chars)))
|
||||||
sd="~/.pdf_signatures"
|
elif not (1 <= len(text) and len(text) <= 100):
|
||||||
sd=os.path.expanduser(sd)
|
msg = "Text must be between 1 and 100 characters long."
|
||||||
if not os.path.exists(sd):
|
if not msg:
|
||||||
raise Exception(f'Signature directory {sd} does not exist')
|
return None
|
||||||
if not os.path.isdir(sd):
|
if do_die:
|
||||||
raise Exception(f'Signature directory {sd} is not a directory')
|
die(msg)
|
||||||
return sd
|
return msg
|
||||||
|
|
||||||
|
def getSignatureDirAndHelp():
|
||||||
|
def candidate(prevpath, prevhelptxt, nr, lbl, envvar, path):
|
||||||
|
helptxt = f"{nr}) {lbl}"
|
||||||
|
if prevhelptxt: helptxt = f"{prevhelptxt} {helptxt}"
|
||||||
|
if envvar and not envvar in os.environ:
|
||||||
|
helptxt += " which is not used since the environment variable is not set."
|
||||||
|
return (prevpath, helptxt)
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
helptxt += f" which is not used since {path} does not exist."
|
||||||
|
return (prevpath, helptxt)
|
||||||
|
if not os.path.isdir(path):
|
||||||
|
helptxt += f" which is not used since {path} is not a directory."
|
||||||
|
return (prevpath, helptxt)
|
||||||
|
if prevpath:
|
||||||
|
helptxt += f" which is valid, but not used since it's not the highest priority directory."
|
||||||
|
return (prevpath, helptxt)
|
||||||
|
helptxt += f" which resolves to {path} and IS USED as the signature directory."
|
||||||
|
return (path, helptxt)
|
||||||
|
(path, helptxt) = (None, None)
|
||||||
|
(path, helptxt) = candidate(path, helptxt,
|
||||||
|
1, '$PDF_SIGNATURE_DIR', 'PDF_SIGNATURE_DIR',
|
||||||
|
os.environ.get('PDF_SIGNATURE_DIR'))
|
||||||
|
(path, helptxt) = candidate(path, helptxt,
|
||||||
|
2, '$XDG_CONFIG_HOME/pdf_signatures', 'XDG_CONFIG_HOME',
|
||||||
|
os.path.join(os.environ.get('XDG_CONFIG_HOME') or '/', 'pdf_signatures'))
|
||||||
|
(path, helptxt) = candidate(path, helptxt,
|
||||||
|
3, '~/.config/pdf_signatures', None,
|
||||||
|
'~/.config/pdf_signatures')
|
||||||
|
(path, helptxt) = candidate(path, helptxt,
|
||||||
|
4, '~/.pdf_signatures', None,
|
||||||
|
'~/.pdf_signatures')
|
||||||
|
return (path, helptxt)
|
||||||
|
|
||||||
# Simple dependency tracking.
|
# Simple dependency tracking.
|
||||||
# Init with a value or function to calculate the value.
|
# Init with a value or function to calculate the value.
|
||||||
|
@ -464,44 +583,33 @@ def getSignatureDir():
|
||||||
# Calculations with unchanged inputs are skipped.
|
# Calculations with unchanged inputs are skipped.
|
||||||
class Cell():
|
class Cell():
|
||||||
currentCell=None
|
currentCell=None
|
||||||
def __init__(self, arg, dbgname=None):
|
def __init__(self, arg):
|
||||||
self._dbgname=dbgname
|
|
||||||
if dbgname==None and callable(arg): self._dbgname=arg.__name__
|
|
||||||
self._arg=arg
|
self._arg=arg
|
||||||
self._isuptodate=False
|
self._isuptodate=False
|
||||||
self._needEval=True
|
self._needEval=True
|
||||||
self._dependents=[]
|
self._dependents=[]
|
||||||
self._precedents=[]
|
self._precedents=[]
|
||||||
self._precedentvalues=[]
|
self._precedentvalues=[]
|
||||||
print(f"Debug: Init cell {dbgname}{repr(arg)}")
|
|
||||||
def __call__(self, *args):
|
def __call__(self, *args):
|
||||||
if(len(args)==1):
|
if(len(args)==1):
|
||||||
if self._arg != args[0]:
|
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._arg=args[0]
|
||||||
self._needEval=True
|
self._needEval=True
|
||||||
self._dirty()
|
self._dirty()
|
||||||
else:
|
|
||||||
print(f"Debug: Called cell {self._dbgname}{repr(args)}, not changing value.")
|
|
||||||
return
|
return
|
||||||
assert len(args)==0
|
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:
|
if not self._isuptodate:
|
||||||
dbgtext+=", updating it"
|
|
||||||
oldcell=Cell.currentCell
|
oldcell=Cell.currentCell
|
||||||
Cell.currentCell=None
|
Cell.currentCell=None
|
||||||
for i in range(len(self._precedents)):
|
for i in range(len(self._precedents)):
|
||||||
p=self._precedents[i]
|
p=self._precedents[i]
|
||||||
oldval=self._precedentvalues[i]
|
oldval=self._precedentvalues[i]
|
||||||
newval=p()
|
p()
|
||||||
|
newval=p._cacheid()
|
||||||
if oldval!=newval:
|
if oldval!=newval:
|
||||||
dbgtext+=f", detected change in precedent {p._dbgname}"
|
|
||||||
self._needEval=True
|
self._needEval=True
|
||||||
break
|
break
|
||||||
if self._needEval:
|
if self._needEval:
|
||||||
dbgtext+=", evaluating"
|
|
||||||
Cell.currentCell=self
|
Cell.currentCell=self
|
||||||
for p in self._precedents:
|
for p in self._precedents:
|
||||||
p._dependents.remove(self)
|
p._dependents.remove(self)
|
||||||
|
@ -514,17 +622,89 @@ class Cell():
|
||||||
if(Cell.currentCell):
|
if(Cell.currentCell):
|
||||||
self._dependents.append(Cell.currentCell)
|
self._dependents.append(Cell.currentCell)
|
||||||
Cell.currentCell._precedents.append(self)
|
Cell.currentCell._precedents.append(self)
|
||||||
Cell.currentCell._precedentvalues.append(self._value)
|
Cell.currentCell._precedentvalues.append(self._cacheid())
|
||||||
dbgtext+=f", returning {repr(self._value)}"
|
|
||||||
Cell._dbgprefix=Cell._dbgprefix[:-2]
|
|
||||||
print(dbgtext)
|
|
||||||
return self._value
|
return self._value
|
||||||
def _dirty(self):
|
def _dirty(self):
|
||||||
if self._isuptodate:
|
if self._isuptodate:
|
||||||
self._isuptodate=False
|
self._isuptodate=False
|
||||||
for d in self._dependents:
|
for d in self._dependents:
|
||||||
d._dirty()
|
d._dirty()
|
||||||
Cell._dbgprefix=''
|
def _cacheid(self):
|
||||||
|
assert self._isuptodate
|
||||||
|
return self._value
|
||||||
|
# When the return value is just a file name, the contents of that file needs to
|
||||||
|
# be taken into account in order to decide whether the "value" has changed. In
|
||||||
|
# these cases, we track the precedent values along the cell's own value.
|
||||||
|
class VolatileCell(Cell):
|
||||||
|
def _cacheid(self):
|
||||||
|
assert self._isuptodate
|
||||||
|
return (self._value, self._precedentvalues)
|
||||||
|
|
||||||
|
# Keep this function in sync with pdf-from-text
|
||||||
|
def text_to_pdf(text, output, size, margin):
|
||||||
|
# Validate text
|
||||||
|
text_chars=set(map(ord, text))
|
||||||
|
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
||||||
|
if not text_chars.issubset(latin1_chars):
|
||||||
|
die(f"Error: Only non-control latin-1 characters are supported. Unsupported characters: {', '.join(map(hex, text_chars - latin1_chars))}")
|
||||||
|
text.encode('latin-1').decode('latin-1') # Assertion. E.i., an exception here indicates a bug.
|
||||||
|
with tempfile.TemporaryDirectory() as tempdir:
|
||||||
|
# Write postscript file
|
||||||
|
ps_file=os.path.join(tempdir, "file.ps")
|
||||||
|
text_len=len(text)
|
||||||
|
w=text_len * size + margin * 2
|
||||||
|
h=size * 3 + margin * 2
|
||||||
|
x=size + margin
|
||||||
|
y=size + margin
|
||||||
|
ps_text = (text
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("(", "\\(")
|
||||||
|
.replace(")", "\\)")
|
||||||
|
)
|
||||||
|
def write_ps():
|
||||||
|
with open(ps_file, "w", encoding="latin-1") as f:
|
||||||
|
f.write('\n'.join([
|
||||||
|
"%!PS-Adobe-3.0",
|
||||||
|
f"%%BoundingBox: 0 0 {w} {h}",
|
||||||
|
"%%Pages: 1",
|
||||||
|
"%%EndComments",
|
||||||
|
"%%Page: 1 1",
|
||||||
|
f"/DejaVuSansMono findfont {size} scalefont setfont",
|
||||||
|
f"{x} {y} moveto",
|
||||||
|
f"({ps_text}) show",
|
||||||
|
"showpage"]))
|
||||||
|
# Write postscript file with too big bounding box
|
||||||
|
write_ps()
|
||||||
|
# Get correct bounding box
|
||||||
|
bbox_result = subprocess.run([
|
||||||
|
"gs", '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||||
|
"-sDEVICE=bbox",
|
||||||
|
ps_file
|
||||||
|
], stderr=subprocess.PIPE, text=True, check=True)
|
||||||
|
bbox = m(r'.*%%HiResBoundingBox: (\d+(\.\d+)?) (\d+(\.\d+)?) (\d+(\.\d+)?) (\d+(\.\d+)?)\n.*', bbox_result.stderr)
|
||||||
|
if not bbox:
|
||||||
|
die("Error: Unable to extract bounding box.")
|
||||||
|
# Adjust variables for bounding box
|
||||||
|
llx, lly, urx, ury = float(bbox[1]), float(bbox[3]), float(bbox[5]), float(bbox[7])
|
||||||
|
llx, lly, urx, ury = llx - margin, lly - margin, urx + margin, ury + margin
|
||||||
|
w=urx - llx
|
||||||
|
h=ury - lly
|
||||||
|
x-=llx
|
||||||
|
y-=lly
|
||||||
|
# Write postscript file with correct bounding box
|
||||||
|
write_ps()
|
||||||
|
# Convert to PDF
|
||||||
|
gs_cmd = [
|
||||||
|
"gs", '-dBATCH', '-dNOPAUSE', '-dSAFER', '-dQUIET',
|
||||||
|
"-o", output,
|
||||||
|
"-sDEVICE=pdfwrite",
|
||||||
|
f"-dDEVICEWIDTHPOINTS={w}",
|
||||||
|
f"-dDEVICEHEIGHTPOINTS={h}",
|
||||||
|
"-dFIXEDMEDIA",
|
||||||
|
"-c", "[ /PAGES pdfmark",
|
||||||
|
"-f", ps_file
|
||||||
|
]
|
||||||
|
subprocess.run(gs_cmd, check=True, stdout=subprocess.DEVNULL)
|
||||||
|
|
||||||
def tkthrottle(frequency, root):
|
def tkthrottle(frequency, root):
|
||||||
wait=1/frequency
|
wait=1/frequency
|
||||||
|
@ -548,10 +728,10 @@ def tkthrottle(frequency, root):
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def pdfCountPages(filePath):
|
def pdfCountPages(filePath):
|
||||||
return int(fromCmdOutput(["pdfinfo", str(filePath)], "^.*\nPages: +([0-9]+)\n.*$")[1])
|
return int(fromCmdOutput(["pdfinfo", filePath], r"^.*\nPages: +([0-9]+)\n.*$")[1])
|
||||||
|
|
||||||
def pdfGetSize(filePath):
|
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], r'^.*\nPage size: +([0-9.]+) x ([0-9.]+) pts( \([A-Za-z0-9]+\))?\n.*$')
|
||||||
return (float(w), float(h))
|
return (float(w), float(h))
|
||||||
|
|
||||||
def m(pattern, string):
|
def m(pattern, string):
|
||||||
|
@ -573,10 +753,6 @@ def fromCmdOutput(cmd, pattern):
|
||||||
result=sp.stdout.decode('utf-8')
|
result=sp.stdout.decode('utf-8')
|
||||||
return m(pattern, result)
|
return m(pattern, result)
|
||||||
|
|
||||||
def die(reason):
|
|
||||||
print(reason, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Monkey-patch argparse if necessary
|
# Monkey-patch argparse if necessary
|
||||||
if not 'BooleanOptionalAction' in dir(argparse):
|
if not 'BooleanOptionalAction' in dir(argparse):
|
||||||
class BooleanOptionalAction(argparse.Action):
|
class BooleanOptionalAction(argparse.Action):
|
||||||
|
@ -597,28 +773,34 @@ if not 'BooleanOptionalAction' in dir(argparse):
|
||||||
return ' | '.join(self.option_strings)
|
return ' | '.join(self.option_strings)
|
||||||
argparse.BooleanOptionalAction = BooleanOptionalAction
|
argparse.BooleanOptionalAction = BooleanOptionalAction
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Sign a PDF file.')
|
|
||||||
|
def getSignatureHelp():
|
||||||
|
(path, helptxt) = getSignatureDirAndHelp()
|
||||||
|
ret="""
|
||||||
|
Path to file used as signature.
|
||||||
|
Required in batch mode unless -t is given.
|
||||||
|
In GUI mode, the user can choose among PDF files in the signature directory.
|
||||||
|
"""
|
||||||
|
if path:
|
||||||
|
ret += f"The signature directory is {path}, chosen from these options: {helptxt}"
|
||||||
|
else:
|
||||||
|
ret += f"Right now, no signature directory could be found. The options are: {helptxt}"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Sign a PDF file, with a non-cryptographic squiggle.')
|
||||||
parser.add_argument('input', metavar='input.pdf', type=str, help='Input PDF file.')
|
parser.add_argument('input', metavar='input.pdf', type=str, help='Input PDF file.')
|
||||||
parser.add_argument('-p', '--page', type=int, default=-1, help='The page to sign, negative for counting from the end. (default: -1)')
|
parser.add_argument('-p', '--page', type=int, default=-1, help='The page to sign, negative for counting from the end. (default: -1)')
|
||||||
parser.add_argument('-s', '--signature', type=str, help='Path to file used as signature. Required in batch mode. In GUI mode, the user can choose among files in $PDF_SIGNATURE_DIR, $XDG_CONFIG_HOME/pdf_signatures (defaulting to ~/.config/pdf_signatures), or ~/.pdf_signatures.')
|
parser.add_argument('-s', '--signature', type=str, help=getSignatureHelp())
|
||||||
parser.add_argument('-x', '--x-coordinate', type=float, default=0.5, help='Horizontal coordinate of signature center, in page width units. (default: 0.5)')
|
parser.add_argument('-x', '--x-coordinate', type=str, default="50%", help='Horizontal coordinate of signature center. Requires unit %% (meaning percent of page width). May be preceded by L+ (default), L-, R+ or R- to give coordinate relative left of right edge of the page. For example, 25%% means a quarter page width from left edge, R-3%% means 3%% of page width from right edge. (default: 50%%)')
|
||||||
parser.add_argument('-y', '--y-coordinate', type=float, default=0.75, help='Vertical coordinate of signature center, in page height units. (default: 0.75)')
|
parser.add_argument('-y', '--y-coordinate', type=str, default="B-25%", help='Vertical coordinate of signature center. Requires unit %% (meaning percent of page height). May be preceded by T+ (default), T-, B+ or B- to give coordinate relative top or bottom edge of the page. For example, 3%% means 3%% of page height from top edge, B-10%% means 10%% of page height from bottom edge. (default: B-25%%)')
|
||||||
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('-r', '--resize-factor', type=float, default=1.0, help='Resize signature by this factor. If this would make the signature larger than allowed by -W or -H, then instead use the maximum allowed size.')
|
||||||
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('-W', '--width', type=str, default="50%", help='Maximum width of signature. Supports units pts, in, cm, mm and %% (meaning percent of page width). (default: 50%%)')
|
||||||
|
parser.add_argument('-H', '--height', type=str, default="50%", help='Maximum height of signature. Supports units pts, in, cm, mm and %% (meaning percent of page height). (default: 50%%)')
|
||||||
parser.add_argument('-o', '--output', type=str, help='Output file. (default: Add ".signed" before the extension)')
|
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. (default: False)')
|
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('-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)')
|
parser.add_argument('-e', '--existing', type=str, choices=['backup', 'overwrite', 'fail'], default='backup', help='What to do if output file exists. (default: backup)')
|
||||||
|
parser.add_argument('-t', '--text', type=str, action='append', help='In GUI mode, a text option to be added to the list of signatures (can be repeated). In batch mode only one can be given, and will be used instead of --signature.')
|
||||||
# debug subprocess runs
|
parser.add_argument('--continue-on-warnings', action=argparse.BooleanOptionalAction, default=False, help='Do not fail on warnings from qpdf. (default: False)')
|
||||||
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']))
|
main(parser.parse_args(sys.argv[1:] or ['-h']))
|
||||||
|
|
Laden …
Tabelle hinzufügen
In neuem Issue referenzieren