"""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,
},
]