add clean badge generation for each user from database

This commit is contained in:
tcprod 2026-03-15 21:15:17 +01:00
parent d11d0b1b38
commit 4e55fc8273
12 changed files with 386 additions and 305 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
.venv
test.pdf
.idea
.idea
files

27
ezgg_badge_generator.py Normal file
View File

@ -0,0 +1,27 @@
import asyncio
from services.DatabaseService import DatabaseService
from services.BadgeGeneratorService import BadgeGeneratorService
from pathlib import Path
async def main():
db = DatabaseService("sql_config.toml")
await db.init_db_pool()
badges = await db.get_user_badges()
badge_generator = BadgeGeneratorService(
svg_template=Path("template/template_dynamic_name_size.svg"),
chrome_path=Path(r"C:\Program Files\Google\Chrome\Application\chrome.exe")
)
for user_badge in badges:
badge_generator.generate_badge(
name=user_badge["user_name"],
seat_id=user_badge["seat_id"],
picture=user_badge["picture"]
)
if __name__ == "__main__":
asyncio.run(main())

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
mysql-connector-python~=9.6.0
aiomysql~=0.3.2
Jinja2~=3.1.6

View File

@ -0,0 +1,68 @@
import logging
import os
import re
import base64
import subprocess
import tempfile
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger(__name__.split(".")[-1])
class BadgeGeneratorService:
def __init__(self, svg_template: Path, chrome_path: Path):
self.svg_template = svg_template
self.chrome_path = chrome_path
def generate_badge(self, name, seat_id, picture) -> None:
svg = self.generate_svg(name, seat_id, picture)
self.svg_to_pdf(svg, f"files/{name}.pdf")
def generate_svg(self, username: str, seat: str, image_base64: bytes) -> str:
with open(self.svg_template, "r", encoding="utf-8") as f:
svg = f.read()
# ToDo: Select font size for long names
svg = svg.replace(">test_username</tspan>", f">{username}</tspan>")
svg = svg.replace(">Platz: XYZ</tspan><", f">Platz: {seat}</tspan><")
if image_base64:
encoded = base64.b64encode(image_base64).decode("utf-8")
svg = svg.replace('xlink:href="data:image/jpeg;base64,"', f'xlink:href="data:image/jpeg;base64,{encoded}"')
logger.info(f"Generate svg string: Username: {username}, Seat: {seat}, Picture: {str(image_base64)[:15]}")
return svg
def svg_to_pdf(self, svg_string: str, pdf_path: str):
width = re.search(r'width="([^"]+)"', svg_string).group(1)
height = re.search(r'height="([^"]+)"', svg_string).group(1)
env = Environment(loader=FileSystemLoader("template"))
template = env.get_template("badge_pdf.html")
html = template.render(
width=width,
height=height,
svg_string=svg_string
)
with tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8") as f:
f.write(html)
html_path = f.name
try:
subprocess.run([
self.chrome_path,
"--headless",
"--disable-gpu",
f"--print-to-pdf={os.path.abspath(pdf_path)}",
html_path
], check=True)
finally:
os.remove(html_path)
logger.info(f"Created: {pdf_path}")

View File

@ -0,0 +1,38 @@
import tomllib
import aiomysql
from typing import Optional
class DatabaseService:
def __init__(self, sql_config: str):
with open(sql_config, "rb") as f:
config = tomllib.load(f)
self.db_config = config["database"]
self._pool: Optional[aiomysql.Pool] = None
async def init_db_pool(self):
self._pool = await aiomysql.create_pool(
host=self.db_config["db_host"],
port=self.db_config["db_port"],
user=self.db_config["db_user"],
password=self.db_config["db_password"],
db=self.db_config["db_name"],
minsize=1,
maxsize=40,
autocommit=True
)
async def get_user_badges(self) -> list:
async with self._pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
await cursor.execute(
"""SELECT u.user_id, u.user_name, s.seat_id, upp.picture
FROM users AS u
LEFT JOIN seats AS s
ON u.user_id = s.`user`
LEFT JOIN user_profile_picture AS upp
ON u.user_id = upp.user_id;"""
)
return await cursor.fetchall()

0
services/__init__.py Normal file
View File

6
sql_config.toml Normal file
View File

@ -0,0 +1,6 @@
[database]
db_user="demo_user"
db_password="demo_password"
db_host="127.0.0.1"
db_port=3306
db_name="ezgg_lan_manager"

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 191 KiB

39
template/badge_pdf.html Normal file
View File

@ -0,0 +1,39 @@
<html>
<head>
<style>
@page {
size: {{ width }} {{ height }};
margin: 0;
}
body {
margin: 0;
}
.page {
width: {{ width }};
height: {{ height }};
page-break-after: always;
display: flex;
align-items: center;
justify-content: center;
}
.rotated {
transform: rotate(180deg);
transform-origin: center;
}
</style>
</head>
<body>
<div class="page">
{{ svg_string | safe }}
</div>
<div class="page rotated">
{{ svg_string | safe }}
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@ -1,104 +0,0 @@
import os
import re
import base64
import subprocess
import tempfile
from pathlib import Path
SVG_TEMPLATE = Path("template/Vorlage_LAN_Ausweis.svg")
CHROME_PATH = Path(r"C:\Program Files\Google\Chrome\Application\chrome.exe")
def get_image_as_base64(image_path: str) -> str:
with open(image_path, "rb") as image_file:
encoded_bytes = base64.b64encode(image_file.read())
encoded_string = encoded_bytes.decode("utf-8")
return f'xlink:href="data:image/jpeg;base64,{encoded_string}"'
def get_user_svg_string(username: str, seat: str, user_image: str) -> str:
with open(SVG_TEMPLATE, "r", encoding="utf-8") as svg_file:
svg_string = svg_file.read()
svg_string = svg_string.replace(">test_username</tspan>", f">{username}</tspan>")
svg_string = svg_string.replace(">Platz: XYZ</tspan><", f">Platz: {seat}</tspan><")
svg_string = svg_string.replace('xlink:href="data:image/jpeg;base64,"', user_image)
"""
with open("test.svg", "w", encoding="utf-8") as f:
f.write(svg_content)
"""
return svg_string
def create_pdf_from_svg(svg_string: str, pdf_path: str):
width = re.search(r'width="([^"]+)"', svg_string)
height = re.search(r'height="([^"]+)"', svg_string)
width = width.group(1)
height = height.group(1)
html = f"""
<html>
<head>
<style>
@page {{
size: {width} {height};
margin: 0;
}}
body {{
margin: 0;
}}
.page {{
width: {width};
height: {height};
page-break-after: always;
display: flex;
align-items: center;
justify-content: center;
}}
.rotated {{
transform: rotate(180deg) scaleX(-1);
transform-origin: center;
}}
</style>
</head>
<body>
<div class="page">
{svg_string}
</div>
<div class="page rotated">
{svg_string}
</div>
</body>
</html>
"""
with tempfile.NamedTemporaryFile(delete=False, suffix=".html", mode="w", encoding="utf-8") as f:
f.write(html)
html_path = f.name
try:
subprocess.run([
CHROME_PATH,
"--headless",
"--disable-gpu",
f"--print-to-pdf={os.path.abspath(pdf_path)}",
html_path
], check=True)
finally:
os.remove(html_path)
if __name__ == "__main__":
img_base64 = get_image_as_base64("template/test_profile_picture.png")
user_svg_string = get_user_svg_string("Tcprod.", "C01", img_base64)
create_pdf_from_svg(user_svg_string, "test.pdf")