initial commit

This commit is contained in:
2025-11-12 21:40:44 -05:00
commit 2f0154b20f
8 changed files with 967 additions and 0 deletions

175
utils/qr_generator.py Normal file
View File

@@ -0,0 +1,175 @@
"""QR code generation utilities with logo overlay support."""
import io
from typing import Optional, Tuple
from PIL import Image, ImageDraw
import qrcode
import segno
class QRCodeGenerator:
"""Generate QR codes with optional logo overlay and multiple export formats."""
def __init__(self, url: str, error_correction: str = "H"):
"""
Initialize QR code generator.
Args:
url: The URL to encode in the QR code
error_correction: Error correction level (L, M, Q, H)
H (30%) recommended for logo overlay
"""
self.url = url
self.error_correction = self._get_error_correction(error_correction)
def _get_error_correction(self, level: str) -> int:
"""Convert error correction level string to qrcode constant."""
levels = {
"L": qrcode.constants.ERROR_CORRECT_L,
"M": qrcode.constants.ERROR_CORRECT_M,
"Q": qrcode.constants.ERROR_CORRECT_Q,
"H": qrcode.constants.ERROR_CORRECT_H,
}
return levels.get(level, qrcode.constants.ERROR_CORRECT_H)
def generate_qr_image(
self,
box_size: int = 10,
border: int = 4,
fill_color: str = "black",
back_color: str = "white"
) -> Image.Image:
"""
Generate QR code as PIL Image.
Args:
box_size: Size of each box in pixels
border: Border size in boxes
fill_color: QR code color
back_color: Background color
Returns:
PIL Image object of the QR code
"""
qr = qrcode.QRCode(
version=1,
error_correction=self.error_correction,
box_size=box_size,
border=border,
)
qr.add_data(self.url)
qr.make(fit=True)
img = qr.make_image(fill_color=fill_color, back_color=back_color)
return img.convert("RGB")
def add_logo(
self,
qr_image: Image.Image,
logo_image: Image.Image,
logo_size_ratio: float = 0.3
) -> Image.Image:
"""
Add logo to center of QR code.
Args:
qr_image: QR code image
logo_image: Logo image to overlay
logo_size_ratio: Logo size as ratio of QR code size (default 0.3)
Returns:
QR code image with logo overlay
"""
# Calculate logo size
qr_width, qr_height = qr_image.size
logo_max_size = int(min(qr_width, qr_height) * logo_size_ratio)
# Resize logo while maintaining aspect ratio
logo_image = logo_image.convert("RGBA")
logo_image.thumbnail((logo_max_size, logo_max_size), Image.Resampling.LANCZOS)
# Create white background for logo
logo_bg_size = int(logo_max_size * 1.1)
logo_bg = Image.new("RGB", (logo_bg_size, logo_bg_size), "white")
# Calculate positions
logo_bg_pos = (
(qr_width - logo_bg_size) // 2,
(qr_height - logo_bg_size) // 2
)
logo_pos = (
(logo_bg_size - logo_image.size[0]) // 2,
(logo_bg_size - logo_image.size[1]) // 2
)
# Paste logo background onto QR code
qr_image.paste(logo_bg, logo_bg_pos)
# Paste logo with transparency
absolute_logo_pos = (
logo_bg_pos[0] + logo_pos[0],
logo_bg_pos[1] + logo_pos[1]
)
qr_image.paste(logo_image, absolute_logo_pos, logo_image)
return qr_image
def generate_svg(self, scale: int = 10) -> bytes:
"""
Generate QR code as SVG.
Args:
scale: Scale factor for SVG
Returns:
SVG data as bytes
"""
qr = segno.make(self.url, error="h")
buffer = io.BytesIO()
qr.save(buffer, kind="svg", scale=scale, border=4)
return buffer.getvalue()
def resize_image(
self,
image: Image.Image,
target_size: Tuple[int, int]
) -> Image.Image:
"""
Resize image to target size.
Args:
image: PIL Image to resize
target_size: Target (width, height) in pixels
Returns:
Resized PIL Image
"""
return image.resize(target_size, Image.Resampling.LANCZOS)
def export_image(
self,
image: Image.Image,
format: str = "PNG",
quality: int = 95
) -> bytes:
"""
Export image to specified format.
Args:
image: PIL Image to export
format: Output format (PNG, JPEG, etc.)
quality: Quality for JPEG (1-100)
Returns:
Image data as bytes
"""
buffer = io.BytesIO()
if format.upper() == "JPEG":
# JPEG doesn't support transparency, convert to RGB
image = image.convert("RGB")
image.save(buffer, format=format, quality=quality, optimize=True)
else:
image.save(buffer, format=format, optimize=True)
return buffer.getvalue()