Files
mop-skills/skills/qr-code/scripts/generate_qr.py
T

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()