aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorB. Stack <bgstack15@gmail.com>2024-09-22 20:41:52 -0400
committerB. Stack <bgstack15@gmail.com>2024-09-22 20:41:52 -0400
commit6ab41891eacbfa910e73c7d521df01ab395d015c (patch)
treef916232de6a62ce2fb36422c7bef11b50fb5d9bc
downloadfprintd-tk-6ab41891eacbfa910e73c7d521df01ab395d015c.tar.gz
fprintd-tk-6ab41891eacbfa910e73c7d521df01ab395d015c.tar.bz2
fprintd-tk-6ab41891eacbfa910e73c7d521df01ab395d015c.zip
initial commit
-rw-r--r--.gitignore1
-rwxr-xr-xfprintd_tk.py149
-rw-r--r--fprintd_tk_lib.py81
-rw-r--r--images/fingerprint-enrolled.svg81
-rw-r--r--images/fingerprint-gui.svg17
5 files changed, 329 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c18dd8d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+__pycache__/
diff --git a/fprintd_tk.py b/fprintd_tk.py
new file mode 100755
index 0000000..8fe0ab6
--- /dev/null
+++ b/fprintd_tk.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+# Startdate: 2024-09-22-1 14:26
+# Purpose: tkinter desktop gui app for management fingerprint enrollments
+# Dependencies:
+# python3-tkstackrpms, python3-tk, python3-pil
+# References:
+# stackrpms_tk.py
+# Improve:
+# implement delete
+# implement enroll
+#
+
+import tkinter as tk, os, tkinter.filedialog, sys, threading
+import tkstackrpms as stk
+import fprintd_tk_lib as lib
+from PIL import Image, ImageTk
+
+# todo: load images array?
+
+ABOUT_TEXT = """
+fprintd_tk \"Gui for fprintd\"
+(C) 2024 bgstack15
+SPDX-License-Identifier: GPL-3.0-only
+Icons adapted from Numix-Icon-Theme-Circle (GPL 3)
+"""
+
+# configurable by admin
+img_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"images")
+
+# These will be used a lot in the program.
+str_hands = ["left","right"]
+str_fingers = ["thumb","index-finger","middle-finger","ring-finger","little-finger"]
+str_all = []
+for h in str_hands:
+ for f in str_fingers:
+ str_all.append(f"{h}-{f}")
+
+class App(tk.Frame):
+ def __init__(self, master):
+ super().__init__(master)
+ self.master.title("Gui for fprintd")
+ imgicon = stk.get_scaled_icon("fingerprint-gui",24,"default", "","apps")
+ self.master.tk.call("wm","iconphoto",self.master._w,imgicon)
+ menu = tk.Menu(self.master)
+ menu_file = tk.Menu(menu,tearoff=0)
+ menu_file.add_command(label="Delete...", command=self.func_delete, underline=0)
+ menu_file.add_separator()
+ menu_file.add_command(label="Exit", command=self.func_exit, underline=1)
+ menu.add_cascade(label="File",menu=menu_file,underline=0)
+ menu_help = tk.Menu(menu,tearoff=0)
+ menu_help.add_command(label="About", command=self.func_about, underline=0)
+ menu.add_cascade(label="Help",menu=menu_help,underline=0)
+ self.master.config(menu=menu)
+ self.grid() # use this instead of pack()
+
+ # statusbar variable
+ self.statustext = tk.StringVar()
+
+ # prepare finger images
+ try:
+ img_path + ""
+ except:
+ img_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"images")
+ self.img_notenrolled = ImageTk.PhotoImage(stk.image_from_svg(os.path.join(img_path,"fingerprint-gui.svg"),32))
+ self.img_enrolled = ImageTk.PhotoImage(stk.image_from_svg(os.path.join(img_path,"fingerprint-enrolled.svg"),32))
+
+ self.frm_actions = tk.Frame(self.master)
+ self.frm_actions.grid(row=0,column=0)
+ self.action = tk.IntVar(value=1)
+ stk.Radiobutton(self.frm_actions,value=1,text="enroll",variable=self.action,underline=0).grid(row=0,column=0)
+ stk.Radiobutton(self.frm_actions,value=2,text="verify",variable=self.action,underline=0).grid(row=1,column=0)
+
+ # array of fingers
+ # we want 2 rows, 5 columns
+ self.frm_fingers = tk.Frame(self.master)
+ self.frm_fingers.grid(row=0,column=1)
+ self.fingers = []
+ hand = 0
+ while hand < 2:
+ finger = 0
+ while finger < 5:
+ fnum = finger if hand==1 else len(str_fingers)-finger-1
+ #print(f"While preparing hand {hand}, evaluating finger {finger}, which might need to be number {fnum}")
+ tf = str_hands[hand] + "-" + str_fingers[fnum]
+ self.fingers.append(tk.Button(self.frm_fingers,text=tf,padx=0,pady=0,command=lambda f=tf: self.func_finger_button(f),image=self.img_notenrolled,compound="top"))
+ self.fingers[-1].grid(row=hand,column=finger)
+ finger = finger + 1
+ hand = hand + 1
+ # because the underline business does not work on the radio button itself
+ # somehow the keypress takse over the lambda first var, so just set a dummy value, and pass our useful value as second parameter.
+ self.master.bind("<Alt-e>",lambda a=1,b=1: self.set_action(a,b))
+ self.master.bind("<Alt-v>",lambda a=1,b=2: self.set_action(a,b))
+
+ # status bar
+ stk.StatusBar(self.master,var=self.statustext)
+ # and now, load data into form for the first time
+ self.load_data_into_form()
+
+ def set_action(self, keypress, a):
+ #print(f"DEBUG: got keypress {keypress}, a={a}")
+ # this is used by Alt+E, Alt+V to select the radio buttons for enroll, verify, etc.
+ self.action.set(a)
+
+ def load_data_into_form(self):
+ self.enrolled_fingers = lib.get_enrolled_fingers()
+ print(f"DEBUG (load_data_into_form): got enrolled fingers {self.enrolled_fingers}")
+ for i in self.fingers:
+ if str(i.cget('text')) in self.enrolled_fingers:
+ i.config(image=self.img_enrolled)
+ else:
+ i.config(image=self.img_notenrolled)
+
+ # functions
+ def func_about(self):
+ """ Display about dialog. """
+ tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show()
+
+ def func_exit(self):
+ # in case we need to manually do stuff
+ # otherwise command=self.client_exit would have sufficed.
+ self.master.quit()
+
+ def func_finger_button(self, finger):
+ print(f"DEBUG: func_finger_button finger {finger}, action {self.action.get()}")
+ action = self.action.get()
+ if 1 == action: # enroll
+ try:
+ # WORKHERE: pass a tk text var to set for intermediate operations?
+ self.statustext.set(lib.enroll_finger(finger, self.func_update_status))
+ except Exception as e:
+ self.statustext.set(e)
+ elif 2 == action: # verify
+ #print(f"stub action: verify, finger {finger}")
+ t1 = threading.Thread(target=lib.verify_finger,args=(finger, self.func_update_status))
+ t1.run()
+ self.load_data_into_form()
+
+ def func_update_status(self, msg):
+ msg = msg.strip()
+ print(f"DEBUG: func_update_status called with msg {msg}",file=sys.stderr)
+ self.statustext.set(msg)
+
+ def func_delete(self):
+ print(f"stub, please ask user if he is certain to delete all enrolled fingerprints")
+
+# main
+root = tk.Tk()
+fprintd_tk = App(root)
+fprintd_tk.mainloop()
diff --git a/fprintd_tk_lib.py b/fprintd_tk_lib.py
new file mode 100644
index 0000000..1f1b022
--- /dev/null
+++ b/fprintd_tk_lib.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+# Startdate: 2024-09-22-1 15:33
+# Purpose: backend for fprintd_tk that uses fprintd-* binaries, in case I ever rewrite this to use dbus directly?
+# Dependencies:
+# /usr/bin/fprintd-*
+# Improve:
+# add a verbose/debug param to everything?
+
+import os, subprocess, re
+
+_user = os.getenv("USER")
+#fre = re.compile("^.* . #[0-9]: \w+$")
+fre = re.compile ("^.* - #[0-9]+: ([^ ]+)$")
+
+def get_enrolled_fingers(user = None):
+ # return list of full strings of fingers that are enrolled.
+ enrolled_fingers = []
+ if user is None:
+ user = _user
+ proc = subprocess.Popen(
+ ["fprintd-list",user],
+ stdout = subprocess.PIPE,
+ universal_newlines = True # or maybe text=True
+ )
+ #print(result.stdout)
+ while True:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ if fre.match(line):
+ #print(f"Got {fre.match(line).groups()[0]}")
+ enrolled_fingers.append(fre.match(line).groups()[0].strip())
+ else:
+ #print(f"Not-matching: {line}",end="")
+ pass
+ return enrolled_fingers
+
+def enroll_finger(finger, status_function = None, user = None):
+ if user is None:
+ user = _user
+ proc = subprocess.Popen(
+ ["fprintd-enroll","-f",finger,user],
+ stdout = subprocess.PIPE,
+ universal_newlines = True
+ )
+ while True:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ if re.match("^.*enroll-duplicate.*",line):
+ # Using the same finger as any other finger enrolled by any user already is considered an error by fprintd, and there is nothing we can do about it. It is an error and we must raise an error here.
+ return "enroll-duplicate"
+ elif re.match("^.*enroll-completed.*",line):
+ return "enroll-completed"
+ elif re.match("^.*enroll-stage-passed.*",line):
+ if status_function:
+ #print(f"Will use function {status_function}(\"enroll-stage-passed\")")
+ status_function("enroll-stage-passed")
+ print(f"Great, keep going!")
+ # so the process has ended, now what?
+ return f"Last line was: {line}"
+
+def verify_finger(finger, status_function = None, user = None):
+ if user is None:
+ user = _user
+ proc = subprocess.Popen(
+ ["fprintd-verify","-f",finger,user],
+ stdout = subprocess.PIPE,
+ universal_newlines = True
+ )
+ while True:
+ line = proc.stdout.readline()
+ if not line:
+ break
+ if re.match("^.*verify-match (done).*",line):
+ return "verify-match"
+ else:
+ if status_function:
+ #print(f"Will use function {status_function}(\"enroll-stage-passed\")")
+ status_function(line)
+ return line
diff --git a/images/fingerprint-enrolled.svg b/images/fingerprint-enrolled.svg
new file mode 100644
index 0000000..b6d2cd7
--- /dev/null
+++ b/images/fingerprint-enrolled.svg
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ version="1.1"
+ viewBox="0 0 48 48"
+ id="svg25"
+ sodipodi:docname="fingerprint-enrolled.svg"
+ inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:svg="http://www.w3.org/2000/svg">
+ <sodipodi:namedview
+ id="namedview27"
+ pagecolor="#ffffff"
+ bordercolor="#000000"
+ borderopacity="0.25"
+ inkscape:showpageshadow="2"
+ inkscape:pageopacity="0.0"
+ inkscape:pagecheckerboard="0"
+ inkscape:deskcolor="#d1d1d1"
+ showgrid="false"
+ inkscape:zoom="1.0569773"
+ inkscape:cx="-41.62814"
+ inkscape:cy="1.8921882"
+ inkscape:window-width="1920"
+ inkscape:window-height="1060"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg25" />
+ <defs
+ id="defs7">
+ <linearGradient
+ id="bg"
+ x1="1"
+ x2="47"
+ gradientUnits="userSpaceOnUse">
+ <stop
+ style="stop-color:#767676"
+ offset="0"
+ id="stop2" />
+ <stop
+ style="stop-color:#808080"
+ offset="1"
+ id="stop4" />
+ </linearGradient>
+ </defs>
+ <path
+ d="m36.31 5c5.859 4.062 9.688 10.831 9.688 18.5 0 12.426-10.07 22.5-22.5 22.5-7.669 0-14.438-3.828-18.5-9.688 1.037 1.822 2.306 3.499 3.781 4.969 4.085 3.712 9.514 5.969 15.469 5.969 12.703 0 23-10.298 23-23 0-5.954-2.256-11.384-5.969-15.469-1.469-1.475-3.147-2.744-4.969-3.781zm4.969 3.781c3.854 4.113 6.219 9.637 6.219 15.719 0 12.703-10.297 23-23 23-6.081 0-11.606-2.364-15.719-6.219 4.16 4.144 9.883 6.719 16.219 6.719 12.703 0 23-10.298 23-23 0-6.335-2.575-12.06-6.719-16.219z"
+ style="opacity:.05"
+ id="path9" />
+ <path
+ d="m41.28 8.781c3.712 4.085 5.969 9.514 5.969 15.469 0 12.703-10.297 23-23 23-5.954 0-11.384-2.256-15.469-5.969 4.113 3.854 9.637 6.219 15.719 6.219 12.703 0 23-10.298 23-23 0-6.081-2.364-11.606-6.219-15.719z"
+ style="opacity:.1"
+ id="path11" />
+ <path
+ d="m31.25 2.375c8.615 3.154 14.75 11.417 14.75 21.13 0 12.426-10.07 22.5-22.5 22.5-9.708 0-17.971-6.135-21.12-14.75a23 23 0 0 0 44.875-7 23 23 0 0 0-16-21.875z"
+ style="opacity:.2"
+ id="path13" />
+ <g
+ transform="matrix(0,-1,1,0,0,48)"
+ style="fill:#b40000;fill-opacity:1"
+ id="g17">
+ <path
+ d="m24 1c12.703 0 23 10.297 23 23s-10.297 23-23 23-23-10.297-23-23 10.297-23 23-23z"
+ style="fill:#b40000;fill-opacity:1"
+ id="path15" />
+ </g>
+ <path
+ d="m40.03 7.531c3.712 4.084 5.969 9.514 5.969 15.469 0 12.703-10.297 23-23 23-5.954 0-11.384-2.256-15.469-5.969 4.178 4.291 10.01 6.969 16.469 6.969 12.703 0 23-10.298 23-23 0-6.462-2.677-12.291-6.969-16.469z"
+ style="opacity:.1"
+ id="path19" />
+ <path
+ d="m25 12c-2.25 0-4.4559 0.8285-6.1406 2.2324s-2.8594 3.4342-2.8594 5.7676c-0.01913 1.3523 2.0191 1.3523 2 0 0-1.6667 0.82533-3.1363 2.1406-4.2324s3.1094-1.7676 4.8594-1.7676c1.8352 0 3.6304 0.47157 4.9023 1.4258 1.2719 0.95421 2.0977 2.3407 2.0977 4.5742 0 1 0.05727 2.4713 1.293 3.707 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.7643-0.7643-0.70703-1.293-0.70703-2.293 0-2.7664-1.1712-4.88-2.8984-6.1758s-3.9333-1.8242-6.1016-1.8242zm0 4c-3.0333 0-5 2.5-5 5 0 0.66667-0.26803 1.0602-0.80469 1.418-0.53665 0.35777-1.362 0.58203-2.1953 0.58203-1.3523-0.01912-1.3523 2.0191 0 2 1.1667 0 2.3413-0.27574 3.3047-0.91797 0.96335-0.64223 1.6953-1.7487 1.6953-3.082 0-1.5 1.0333-3 3-3s3 1.5 3 3c0 2.1667 0.54117 3.8863 1.4512 5.0996 0.91 1.2133 2.2155 1.9004 3.5488 1.9004 1.3523 0.01912 1.3523-2.0191 0-2-0.66667 0-1.3592-0.31294-1.9492-1.0996-0.59-0.78667-1.0508-2.0671-1.0508-3.9004 0-2.5-1.9667-5-5-5zm-0.04297 4.0156c-0.52942 0.02276-0.94919 0.45453-0.95703 0.98438 0 2.2333-0.84227 3.6479-2.0078 4.6016-1.1655 0.95363-2.7144 1.3984-3.9922 1.3984-1.3523-0.01913-1.3523 2.0191 0 2 1.7222 0 3.6734-0.55519 5.2578-1.8516 0.58677-0.48008 1.0226-1.178 1.4551-1.8691 0.55398 1.6407 1.3631 3.0055 2.3027 4.0234 1.6589 1.7972 3.4844 2.6973 4.9844 2.6973 1.3523 0.01913 1.3523-2.0191 0-2-0.5 0-2.1745-0.59989-3.5156-2.0527-1.3411-1.4528-2.4844-3.6854-2.4844-6.9473-0.0083-0.56333-0.48012-1.0086-1.043-0.98438zm-0.9707 8.9844c-0.26045 0.0036-0.50921 0.10875-0.69336 0.29297-0.7643 0.7643-2.6263 1.707-4.293 1.707-1.3523-0.01913-1.3523 2.0191 0 2 1.9041 0 3.5488-0.77065 4.8086-1.7012 0.81628 1.0033 2.051 2.7091 4.875 3.6504 1.2744 0.43953 1.916-1.4854 0.63281-1.8984-2.6838-0.89459-3.3737-2.5221-4.6094-3.7578-0.19085-0.19092-0.45077-0.29658-0.7207-0.29297zm-1.5703 5.5993c-0.61086 0.04003-1.244 0.19623-1.8633 0.50586-1.1927 0.59635-0.2982 2.3855 0.89453 1.7891 0.76149-0.38074 1.4709-0.3783 2.1523-0.18359 0.68149 0.19471 1.3112 0.61394 1.6934 0.99609 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.61785-0.61785-1.4881-1.1986-2.5566-1.5039-0.53426-0.15264-1.1235-0.22948-1.7344-0.18945z"
+ style="opacity:.1"
+ id="path21" />
+ <path
+ d="m24 11c-2.25 0-4.4559 0.8285-6.1406 2.2324s-2.8594 3.4342-2.8594 5.7676c-0.01913 1.3523 2.0191 1.3523 2 0 0-1.6667 0.82533-3.1363 2.1406-4.2324s3.1094-1.7676 4.8594-1.7676c1.8352 0 3.6285 0.47157 4.9004 1.4258 1.2719 0.95421 2.0996 2.3407 2.0996 4.5742 0 1 0.05727 2.4713 1.293 3.707 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.7643-0.7643-0.70703-1.293-0.70703-2.293 0-2.7664-1.1712-4.88-2.8984-6.1758s-3.9333-1.8242-6.1016-1.8242zm0 4c-3.0333 0-5 2.5-5 5 0 0.66667-0.26803 1.0602-0.80469 1.418-0.53665 0.35777-1.362 0.58203-2.1953 0.58203-1.3523-0.01912-1.3523 2.0191 0 2 1.1667 0 2.3413-0.27574 3.3047-0.91797 0.96335-0.64223 1.6953-1.7487 1.6953-3.082 0-1.5 1.0333-3 3-3s3 1.5 3 3c0 2.1667 0.53922 3.8863 1.4492 5.0996 0.91 1.2133 2.2174 1.9004 3.5508 1.9004 1.3523 0.01912 1.3523-2.0191 0-2-0.66667 0-1.3592-0.31294-1.9492-1.0996-0.59-0.78667-1.0508-2.0671-1.0508-3.9004 0-2.5-1.9667-5-5-5zm-0.04297 4.0156c-0.52942 0.02276-0.94919 0.45453-0.95703 0.98438 0 2.2333-0.84227 3.6479-2.0078 4.6016-1.1655 0.95363-2.7144 1.3984-3.9922 1.3984-1.3523-0.01913-1.3523 2.0191 0 2 1.7222 0 3.6734-0.55519 5.2578-1.8516 0.58677-0.48008 1.0226-1.178 1.4551-1.8691 0.55398 1.6407 1.3631 3.0055 2.3027 4.0234 1.6589 1.7972 3.4844 2.6973 4.9844 2.6973 1.3523 0.01913 1.3523-2.0191 0-2-0.5 0-2.1745-0.59989-3.5156-2.0527-1.3411-1.4528-2.4844-3.6854-2.4844-6.9473-0.0083-0.56333-0.48012-1.0086-1.043-0.98438zm-0.97266 8.9844c-0.25978 0.0041-0.50774 0.10921-0.69141 0.29297-0.7643 0.7643-2.6263 1.707-4.293 1.707-1.3523-0.01913-1.3523 2.0191 0 2 1.9041 0 3.5488-0.77065 4.8086-1.7012 0.81628 1.0033 2.051 2.7091 4.875 3.6504 1.2744 0.43953 1.916-1.4854 0.63281-1.8984-2.6838-0.89459-3.3737-2.5221-4.6094-3.7578-0.19132-0.19141-0.45206-0.29712-0.72266-0.29297zm-1.5703 5.5993c-0.61086 0.04003-1.2421 0.19623-1.8613 0.50586-1.1927 0.59635-0.2982 2.3855 0.89453 1.7891 0.76149-0.38074 1.4709-0.3783 2.1523-0.18359 0.68149 0.19471 1.3112 0.61394 1.6934 0.99609 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.61785-0.61785-1.4881-1.1986-2.5566-1.5039-0.53426-0.15264-1.1255-0.22948-1.7363-0.18945z"
+ style="fill:#fafafa"
+ id="path23" />
+</svg>
diff --git a/images/fingerprint-gui.svg b/images/fingerprint-gui.svg
new file mode 100644
index 0000000..2162ef2
--- /dev/null
+++ b/images/fingerprint-gui.svg
@@ -0,0 +1,17 @@
+<svg version="1.1" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
+ <defs>
+ <linearGradient id="bg" x1="1" x2="47" gradientUnits="userSpaceOnUse">
+ <stop style="stop-color:#767676" offset="0"/>
+ <stop style="stop-color:#808080" offset="1"/>
+ </linearGradient>
+ </defs>
+ <path d="m36.31 5c5.859 4.062 9.688 10.831 9.688 18.5 0 12.426-10.07 22.5-22.5 22.5-7.669 0-14.438-3.828-18.5-9.688 1.037 1.822 2.306 3.499 3.781 4.969 4.085 3.712 9.514 5.969 15.469 5.969 12.703 0 23-10.298 23-23 0-5.954-2.256-11.384-5.969-15.469-1.469-1.475-3.147-2.744-4.969-3.781zm4.969 3.781c3.854 4.113 6.219 9.637 6.219 15.719 0 12.703-10.297 23-23 23-6.081 0-11.606-2.364-15.719-6.219 4.16 4.144 9.883 6.719 16.219 6.719 12.703 0 23-10.298 23-23 0-6.335-2.575-12.06-6.719-16.219z" style="opacity:.05"/>
+ <path d="m41.28 8.781c3.712 4.085 5.969 9.514 5.969 15.469 0 12.703-10.297 23-23 23-5.954 0-11.384-2.256-15.469-5.969 4.113 3.854 9.637 6.219 15.719 6.219 12.703 0 23-10.298 23-23 0-6.081-2.364-11.606-6.219-15.719z" style="opacity:.1"/>
+ <path d="m31.25 2.375c8.615 3.154 14.75 11.417 14.75 21.13 0 12.426-10.07 22.5-22.5 22.5-9.708 0-17.971-6.135-21.12-14.75a23 23 0 0 0 44.875-7 23 23 0 0 0-16-21.875z" style="opacity:.2"/>
+ <g transform="matrix(0,-1,1,0,0,48)" style="fill:#501616">
+ <path d="m24 1c12.703 0 23 10.297 23 23s-10.297 23-23 23-23-10.297-23-23 10.297-23 23-23z" style="fill:url(#bg)"/>
+ </g>
+ <path d="m40.03 7.531c3.712 4.084 5.969 9.514 5.969 15.469 0 12.703-10.297 23-23 23-5.954 0-11.384-2.256-15.469-5.969 4.178 4.291 10.01 6.969 16.469 6.969 12.703 0 23-10.298 23-23 0-6.462-2.677-12.291-6.969-16.469z" style="opacity:.1"/>
+ <path d="m25 12c-2.25 0-4.4559 0.8285-6.1406 2.2324s-2.8594 3.4342-2.8594 5.7676c-0.01913 1.3523 2.0191 1.3523 2 0 0-1.6667 0.82533-3.1363 2.1406-4.2324s3.1094-1.7676 4.8594-1.7676c1.8352 0 3.6304 0.47157 4.9023 1.4258 1.2719 0.95421 2.0977 2.3407 2.0977 4.5742 0 1 0.05727 2.4713 1.293 3.707 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.7643-0.7643-0.70703-1.293-0.70703-2.293 0-2.7664-1.1712-4.88-2.8984-6.1758s-3.9333-1.8242-6.1016-1.8242zm0 4c-3.0333 0-5 2.5-5 5 0 0.66667-0.26803 1.0602-0.80469 1.418-0.53665 0.35777-1.362 0.58203-2.1953 0.58203-1.3523-0.01912-1.3523 2.0191 0 2 1.1667 0 2.3413-0.27574 3.3047-0.91797 0.96335-0.64223 1.6953-1.7487 1.6953-3.082 0-1.5 1.0333-3 3-3s3 1.5 3 3c0 2.1667 0.54117 3.8863 1.4512 5.0996 0.91 1.2133 2.2155 1.9004 3.5488 1.9004 1.3523 0.01912 1.3523-2.0191 0-2-0.66667 0-1.3592-0.31294-1.9492-1.0996-0.59-0.78667-1.0508-2.0671-1.0508-3.9004 0-2.5-1.9667-5-5-5zm-0.04297 4.0156c-0.52942 0.02276-0.94919 0.45453-0.95703 0.98438 0 2.2333-0.84227 3.6479-2.0078 4.6016-1.1655 0.95363-2.7144 1.3984-3.9922 1.3984-1.3523-0.01913-1.3523 2.0191 0 2 1.7222 0 3.6734-0.55519 5.2578-1.8516 0.58677-0.48008 1.0226-1.178 1.4551-1.8691 0.55398 1.6407 1.3631 3.0055 2.3027 4.0234 1.6589 1.7972 3.4844 2.6973 4.9844 2.6973 1.3523 0.01913 1.3523-2.0191 0-2-0.5 0-2.1745-0.59989-3.5156-2.0527-1.3411-1.4528-2.4844-3.6854-2.4844-6.9473-0.0083-0.56333-0.48012-1.0086-1.043-0.98438zm-0.9707 8.9844c-0.26045 0.0036-0.50921 0.10875-0.69336 0.29297-0.7643 0.7643-2.6263 1.707-4.293 1.707-1.3523-0.01913-1.3523 2.0191 0 2 1.9041 0 3.5488-0.77065 4.8086-1.7012 0.81628 1.0033 2.051 2.7091 4.875 3.6504 1.2744 0.43953 1.916-1.4854 0.63281-1.8984-2.6838-0.89459-3.3737-2.5221-4.6094-3.7578-0.19085-0.19092-0.45077-0.29658-0.7207-0.29297zm-1.5703 5.5993c-0.61086 0.04003-1.244 0.19623-1.8633 0.50586-1.1927 0.59635-0.2982 2.3855 0.89453 1.7891 0.76149-0.38074 1.4709-0.3783 2.1523-0.18359 0.68149 0.19471 1.3112 0.61394 1.6934 0.99609 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.61785-0.61785-1.4881-1.1986-2.5566-1.5039-0.53426-0.15264-1.1235-0.22948-1.7344-0.18945z" style="opacity:.1"/>
+ <path d="m24 11c-2.25 0-4.4559 0.8285-6.1406 2.2324s-2.8594 3.4342-2.8594 5.7676c-0.01913 1.3523 2.0191 1.3523 2 0 0-1.6667 0.82533-3.1363 2.1406-4.2324s3.1094-1.7676 4.8594-1.7676c1.8352 0 3.6285 0.47157 4.9004 1.4258 1.2719 0.95421 2.0996 2.3407 2.0996 4.5742 0 1 0.05727 2.4713 1.293 3.707 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.7643-0.7643-0.70703-1.293-0.70703-2.293 0-2.7664-1.1712-4.88-2.8984-6.1758s-3.9333-1.8242-6.1016-1.8242zm0 4c-3.0333 0-5 2.5-5 5 0 0.66667-0.26803 1.0602-0.80469 1.418-0.53665 0.35777-1.362 0.58203-2.1953 0.58203-1.3523-0.01912-1.3523 2.0191 0 2 1.1667 0 2.3413-0.27574 3.3047-0.91797 0.96335-0.64223 1.6953-1.7487 1.6953-3.082 0-1.5 1.0333-3 3-3s3 1.5 3 3c0 2.1667 0.53922 3.8863 1.4492 5.0996 0.91 1.2133 2.2174 1.9004 3.5508 1.9004 1.3523 0.01912 1.3523-2.0191 0-2-0.66667 0-1.3592-0.31294-1.9492-1.0996-0.59-0.78667-1.0508-2.0671-1.0508-3.9004 0-2.5-1.9667-5-5-5zm-0.04297 4.0156c-0.52942 0.02276-0.94919 0.45453-0.95703 0.98438 0 2.2333-0.84227 3.6479-2.0078 4.6016-1.1655 0.95363-2.7144 1.3984-3.9922 1.3984-1.3523-0.01913-1.3523 2.0191 0 2 1.7222 0 3.6734-0.55519 5.2578-1.8516 0.58677-0.48008 1.0226-1.178 1.4551-1.8691 0.55398 1.6407 1.3631 3.0055 2.3027 4.0234 1.6589 1.7972 3.4844 2.6973 4.9844 2.6973 1.3523 0.01913 1.3523-2.0191 0-2-0.5 0-2.1745-0.59989-3.5156-2.0527-1.3411-1.4528-2.4844-3.6854-2.4844-6.9473-0.0083-0.56333-0.48012-1.0086-1.043-0.98438zm-0.97266 8.9844c-0.25978 0.0041-0.50774 0.10921-0.69141 0.29297-0.7643 0.7643-2.6263 1.707-4.293 1.707-1.3523-0.01913-1.3523 2.0191 0 2 1.9041 0 3.5488-0.77065 4.8086-1.7012 0.81628 1.0033 2.051 2.7091 4.875 3.6504 1.2744 0.43953 1.916-1.4854 0.63281-1.8984-2.6838-0.89459-3.3737-2.5221-4.6094-3.7578-0.19132-0.19141-0.45206-0.29712-0.72266-0.29297zm-1.5703 5.5993c-0.61086 0.04003-1.2421 0.19623-1.8613 0.50586-1.1927 0.59635-0.2982 2.3855 0.89453 1.7891 0.76149-0.38074 1.4709-0.3783 2.1523-0.18359 0.68149 0.19471 1.3112 0.61394 1.6934 0.99609 0.94256 0.97905 2.3932-0.47154 1.4141-1.4141-0.61785-0.61785-1.4881-1.1986-2.5566-1.5039-0.53426-0.15264-1.1255-0.22948-1.7363-0.18945z" style="fill:#fafafa"/>
+</svg>
bgstack15