Merge branch 'dev'
Dieser Commit ist enthalten in:
Commit
7b5a699060
5 geänderte Dateien mit 102 neuen und 60 gelöschten Zeilen
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 |
|
@ -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,6 +16,11 @@ 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.
|
||||||
|
|
||||||
|
<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.
|
* 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.
|
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.
|
||||||
|
|
||||||
|
|
Binäre Datei nicht angezeigt.
155
pdf-sign
155
pdf-sign
|
@ -65,41 +65,44 @@ def main(args):
|
||||||
return outFile
|
return outFile
|
||||||
pageSize=Cell(lambda: pdfGetSize(pagePDF()))
|
pageSize=Cell(lambda: pdfGetSize(pagePDF()))
|
||||||
# The chosen signature
|
# The chosen signature
|
||||||
if args.batch:
|
if args.signature:
|
||||||
if not args.signature and not args.text:
|
if args.text:
|
||||||
|
die('--signature and --text cannot be specified together.')
|
||||||
|
if not os.path.exists(args.signature):
|
||||||
|
die(f'File not found: {args.signature}')
|
||||||
|
signatures=[('file', args.signature, args.signature)]
|
||||||
|
elif args.batch:
|
||||||
|
assert not args.signature
|
||||||
|
if not args.text:
|
||||||
die('In batch mode, --signature or --text must be specified.')
|
die('In batch mode, --signature or --text must be specified.')
|
||||||
if args.text and len(args.text) > 1:
|
if len(args.text) > 1:
|
||||||
die('In batch mode, --text must be given only once.')
|
die('In batch mode, --text must be given only once.')
|
||||||
if args.signature and args.text:
|
validateText(args.text[0], True)
|
||||||
die('--signature and --text cannot be specified together.')
|
signatures=[('text', textLabel(args.text[0]), args.text[0])]
|
||||||
if args.signature and not os.path.exists(args.signature):
|
else:
|
||||||
die(f'File not found: {args.signature}')
|
(signatureDir, signatureDirHelp)=getSignatureDirAndHelp()
|
||||||
if args.text:
|
signatures=([('file', x, os.path.join(signatureDir, x))
|
||||||
assert(len(args.text) > 0)
|
for x in filter(isPdfFilename, os.listdir(signatureDir))]
|
||||||
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
if signatureDir else [])
|
||||||
for text in args.text:
|
signatures.sort()
|
||||||
text_chars=set(map(ord, text))
|
if not signatures and not args.text:
|
||||||
if not text_chars.issubset(latin1_chars):
|
die('Could not find any usable signatures. No --signature, no --text, and ' +
|
||||||
die("Error: Only non-control latin-1 characters are supported in --text." +
|
('no .pdf files found in {signatureDir}.'
|
||||||
" Unsupported characters: " + ', '.join(map(hex, text_chars - latin1_chars)))
|
if signatureDir else
|
||||||
signatureDir=getSignatureDir()
|
f'no signature directory. The options considered for signature directory are: {signatureDirHelp}'))
|
||||||
signatures=[('file', x) for x in filter(isPdfFilename, os.listdir(signatureDir))] if not args.signature else [None]
|
if args.text:
|
||||||
if not signatures and not args.text:
|
assert(len(args.text) > 0)
|
||||||
die(f'Could not find anything usable as signature, since no .pdf files found in {signatureDir} and no --text option given.')
|
for text in args.text:
|
||||||
signatures.sort()
|
validateText(text, True)
|
||||||
if args.text:
|
signatures=[('text', textLabel(x), x) for x in args.text] + signatures
|
||||||
signatures=[('text', x) for x in args.text] + signatures
|
|
||||||
signatureIndex=Cell(0)
|
signatureIndex=Cell(0)
|
||||||
customText=Cell(('text', time.strftime('%Y-%m-%d')))
|
|
||||||
@Cell
|
@Cell
|
||||||
def signaturePath():
|
def signaturePath():
|
||||||
if args.signature:
|
(signType, _, content)=signatures[signatureIndex()]
|
||||||
return args.signature
|
|
||||||
(signType, content)=signatures[signatureIndex()]
|
|
||||||
if signType=='cell':
|
if signType=='cell':
|
||||||
(signType, content)=content()
|
(signType, _, content)=content()
|
||||||
if signType=='file':
|
if signType=='file':
|
||||||
return os.path.join(signatureDir, content)
|
return content
|
||||||
assert signType=='text'
|
assert signType=='text'
|
||||||
cache = signaturePath._cache
|
cache = signaturePath._cache
|
||||||
if content in cache:
|
if content in cache:
|
||||||
|
@ -110,13 +113,25 @@ def main(args):
|
||||||
return fileName
|
return fileName
|
||||||
signaturePath._cache={}
|
signaturePath._cache={}
|
||||||
signatureSize=Cell(lambda: pdfGetSize(signaturePath()))
|
signatureSize=Cell(lambda: pdfGetSize(signaturePath()))
|
||||||
signaturePositionX=Cell(args.x_coordinate)
|
try:
|
||||||
signaturePositionY=Cell(args.y_coordinate)
|
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)
|
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 = intmp('translateTest.pdf')
|
testFile = intmp('translateTest.pdf')
|
||||||
subprocess.run([
|
subprocess.run([
|
||||||
|
@ -128,7 +143,7 @@ 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):
|
||||||
|
@ -159,11 +174,28 @@ def main(args):
|
||||||
die(f"The PDF at {path} is unusable as a signature. Reason unknown.")
|
die(f"The PDF at {path} is unusable as a signature. Reason unknown.")
|
||||||
return cache[path]
|
return cache[path]
|
||||||
translatablePDF._cache={}
|
translatablePDF._cache={}
|
||||||
|
@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
|
@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')
|
||||||
|
@ -220,6 +252,7 @@ def main(args):
|
||||||
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)
|
customTextIndex = len(signatures)
|
||||||
# Commands
|
# Commands
|
||||||
def uf(fun):
|
def uf(fun):
|
||||||
|
@ -260,32 +293,25 @@ def main(args):
|
||||||
text = simpledialog.askstring(
|
text = simpledialog.askstring(
|
||||||
"Custom Text",
|
"Custom Text",
|
||||||
"Input the text you want to stamp this PDF file with",
|
"Input the text you want to stamp this PDF file with",
|
||||||
initialvalue=customText()[1])
|
initialvalue=customText()[2])
|
||||||
if text == None:
|
if text == None:
|
||||||
return
|
return
|
||||||
# Validate text
|
# Validate text
|
||||||
text_chars=set(map(ord, text))
|
validateMsg = validateText(text, False)
|
||||||
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
if validateMsg:
|
||||||
if not text_chars.issubset(latin1_chars):
|
|
||||||
simpledialog.messagebox.showerror(
|
simpledialog.messagebox.showerror(
|
||||||
parent=root,
|
parent=root,
|
||||||
title="Invalid text",
|
title="Invalid text",
|
||||||
message=f"Only non-control latin-1 characters are supported. Unsupported characters: {', '.join(map(hex, text_chars - latin1_chars))}"
|
message=validateMsg
|
||||||
)
|
|
||||||
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
|
return
|
||||||
# Set text
|
# Set text
|
||||||
customText(('text', text))
|
customText(('text', None, text))
|
||||||
signatureIndex(customTextIndex)
|
signatureIndex(customTextIndex)
|
||||||
label='Custom text: ' + (text if len(text) <= 20 else (text[:17] + '...'))
|
label='Custom text: ' + textLabel(text)
|
||||||
placemenu.entryconfig(len(signatures) + 2, label=label)
|
placemenu.entryconfig(len(signatures) + 2, label=label)
|
||||||
update()
|
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():
|
||||||
|
@ -323,18 +349,18 @@ 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, (_, signature) in enumerate(signatures):
|
for index, (_, signatureText, _) in enumerate(signatures):
|
||||||
placemenu.add_radiobutton(
|
placemenu.add_radiobutton(
|
||||||
value=index,
|
value=index,
|
||||||
label=f'{index+1}: {signature}',
|
label=f'{index+1}: {signatureText}',
|
||||||
underline=0 if index<9 else None,
|
underline=0 if index<9 else None,
|
||||||
variable=root.signatureControlVar,
|
variable=root.signatureControlVar,
|
||||||
accelerator=(str(index+1) if index<9 else None),
|
accelerator=(str(index+1) if index<9 else None),
|
||||||
command=updateFromSignatureRadio)
|
command=updateFromSignatureRadio)
|
||||||
signatures.append(('cell', customText))
|
signatures.append(('cell', None, customText))
|
||||||
placemenu.add_radiobutton(
|
placemenu.add_radiobutton(
|
||||||
value=customTextIndex,
|
value=customTextIndex,
|
||||||
label='Custom text: ' + customText()[1],
|
label='Custom text: ' + textLabel(customText()[2]),
|
||||||
underline=7,
|
underline=7,
|
||||||
variable=root.signatureControlVar,
|
variable=root.signatureControlVar,
|
||||||
accelerator='T',
|
accelerator='T',
|
||||||
|
@ -487,11 +513,21 @@ def qpdfOrPdftk(qpdfCmd, pdftkCmd):
|
||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, 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):
|
||||||
(path, helptxt) = getSignatureDirAndHelp()
|
latin1_chars=set([*range(0x20, 0x7f), *range(0xa0, 0x100)])
|
||||||
if not path:
|
text_chars=set(map(ord, text))
|
||||||
die(f"Could not find a valid signature directory. The options considered are: {helptxt}")
|
msg = ""
|
||||||
return path
|
if not text_chars.issubset(latin1_chars):
|
||||||
|
msg = ("Error: Only non-control latin-1 characters are supported" +
|
||||||
|
(" in --text" if do_die else "") +
|
||||||
|
". Unsupported characters: " + ', '.join(map(hex, text_chars - latin1_chars)))
|
||||||
|
elif not (1 <= len(text) and len(text) <= 100):
|
||||||
|
msg = "Text must be between 1 and 100 characters long."
|
||||||
|
if not msg:
|
||||||
|
return None
|
||||||
|
if do_die:
|
||||||
|
die(msg)
|
||||||
|
return msg
|
||||||
|
|
||||||
def getSignatureDirAndHelp():
|
def getSignatureDirAndHelp():
|
||||||
def candidate(prevpath, prevhelptxt, nr, lbl, envvar, path):
|
def candidate(prevpath, prevhelptxt, nr, lbl, envvar, path):
|
||||||
|
@ -742,10 +778,11 @@ parser = argparse.ArgumentParser(description='Sign a PDF file, with a non-crypto
|
||||||
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=getSignatureHelp())
|
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.11, help='Height of box to fit signature to, in page height units. (default: 0.11)')
|
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)')
|
||||||
|
|
Laden …
Tabelle hinzufügen
In neuem Issue referenzieren