Merge branch 'dev'

Dieser Commit ist enthalten in:
Axel Svensson 2024-10-24 06:53:39 +02:00
Commit 7b5a699060
5 geänderte Dateien mit 102 neuen und 60 gelöschten Zeilen

BIN
README-example-signature.gif Normale Datei

Binäre Datei nicht angezeigt.

Nachher

Breite:  |  Höhe:  |  Größe: 22 KiB

Datei anzeigen

Vorher

Breite:  |  Höhe:  |  Größe: 342 KiB

Nachher

Breite:  |  Höhe:  |  Größe: 342 KiB

Datei anzeigen

@ -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.
![](README-example.gif) ![](README-example-use.gif)
## 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
Datei anzeigen

@ -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)')