123 lines
4.5 KiB
Python
Executable File
123 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# /// script
|
|
# dependencies = [
|
|
# "qrcode[pil]>=7.4",
|
|
# "pillow>=10.0.0",
|
|
# ]
|
|
# ///
|
|
"""
|
|
Haumdaucher QR Code Generator with Centered Logo.
|
|
Encodes any URL or string into a high-error-correction QR code (ERROR_CORRECT_H),
|
|
embeds the Haumdaucher logo in the center with a clean white rounded badge,
|
|
and outputs MEDIA:<path> for direct Telegram inline photo delivery.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from PIL import Image, ImageDraw
|
|
import qrcode
|
|
from qrcode.constants import ERROR_CORRECT_H
|
|
|
|
DEFAULT_LOGO_PATH = Path(__file__).resolve().parent.parent / "assets" / "haumdaucher_logo.png"
|
|
|
|
def generate_qr_code(text: str, output_path: str = None, logo_path: str = None) -> str:
|
|
if not text:
|
|
raise ValueError("Text/URL to encode cannot be empty.")
|
|
|
|
if not output_path:
|
|
text_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()[:8]
|
|
output_path = f"/tmp/qr_{text_hash}.png"
|
|
|
|
# Resolve logo path
|
|
logo_file = Path(logo_path) if logo_path else DEFAULT_LOGO_PATH
|
|
if not logo_file.exists():
|
|
# Fallback to current script dir or assets if relative path differs
|
|
logo_file = DEFAULT_LOGO_PATH
|
|
|
|
# Initialize QR code generator with High error correction (30% redundancy)
|
|
qr = qrcode.QRCode(
|
|
version=None, # Auto-detect best version
|
|
error_correction=ERROR_CORRECT_H,
|
|
box_size=12,
|
|
border=4,
|
|
)
|
|
qr.add_data(text)
|
|
qr.make(fit=True)
|
|
|
|
# Generate base QR code image (RGBA for clean compositing)
|
|
qr_img = qr.make_image(fill_color="black", back_color="white").convert("RGBA")
|
|
qr_width, qr_height = qr_img.size
|
|
|
|
# Overlay logo if available
|
|
if logo_file.exists():
|
|
try:
|
|
logo = Image.open(logo_file).convert("RGBA")
|
|
|
|
# Logo should occupy roughly 22% of the QR code dimensions to ensure readability
|
|
max_logo_size = int(qr_width * 0.23)
|
|
|
|
# Maintain aspect ratio
|
|
logo_w, logo_h = logo.size
|
|
scale = min(max_logo_size / logo_w, max_logo_size / logo_h)
|
|
new_w = int(logo_w * scale)
|
|
new_h = int(logo_h * scale)
|
|
logo = logo.resize((new_w, new_h), Image.Resampling.LANCZOS)
|
|
|
|
# Create a white rounded badge behind the logo for contrast and module separation
|
|
badge_pad = 8
|
|
badge_w = new_w + badge_pad * 2
|
|
badge_h = new_h + badge_pad * 2
|
|
badge = Image.new("RGBA", (badge_w, badge_h), (0, 0, 0, 0))
|
|
draw = ImageDraw.Draw(badge)
|
|
radius = 12
|
|
draw.rounded_rectangle(
|
|
[(0, 0), (badge_w - 1, badge_h - 1)],
|
|
radius=radius,
|
|
fill=(255, 255, 255, 255),
|
|
outline=(220, 220, 220, 255),
|
|
width=1,
|
|
)
|
|
|
|
# Paste logo onto the white badge
|
|
badge.paste(logo, (badge_pad, badge_pad), mask=logo)
|
|
|
|
# Paste badge into the center of the QR code
|
|
pos_x = (qr_width - badge_w) // 2
|
|
pos_y = (qr_height - badge_h) // 2
|
|
qr_img.paste(badge, (pos_x, pos_y), mask=badge)
|
|
except Exception as e:
|
|
sys.stderr.write(f"Warning: could not overlay logo: {e}\n")
|
|
|
|
# Ensure output directory exists and save
|
|
out_file = Path(output_path)
|
|
out_file.parent.mkdir(parents=True, exist_ok=True)
|
|
qr_img.save(str(out_file), format="PNG")
|
|
|
|
return str(out_file)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Generate a Haumdaucher-branded QR code.")
|
|
parser.add_argument("text", nargs="?", help="Text or URL to encode into the QR code")
|
|
parser.add_argument("--text", "-t", dest="text_opt", help="Text or URL to encode")
|
|
parser.add_argument("--output", "-o", help="Output file path (default: /tmp/qr_<hash>.png)")
|
|
parser.add_argument("--logo", "-l", help="Custom path to center logo PNG")
|
|
parser.add_argument("--quiet", "-q", action="store_true", help="Only output MEDIA tag")
|
|
|
|
args = parser.parse_args()
|
|
raw_text = args.text or args.text_opt
|
|
if not raw_text:
|
|
parser.error("You must provide text or a URL to encode.")
|
|
|
|
out_path = generate_qr_code(raw_text, output_path=args.output, logo_path=args.logo)
|
|
|
|
# Standard OpenClaw / Telegram media dispatch tag
|
|
print(f"MEDIA:{out_path}")
|
|
if not args.quiet:
|
|
sys.stderr.write(f"QR code successfully generated for: {raw_text}\nSaved to: {out_path}\n")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|