Ursprung
3eed74e1ef
Commit
c61747cf7a
3 geänderte Dateien mit 271 neuen und 12 gelöschten Zeilen
14
README.md
14
README.md
|
@ -22,7 +22,7 @@ The recommended way is:
|
|||
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.
|
||||
Run `pdf-sign -h`, `pdf-create-empty -h` or `pdf-from-text -h` for details.
|
||||
|
||||
**Installation**
|
||||
|
||||
|
@ -41,7 +41,17 @@ apt-get update
|
|||
apt-get install -y coreutils git python3 python3-tk ghostscript pdftk poppler-utils
|
||||
git clone https://github.com/svenssonaxel/pdf-sign.git
|
||||
cd pdf-sign
|
||||
cp pdf-sign pdf-create-empty /usr/local/bin/
|
||||
cp pdf-sign pdf-create-empty pdf-from-text /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.
|
||||
* 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 ~/.pdf_signatures/check.pdf checkmark-round.svg
|
||||
```
|
||||
|
||||
## Why
|
||||
|
|
98
pdf-from-text
Ausführbare Datei
98
pdf-from-text
Ausführbare Datei
|
@ -0,0 +1,98 @@
|
|||
#!/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)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
171
pdf-sign
171
pdf-sign
|
@ -57,15 +57,48 @@ def main(args):
|
|||
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.')
|
||||
if args.batch:
|
||||
if not args.signature and not args.text:
|
||||
die('In batch mode, --signature or --text must be specified.')
|
||||
if args.text and len(args.text) > 1:
|
||||
die('In batch mode, --text must be given only once.')
|
||||
if args.signature and args.text:
|
||||
die('--signature and --text cannot be specified together.')
|
||||
if args.text:
|
||||
assert(len(args.text) > 0)
|
||||
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
||||
for text in args.text:
|
||||
text_chars=set(map(ord, text))
|
||||
if not text_chars.issubset(latin1_chars):
|
||||
die("Error: Only non-control latin-1 characters are supported in --text." +
|
||||
" Unsupported characters: " + ', '.join(map(hex, text_chars - latin1_chars)))
|
||||
signatureDir=getSignatureDir()
|
||||
signatures=[*filter(isPdfFilename, os.listdir(signatureDir))] if not args.signature else [None]
|
||||
if not signatures:
|
||||
die(f'No .pdf files found in {signatureDir}')
|
||||
signatures=[('file', x) for x in filter(isPdfFilename, os.listdir(signatureDir))] if not args.signature else [None]
|
||||
if not signatures and not args.text:
|
||||
die(f'Could not find anything usable as signature, since no .pdf files found in {signatureDir} and no --text option given.')
|
||||
signatures.sort()
|
||||
if args.text:
|
||||
signatures=[('text', x) for x in args.text] + signatures
|
||||
signatureIndex=Cell(0)
|
||||
signaturePath=Cell(lambda: args.signature if args.signature else os.path.join(signatureDir, signatures[signatureIndex()]))
|
||||
customText=Cell(('text', time.strftime('%Y-%m-%d')))
|
||||
@Cell
|
||||
def signaturePath():
|
||||
if args.signature:
|
||||
return args.signature
|
||||
(signType, content)=signatures[signatureIndex()]
|
||||
if signType=='cell':
|
||||
(signType, content)=content()
|
||||
if signType=='file':
|
||||
return os.path.join(signatureDir, content)
|
||||
assert signType=='text'
|
||||
cache = signaturePath._cache
|
||||
if content in cache:
|
||||
return cache[content]
|
||||
fileName=os.path.join(tempdir, f"text{len(cache)}.pdf")
|
||||
text_to_pdf(content, fileName, 12, 1)
|
||||
cache[content]=fileName
|
||||
return fileName
|
||||
signaturePath._cache={}
|
||||
signatureSize=Cell(lambda: pdfGetSize(signaturePath()))
|
||||
signaturePositionX=Cell(args.x_coordinate)
|
||||
signaturePositionY=Cell(args.y_coordinate)
|
||||
|
@ -173,9 +206,11 @@ def main(args):
|
|||
if gui:
|
||||
try:
|
||||
import tkinter as tk
|
||||
from tkinter import simpledialog
|
||||
except ModuleNotFoundError:
|
||||
die('Cannot find Python module `tkinter`, which is needed for interactive use.')
|
||||
doSign=False
|
||||
customTextIndex = len(signatures)
|
||||
# Commands
|
||||
def uf(fun):
|
||||
def ret():
|
||||
|
@ -208,6 +243,39 @@ def main(args):
|
|||
if i<len(signatures):
|
||||
signatureIndex(i)
|
||||
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()[1])
|
||||
if text == None:
|
||||
return
|
||||
# Validate text
|
||||
text_chars=set(map(ord, text))
|
||||
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
||||
if not text_chars.issubset(latin1_chars):
|
||||
simpledialog.messagebox.showerror(
|
||||
parent=root,
|
||||
title="Invalid text",
|
||||
message=f"Only non-control latin-1 characters are supported. Unsupported characters: {', '.join(map(hex, text_chars - latin1_chars))}"
|
||||
)
|
||||
return
|
||||
if not (1 <= len(text) and len(text) <= 100):
|
||||
simpledialog.messagebox.showerror(
|
||||
parent=root,
|
||||
title="Invalid text",
|
||||
message="Text must be between 1 and 100 characters long."
|
||||
)
|
||||
return
|
||||
# Set text
|
||||
customText(('text', text))
|
||||
signatureIndex(customTextIndex)
|
||||
label='Custom text: ' + (text if len(text) <= 20 else (text[:17] + '...'))
|
||||
placemenu.entryconfig(len(signatures) + 2, label=label)
|
||||
update()
|
||||
def cmd_abort():
|
||||
root.destroy()
|
||||
def cmd_sign():
|
||||
|
@ -245,8 +313,22 @@ def main(args):
|
|||
if root.signatureControlVar.get() != signatureIndex():
|
||||
signatureIndex(root.signatureControlVar.get())
|
||||
update()
|
||||
for index, filename 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)
|
||||
for index, (_, signature) in enumerate(signatures):
|
||||
placemenu.add_radiobutton(
|
||||
value=index,
|
||||
label=f'{index+1}: {signature}',
|
||||
underline=0 if index<9 else None,
|
||||
variable=root.signatureControlVar,
|
||||
accelerator=(str(index+1) if index<9 else None),
|
||||
command=updateFromSignatureRadio)
|
||||
signatures.append(('cell', customText))
|
||||
placemenu.add_radiobutton(
|
||||
value=customTextIndex,
|
||||
label='Custom text: ' + customText()[1],
|
||||
underline=7,
|
||||
variable=root.signatureControlVar,
|
||||
accelerator='T',
|
||||
command=cmd_customText)
|
||||
placemenu.add_separator()
|
||||
placemenu.add_command(label='Enlarge signature', underline=0, accelerator='+', command=cmd_enlargeSignature)
|
||||
placemenu.add_command(label='Shrink signature', underline=0, accelerator='-', command=cmd_shrinkSignature)
|
||||
|
@ -274,6 +356,8 @@ def main(args):
|
|||
'q': cmd_abort,
|
||||
'S': cmd_sign,
|
||||
's': cmd_sign,
|
||||
'T': cmd_customText,
|
||||
't': cmd_customText,
|
||||
'space': cmd_sign,
|
||||
}
|
||||
def onkey(event):
|
||||
|
@ -492,6 +576,72 @@ class Cell():
|
|||
for d in self._dependents:
|
||||
d._dirty()
|
||||
|
||||
# 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):
|
||||
wait=1/frequency
|
||||
now=lambda: time.time()
|
||||
|
@ -568,7 +718,7 @@ def getSignatureHelp():
|
|||
(path, helptxt) = getSignatureDirAndHelp()
|
||||
ret="""
|
||||
Path to file used as signature.
|
||||
Required in batch mode.
|
||||
Required in batch mode unless -t is given.
|
||||
In GUI mode, the user can choose among PDF files in the signature directory.
|
||||
"""
|
||||
if path:
|
||||
|
@ -584,10 +734,11 @@ 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('-y', '--y-coordinate', type=float, default=0.75, help='Vertical coordinate of signature center, in page height units. (default: 0.75)')
|
||||
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('-H', '--height', type=float, default=0.11, help='Height of box to fit signature to, in page height units. (default: 0.11)')
|
||||
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('-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('-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.')
|
||||
|
||||
main(parser.parse_args(sys.argv[1:] or ['-h']))
|
||||
|
|
Laden …
Tabelle hinzufügen
In neuem Issue referenzieren