Source code for tools.qr_generator

"""QR Code Generator Tools.

Generates QR codes for URLs, text, contacts (vCard), WiFi credentials,
email, and batch generation. Supports PNG, SVG, and ASCII output.
"""

from __future__ import annotations

import asyncio
import json
import logging
import io
import base64
import tempfile
import os
from typing import Optional, TYPE_CHECKING

if TYPE_CHECKING:
    from tool_context import ToolContext

try:
    import qrcode
    from qrcode.image import svg
    import qrcode.constants
except ImportError:
    qrcode = None

logger = logging.getLogger(__name__)


[docs] class QRCodeGenerator: """QRCodeGenerator. """
[docs] def __init__(self): """Initialize the instance. """ if qrcode is None: raise ImportError("qrcode library is required but not installed")
[docs] def generate_qr_code(self, data, size=10, border=4, error_correction="M", fill_color="black", back_color="white", format_type="PNG"): """Generate qr code. Args: data: Input data payload. size: The size value. border: The border value. error_correction: The error correction value. fill_color: The fill color value. back_color: The back color value. format_type: The format type value. """ try: ec_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} qr = qrcode.QRCode(version=None, error_correction=ec_levels.get(error_correction.upper(), ec_levels["M"]), box_size=size, border=border) qr.add_data(data) qr.make(fit=True) result = {"success": True, "data": data, "size": qr.modules_count, "error_correction": error_correction.upper(), "format": format_type.upper()} if format_type.upper() == "ASCII": lines = [] for row in qr.get_matrix(): lines.append("".join("██" if cell else " " for cell in row)) result["qr_code_ascii"] = "\n".join(lines) result["mime_type"] = "text/plain" elif format_type.upper() == "SVG": img = qr.make_image(image_factory=svg.SvgImage) buffer = io.BytesIO() img.save(buffer) buffer.seek(0) result["qr_code_svg"] = buffer.getvalue().decode('utf-8') result["mime_type"] = "image/svg+xml" else: img = qr.make_image(fill_color=fill_color, back_color=back_color) buffer = io.BytesIO() img.save(buffer, format='PNG') buffer.seek(0) tmp_dir = tempfile.mkdtemp() filepath = os.path.join(tmp_dir, "qrcode.png") with open(filepath, 'wb') as f: f.write(buffer.getvalue()) result["file_path"] = filepath result["mime_type"] = "image/png" return result except Exception as e: logger.error(f"Error generating QR code: {e}") return {"success": False, "error": str(e), "data": data}
[docs] def create_vcard_qr(self, contact_info): """Create a new vcard qr. Args: contact_info: The contact info value. """ vcard_lines = ["BEGIN:VCARD", "VERSION:3.0"] if "first_name" in contact_info or "last_name" in contact_info: vcard_lines.append(f"N:{contact_info.get('last_name', '')};{contact_info.get('first_name', '')};;") if "display_name" in contact_info: vcard_lines.append(f"FN:{contact_info['display_name']}") for field, prefix in [("phone", "TEL"), ("email", "EMAIL")]: if field in contact_info: items = contact_info[field] if isinstance(contact_info[field], list) else [contact_info[field]] for item in items: vcard_lines.append(f"{prefix}:{item}") if "organization" in contact_info: vcard_lines.append(f"ORG:{contact_info['organization']}") if "title" in contact_info: vcard_lines.append(f"TITLE:{contact_info['title']}") if "website" in contact_info: vcard_lines.append(f"URL:{contact_info['website']}") vcard_lines.append("END:VCARD") return "\n".join(vcard_lines)
[docs] def create_wifi_qr(self, wifi_info): """Create a new wifi qr. Args: wifi_info: The wifi info value. """ ssid = wifi_info.get("ssid", "") if not ssid: return "Error: SSID is required for WiFi QR code" return f"WIFI:T:{wifi_info.get('security', 'WPA').upper()};S:{ssid};P:{wifi_info.get('password', '')};;"
[docs] def create_email_qr(self, email_info): """Create a new email qr. Args: email_info: The email info value. """ to = email_info.get("to", "") if not to: return "Error: Recipient email address is required" mailto_url = f"mailto:{to}" params = [] if email_info.get("subject"): params.append(f"subject={email_info['subject'].replace(' ', '%20')}") if email_info.get("body"): params.append(f"body={email_info['body'].replace(' ', '%20')}") if params: mailto_url += "?" + "&".join(params) return mailto_url
_qr_gen = QRCodeGenerator() if qrcode else None async def _generate_qr_code(data: str, size: int = 10, border: int = 4, error_correction: str = "M", fill_color: str = "black", back_color: str = "white", format_type: str = "PNG", ctx: ToolContext | None = None) -> str: """Internal helper: generate qr code. Args: data (str): Input data payload. size (int): The size value. border (int): The border value. error_correction (str): The error correction value. fill_color (str): The fill color value. back_color (str): The back color value. format_type (str): The format type value. ctx (ToolContext | None): Tool execution context providing access to bot internals. Returns: str: Result string. """ if qrcode is None: return json.dumps({"error": "QR code library not installed"}) if not data or not data.strip(): return json.dumps({"error": "Data cannot be empty"}) result = await asyncio.to_thread( _qr_gen.generate_qr_code, data.strip(), max(1, min(40, size or 10)), max(0, border or 4), error_correction or "M", fill_color or "black", back_color or "white", format_type or "PNG", ) return json.dumps(result, indent=2) async def _generate_contact_qr(contact_info: str, size: int = 10, format_type: str = "PNG", ctx: ToolContext | None = None) -> str: """Internal helper: generate contact qr. Args: contact_info (str): The contact info value. size (int): The size value. format_type (str): The format type value. ctx (ToolContext | None): Tool execution context providing access to bot internals. Returns: str: Result string. """ if qrcode is None: return json.dumps({"error": "QR code library not installed"}) try: contact_data = json.loads(contact_info) vcard_data = _qr_gen.create_vcard_qr(contact_data) if vcard_data.startswith("Error"): return json.dumps({"error": vcard_data}) result = await asyncio.to_thread( _qr_gen.generate_qr_code, vcard_data, max(1, min(40, size or 10)), format_type=format_type or "PNG", ) result["vcard_data"] = vcard_data return json.dumps(result, indent=2) except json.JSONDecodeError as e: return json.dumps({"error": f"Invalid JSON: {e}"}) async def _generate_wifi_qr(wifi_info: str, size: int = 10, format_type: str = "PNG", ctx: ToolContext | None = None) -> str: """Internal helper: generate wifi qr. Args: wifi_info (str): The wifi info value. size (int): The size value. format_type (str): The format type value. ctx (ToolContext | None): Tool execution context providing access to bot internals. Returns: str: Result string. """ if qrcode is None: return json.dumps({"error": "QR code library not installed"}) try: wifi_data = json.loads(wifi_info) wifi_string = _qr_gen.create_wifi_qr(wifi_data) if wifi_string.startswith("Error"): return json.dumps({"error": wifi_string}) result = await asyncio.to_thread( _qr_gen.generate_qr_code, wifi_string, max(1, min(40, size or 10)), format_type=format_type or "PNG", ) result["wifi_string"] = wifi_string return json.dumps(result, indent=2) except json.JSONDecodeError as e: return json.dumps({"error": f"Invalid JSON: {e}"}) async def _generate_email_qr(email_info: str, size: int = 10, format_type: str = "PNG", ctx: ToolContext | None = None) -> str: """Internal helper: generate email qr. Args: email_info (str): The email info value. size (int): The size value. format_type (str): The format type value. ctx (ToolContext | None): Tool execution context providing access to bot internals. Returns: str: Result string. """ if qrcode is None: return json.dumps({"error": "QR code library not installed"}) try: email_data = json.loads(email_info) mailto_url = _qr_gen.create_email_qr(email_data) if mailto_url.startswith("Error"): return json.dumps({"error": mailto_url}) result = await asyncio.to_thread( _qr_gen.generate_qr_code, mailto_url, max(1, min(40, size or 10)), format_type=format_type or "PNG", ) result["mailto_url"] = mailto_url return json.dumps(result, indent=2) except json.JSONDecodeError as e: return json.dumps({"error": f"Invalid JSON: {e}"}) async def _batch_generate_qr(data_list: str, size: int = 10, format_type: str = "PNG", ctx: ToolContext | None = None) -> str: """Internal helper: batch generate qr. Args: data_list (str): The data list value. size (int): The size value. format_type (str): The format type value. ctx (ToolContext | None): Tool execution context providing access to bot internals. Returns: str: Result string. """ if qrcode is None: return json.dumps({"error": "QR code library not installed"}) try: data_items = json.loads(data_list) if not isinstance(data_items, list): return json.dumps({"error": "data_list must be a JSON array"}) results = [] for i, data in enumerate(data_items): if isinstance(data, str) and data.strip(): result = await asyncio.to_thread( _qr_gen.generate_qr_code, data.strip(), max(1, min(40, size or 10)), format_type=format_type or "PNG", ) result["index"] = i results.append(result) else: results.append({"index": i, "success": False, "error": "Invalid or empty data"}) return json.dumps({"success": True, "count": len(results), "results": results}, indent=2) except json.JSONDecodeError as e: return json.dumps({"error": f"Invalid JSON: {e}"}) TOOLS = [ { "name": "generate_qr_code", "description": "Generate a QR code from text data with customizable size, colors, error correction, and output format (PNG file, SVG, or ASCII).", "parameters": { "type": "object", "properties": { "data": {"type": "string", "description": "The text/URL/data to encode in the QR code."}, "size": {"type": "integer", "description": "Size of QR code boxes (1-40, default 10)."}, "border": {"type": "integer", "description": "Border width in boxes (default 4)."}, "error_correction": {"type": "string", "description": "Error correction level: L, M, Q, H (default M)."}, "fill_color": {"type": "string", "description": "Foreground color (default black)."}, "back_color": {"type": "string", "description": "Background color (default white)."}, "format_type": {"type": "string", "description": "Output format: PNG, SVG, or ASCII (default PNG)."}, }, "required": ["data"], }, "handler": _generate_qr_code, }, { "name": "generate_contact_qr", "description": "Generate a QR code for contact information using vCard format.", "parameters": { "type": "object", "properties": { "contact_info": {"type": "string", "description": "JSON with contact fields: first_name, last_name, display_name, phone, email, organization, title, website."}, "size": {"type": "integer", "description": "Size of QR code boxes (1-40)."}, "format_type": {"type": "string", "description": "Output format: PNG, SVG, or ASCII."}, }, "required": ["contact_info"], }, "handler": _generate_contact_qr, }, { "name": "generate_wifi_qr", "description": "Generate a QR code for WiFi network connection.", "parameters": { "type": "object", "properties": { "wifi_info": {"type": "string", "description": "JSON with ssid, password, security (WPA/WEP/nopass)."}, "size": {"type": "integer", "description": "Size of QR code boxes (1-40)."}, "format_type": {"type": "string", "description": "Output format: PNG, SVG, or ASCII."}, }, "required": ["wifi_info"], }, "handler": _generate_wifi_qr, }, { "name": "generate_email_qr", "description": "Generate a QR code that opens email client with pre-filled information.", "parameters": { "type": "object", "properties": { "email_info": {"type": "string", "description": "JSON with to, subject, body."}, "size": {"type": "integer", "description": "Size of QR code boxes (1-40)."}, "format_type": {"type": "string", "description": "Output format: PNG, SVG, or ASCII."}, }, "required": ["email_info"], }, "handler": _generate_email_qr, }, { "name": "batch_generate_qr", "description": "Generate multiple QR codes from a JSON array of data strings.", "parameters": { "type": "object", "properties": { "data_list": {"type": "string", "description": "JSON array of strings to encode."}, "size": {"type": "integer", "description": "Size of QR code boxes (1-40)."}, "format_type": {"type": "string", "description": "Output format: PNG, SVG, or ASCII."}, }, "required": ["data_list"], }, "handler": _batch_generate_qr, }, ]