feat: add hosting and qr-code skills
This commit is contained in:
Executable
+122
@@ -0,0 +1,122 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user