# Core_Nodes/qrcode_node.py import torch import numpy as np import traceback # For detailed error printing import logging logger = logging.getLogger(__name__) # Try to import the bundled qrcodegen module try: from . import qrcodegen qrcodegen_available = True except ImportError: logger.error("QRCodeNode - Could not import the bundled 'qrcodegen.py'.") logger.error("Please ensure 'qrcodegen.py' is in the same directory as 'qrcode_node.py'.") logger.error("Download it from: https://github.com/nayuki/QR-Code-generator/blob/master/python/qrcodegen.py") qrcodegen_available = False except Exception as e: logger.error(f"QRCodeNode - An unexpected error occurred importing qrcodegen: {e}") qrcodegen_available = False # --- Helper Function for Color Parsing --- def hex_to_rgb(hex_color): """Converts a hex color string (e.g., '#FF0000') to an RGB tuple (0-255). Returns None on error.""" hex_color = hex_color.lstrip('#') if len(hex_color) != 6: return None try: return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) except ValueError: return None # --- The Node Class --- class QRCodeNode: """ Generates a QR Code image from input text (e.g., URL). Allows customization of colors, scale, border, optional logo overlay, and optional transparent background. Supports RGBA input logos. Uses the bundled 'qrcodegen.py'. Requires 'qrcodegen.py' in the same directory. NOTE: ComfyUI preview might not display transparency correctly; verify with SaveImage (PNG). """ def __init__(self): pass @classmethod def INPUT_TYPES(cls): return { "required": { "text": ("STRING", {"multiline": True, "default": "https://civitai.com/user/Isulion"}), "error_correction": (["L", "M", "Q", "H"], {"default": "H"}), "scale": ("INT", {"default": 10, "min": 1, "max": 100, "step": 1, "display": "number"}), "border": ("INT", {"default": 4, "min": 0, "max": 40, "step": 1, "display": "number"}), "foreground_color": ("STRING", {"default": "#000000", "multiline": False}), "background_color": ("STRING", {"default": "#FFFFFF", "multiline": False}), "transparent_background": ("BOOLEAN", {"default": False}), # Option for transparency }, "optional": { "logo": ("IMAGE",), # Optional input image (expects B, H, W, C float tensor) "logo_scale_percent": ("FLOAT", {"default": 23.0, "min": 5.0, "max": 33.0, "step": 0.5, "display": "number"}), "clear_behind_logo": ("BOOLEAN", {"default": True}), } } RETURN_TYPES = ("IMAGE",) # Output is IMAGE type, ComfyUI handles RGBA (B, H, W, 4) FUNCTION = "generate_qr_code" CATEGORY = "image" def generate_qr_code(self, text, error_correction, scale, border, foreground_color, background_color, transparent_background, logo=None, logo_scale_percent=23.0, clear_behind_logo=True): # Determine output channels and create a dummy tensor for error returns output_channels = 4 if transparent_background else 3 # Create dummy tensor matching expected output channels dummy_tensor = torch.zeros((1, 64, 64, output_channels), dtype=torch.float32) if not qrcodegen_available: logger.error("QRCodeNode - Bundled 'qrcodegen' module is not available.") return (dummy_tensor,) data_to_encode = text # --- 1. Parse Colors and Define Final Color Tuples (RGB or RGBA) --- fg_rgb = hex_to_rgb(foreground_color) or (0, 0, 0) bg_rgb = hex_to_rgb(background_color) or (255, 255, 255) if transparent_background: logger.info("QRCodeNode - Generating RGBA image with transparent background.") # Foreground is opaque black/color, Background is transparent white/color final_fg_color = fg_rgb + (255,) # RGBA, Alpha=255 (Opaque) final_bg_color = bg_rgb + (0,) # RGBA, Alpha=0 (Transparent) final_channels = 4 else: # Standard RGB final_fg_color = fg_rgb # RGB final_bg_color = bg_rgb # RGB final_channels = 3 # Handle empty text input gracefully if not data_to_encode: logger.warning("QRCodeNode - Input text is empty. Generating blank QR code area.") img_size = max(64, (border * 2 + 21) * scale) # Estimate minimum size try: # Use the determined final background color and channels img_np = np.full((img_size, img_size, final_channels), final_bg_color, dtype=np.uint8) img_tensor_out = torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0).cpu() return (img_tensor_out,) except Exception as e: logger.error(f"QRCodeNode - Failed to create blank image: {e}") return (dummy_tensor,) # --- 2. Generate QR Code Structure --- ecl_map = { "L": qrcodegen.QrCode.Ecc.LOW, "M": qrcodegen.QrCode.Ecc.MEDIUM, "Q": qrcodegen.QrCode.Ecc.QUARTILE, "H": qrcodegen.QrCode.Ecc.HIGH } ecl = ecl_map.get(error_correction, qrcodegen.QrCode.Ecc.HIGH) try: qr = qrcodegen.QrCode.encode_text(data_to_encode, ecl) except qrcodegen.DataTooLongError as e: logger.error(f"QRCodeNode - Data too long. Reduce text or lower Error Correction ('{error_correction}'). Details: {str(e)}") return (dummy_tensor,) except ValueError as e: logger.error(f"QRCodeNode - Value error during encoding (invalid chars?). Details: {str(e)}") return (dummy_tensor,) except Exception as e: logger.error(f"QRCodeNode - Error generating QR code structure: {str(e)}\n{traceback.format_exc()}") return (dummy_tensor,) # --- 3. Draw QR Code Base Image (RGB or RGBA) --- qr_size = qr.get_size() if qr_size <= 0: logger.error("QRCodeNode - QR Code generation resulted in zero size.") return (dummy_tensor,) module_size = qr_size + border * 2 image_size = module_size * scale if image_size <= 0 or image_size > 16384: # Size check logger.error(f"QRCodeNode - Invalid calculated image size ({image_size}x{image_size}). Check scale/border.") return (dummy_tensor,) # Create base image with the correct number of channels and background color try: # Use final_channels and final_bg_color determined earlier img_np = np.full((image_size, image_size, final_channels), final_bg_color, dtype=np.uint8) except MemoryError: logger.error(f"QRCodeNode - Not enough memory for {image_size}x{image_size}x{final_channels} image.") return (dummy_tensor,) except Exception as e: logger.error(f"QRCodeNode - Failed to create numpy array for QR code: {e}") return (dummy_tensor,) # Draw the QR code modules using the final foreground color (RGB or RGBA). # Vectorized: build a boolean module matrix, upscale it with np.kron, # then assign the foreground color through the mask (no per-module loops). modules = np.fromiter( (qr.get_module(x, y) for y in range(qr_size) for x in range(qr_size)), dtype=bool, count=qr_size * qr_size, ).reshape(qr_size, qr_size) padded_modules = np.pad(modules, pad_width=border, constant_values=False) fg_mask = np.kron(padded_modules, np.ones((scale, scale), dtype=bool)) img_np[fg_mask] = final_fg_color # --- 4. Handle Optional Logo --- if logo is not None and logo.numel() > 0: # Warnings about ECL vs logo size if ecl == qrcodegen.QrCode.Ecc.LOW: logger.warning("QRCodeNode - Using logo with LOW ECL is not recommended.") elif ecl == qrcodegen.QrCode.Ecc.MEDIUM and logo_scale_percent > 20: logger.warning("QRCodeNode - Logo > 20% with MEDIUM ECL might impact scannability.") try: # --- 4a. Prepare Input Logo --- logo_input_tensor = logo # B, H, W, C float if logo_input_tensor.shape[0] > 1: logger.warning("QRCodeNode - Multiple logos in batch, using only the first one.") logo_np_float = logo_input_tensor[0].cpu().numpy() # H, W, C float [0, 1] # Check input logo channels and handle grayscale input_logo_channels = logo_np_float.shape[2] if logo_np_float.ndim == 3 else 1 if logo_np_float.ndim == 2: # Grayscale (H, W) logger.info("QRCodeNode - Input logo is grayscale, converting to RGB.") logo_np_float = np.stack((logo_np_float,) * 3, axis=-1) # -> H, W, 3 input_logo_channels = 3 elif input_logo_channels == 1: # Grayscale (H, W, 1) logger.info("QRCodeNode - Input logo is grayscale (1 channel), converting to RGB.") logo_np_float = np.concatenate((logo_np_float,) * 3, axis=-1) # -> H, W, 3 input_logo_channels = 3 elif input_logo_channels == 4: # RGBA logger.info("QRCodeNode - Input logo has alpha channel.") elif input_logo_channels != 3: # RGB logger.warning(f"QRCodeNode - Input logo has unsupported channel count ({input_logo_channels}). Treating as RGB.") logo_np_float = logo_np_float[:, :, :3] # Take first 3 channels if > 4? Risky. Better to just treat as RGB. input_logo_channels = 3 # Convert prepared logo (RGB or RGBA float) to uint8 logo_np_uint8 = np.clip(logo_np_float * 255.0, 0, 255).astype(np.uint8) logo_h, logo_w = logo_np_uint8.shape[:2] # --- 4b. Calculate Logo Size and Position --- qr_data_area_size_pixels = qr_size * scale max_logo_dim = max(1, int(qr_data_area_size_pixels * (logo_scale_percent / 100.0))) aspect_ratio = logo_w / logo_h if logo_h > 0 else 1 target_w = max_logo_dim if logo_w >= logo_h else max(1, int(max_logo_dim * aspect_ratio)) target_h = max_logo_dim if logo_h > logo_w else max(1, int(max_logo_dim / aspect_ratio)) if target_w <= 0 or target_h <= 0: logger.warning("QRCodeNode - Calculated logo dimensions are zero/negative. Skipping overlay.") else: # --- 4c. Resize Logo --- # Resize using torch interpolation (requires float NCHW) logo_tensor_for_resize = torch.from_numpy(logo_np_uint8.astype(np.float32) / 255.0).permute(2, 0, 1).unsqueeze(0).cpu() resized_logo_tensor = torch.nn.functional.interpolate( logo_tensor_for_resize, size=(target_h, target_w), mode='bicubic', align_corners=False ) # Convert back to HWC uint8 NumPy array resized_logo_np = (resized_logo_tensor.squeeze(0).permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8) resized_logo_channels = resized_logo_np.shape[2] # --- 4d. Calculate Placement --- offset_x = max(0, (image_size - target_w) // 2) offset_y = max(0, (image_size - target_h) // 2) slice_y = slice(offset_y, min(offset_y + target_h, image_size)) slice_x = slice(offset_x, min(offset_x + target_w, image_size)) # Adjust logo size if placement goes out of bounds bg_slice_shape = img_np[slice_y, slice_x].shape logo_to_blend = resized_logo_np[:bg_slice_shape[0], :bg_slice_shape[1], :] if logo_to_blend.size == 0: logger.warning("QRCodeNode - Logo placement resulted in zero-sized area. Skipping overlay.") else: # --- 4e. Clear Background Area --- if clear_behind_logo: # Use the final background color (RGB or RGBA) img_np[slice_y, slice_x] = final_bg_color # --- 4f. Blend Logo onto QR Image --- # Perform calculations using float32 for precision current_bg_area_float = img_np[slice_y, slice_x].astype(np.float32) / 255.0 logo_to_blend_float = logo_to_blend.astype(np.float32) / 255.0 final_blended_area_float = current_bg_area_float # Initialize with background # Extract logo alpha (assume 1.0 if logo is RGB) logo_alpha = logo_to_blend_float[:, :, 3:4] if resized_logo_channels == 4 else np.ones_like(logo_to_blend_float[:, :, 0:1]) # Extract background alpha (use 1.0 if background is RGB) bg_alpha = current_bg_area_float[:, :, 3:4] if final_channels == 4 else np.ones_like(current_bg_area_float[:, :, 0:1]) # Blend RGB channels using "over" compositing: Co = Fa*alpha_f + Ba*(1-alpha_f) # Ensure logo has 3 channels for RGB blending logo_rgb = logo_to_blend_float[:, :, :3] bg_rgb = current_bg_area_float[:, :, :3] blended_rgb = logo_rgb * logo_alpha + bg_rgb * (1.0 - logo_alpha) # Calculate output alpha using "over" compositing: Ao = alpha_f + alpha_b*(1-alpha_f) blended_alpha = logo_alpha + bg_alpha * (1.0 - logo_alpha) if final_channels == 4: # RGBA Output final_blended_area_float = np.concatenate((blended_rgb, blended_alpha), axis=2) else: # RGB Output final_blended_area_float = blended_rgb # Discard calculated alpha # Convert back to uint8 and assign to the main image blended_area_uint8 = np.clip(final_blended_area_float * 255.0, 0, 255).astype(np.uint8) img_np[slice_y, slice_x] = blended_area_uint8 except Exception as e: logger.error(f"QRCodeNode - Error processing or overlaying logo: {str(e)}\n{traceback.format_exc()}") # Continue without logo if processing fails # --- 5. Convert Final Image to Torch Tensor --- try: # Convert the final NumPy array (potentially RGBA) to tensor img_tensor_out = torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0).cpu() # Final check on tensor shape if img_tensor_out.shape[3] != final_channels: logger.error(f"QRCodeNode - Final tensor channel mismatch! Expected {final_channels}, got {img_tensor_out.shape[3]}. Returning dummy.") return (dummy_tensor,) # print(f"DEBUG: QRCodeNode - Output tensor shape: {img_tensor_out.shape}") # Optional debug except Exception as e: logger.error(f"QRCodeNode - Failed to convert final numpy array to tensor: {e}") return (dummy_tensor,) return (img_tensor_out,)