Source code for segnomms.plugin.interface

"""Main API interface for the SegnoMMS plugin.

This module contains the primary public functions that users interact with:
- write(): Generate interactive SVG from existing QR code
- write_advanced(): Generate QR code with advanced features
- register_with_segno(): Register plugin with Segno
"""

import logging
from pathlib import Path
from typing import Any, BinaryIO, Dict, Optional, TextIO, Union

from ..config import RenderingConfig
from .config import AdvancedQRConfig, create_advanced_qr_generator
from .export import _export_configuration, _generate_config_hash
from .rendering import generate_interactive_svg

# Maximum QR code size to prevent DoS attacks
MAX_QR_SIZE = 1000  # ~1000x1000 modules is very large but still reasonable


[docs] def write(qr_code: Any, out: Union[TextIO, BinaryIO, str], **kwargs: Any) -> Optional[Dict[str, Any]]: """Write an interactive SVG representation of a QR code. This is the main entry point for the Segno plugin system. It generates an SVG with custom shapes and optional interactive features. Args: qr_code: Segno QR code object to render. out: Output destination β€” file path (str), text stream, or binary stream. **kwargs: Additional rendering options. Keyword Args: Refer to the user guide for a complete list of supported options and defaults: https://segnomms.readthedocs.io/en/latest/api/main.html Raises: ValueError: If an invalid option combination is provided. TypeError: If the output type is not supported. Example: >>> import segno >>> from segnomms import write >>> qr = segno.make("Hello, World!") >>> # Basic usage >>> with open('output.svg', 'w') as f: ... write(qr, f, shape='connected', scale=15) >>> # With circle frame and centerpiece >>> with open('framed.svg', 'w') as f: ... write(qr, f, frame_shape='circle', centerpiece_enabled=True, ... centerpiece_size=0.2, centerpiece_shape='circle') """ # Create configuration from kwargs config = RenderingConfig.from_kwargs(**kwargs) # Extract export configuration options export_config = kwargs.get("export_config", True) use_hash_naming = kwargs.get("use_hash_naming", False) config_format = kwargs.get("config_format", "json") # json or yaml # Generate the SVG content svg_content = generate_interactive_svg(qr_code, config) # Track files created files_created = [] config_path = None config_hash = None # Handle output if isinstance(out, str): # File path provided - handle new naming and config export output_path = Path(out) # Generate config hash if needed for naming or export if use_hash_naming or export_config: config_hash = _generate_config_hash(config) # Generate improved filename if requested if use_hash_naming and config_hash: new_filename = f"qr_{config_hash[:8]}.svg" output_path = output_path.parent / new_filename # Write SVG file with open(output_path, "w", encoding="utf-8") as f: f.write(svg_content) files_created.append(str(output_path)) # Export configuration if requested if export_config: config_path = _export_configuration(config, output_path, config_format) if config_path: files_created.append(str(config_path)) # Return result information if hash naming or config export was used if use_hash_naming or export_config: result = { "files": files_created, "svg_file": str(output_path), "config_files": [str(config_path)] if config_path else [], "files_created": files_created, # Backward compatibility } if use_hash_naming and config_hash: result["config_hash"] = config_hash[:8] if export_config and config_path: result["config_file"] = str(config_path) # Backward compatibility return result # No special result needed for basic file output return None elif hasattr(out, "write"): # Stream provided - use type: ignore for complex Union handling if hasattr(out, "mode") and "b" in getattr(out, "mode", ""): # Binary mode - write as UTF-8 bytes out.write(svg_content.encode("utf-8")) # type: ignore[arg-type] else: # Text mode or unknown - try text first try: out.write(svg_content) # type: ignore[call-overload] except (TypeError, AttributeError): # Fallback to binary out.write(svg_content.encode("utf-8")) # type: ignore[arg-type] return None else: # Unsupported output type raise TypeError(f"Unsupported output type: {type(out)}")
[docs] def write_advanced(content: str, out: Union[TextIO, BinaryIO, str], **kwargs: Any) -> Dict[str, Any]: """Write advanced QR code(s) with ECI, mask patterns, or structured append. This function provides enhanced QR code generation with advanced features including international character encoding, manual mask pattern selection, and multi-symbol structured append sequences. Args: content: Text content to encode out: Output path or stream for the generated QR code(s) **kwargs: Advanced configuration options: Basic Parameters: * scale (int): Module size in pixels (default: 10) * border (int): Quiet zone modules (default: 4) * dark (str): Dark module color (default: 'black') * light (str): Light module color (default: 'white') Advanced QR Generation: * eci_enabled (bool): Enable Extended Channel Interpretation * mask_pattern (int): Manual mask pattern selection (0-7) * structured_append (bool): Enable structured append for long content * auto_mask (bool): Use automatic mask pattern selection (default: True) * boost_error (bool): Use higher error correction when possible Export Options: * export_config (bool): Export configuration file (default: True) * use_hash_naming (bool): Use content-based filenames (default: False) * config_format (str): Configuration format - 'json' or 'yaml' (default: 'json') Rendering Options: All standard rendering options from write() are supported Returns: dict: Generation result containing: - files (list): List of all generated files - config_files (list): Configuration files created - warnings (list): Any generation warnings - advanced_config (dict): Advanced QR configuration used - fallback_used (bool): Whether fallback generation was used Example: >>> result = write_advanced( ... "Hello, δΈ–η•Œ! This is a long message with international characters.", ... "output.svg", ... eci_enabled=True, ... structured_append=True, ... export_config=True ... ) >>> print(f"Generated {len(result['files'])} files") """ # Parse and validate advanced configuration advanced_keys = { "eci_enabled", "mask_pattern", "structured_append", "symbol_count", "auto_mask", "boost_error", "encoding", "multi_symbol", } qr_keys = {"error", "version", "mode"} # Alternative parameter names for backward compatibility alternative_names = { "multi_symbol": "structured_append", "qr_eci": "eci_enabled", "qr_encoding": "encoding", "qr_mask": "mask_pattern", "qr_symbol_count": "symbol_count", } advanced_params = {} qr_params = {} # Separate parameters by target for key, value in kwargs.items(): # Check if it's an alternative name first if key in alternative_names: mapped_key = alternative_names[key] advanced_params[mapped_key] = value elif key in advanced_keys: advanced_params[key] = value elif key in qr_keys: qr_params[key] = value # Handle special parameter formats for backward compatibility if "structured_append" in advanced_params: value = advanced_params["structured_append"] if isinstance(value, dict): # Convert dictionary format to boolean (enabled flag) advanced_params["structured_append"] = value.get("enabled", False) # Extract symbol_count if provided if "total" in value: advanced_params["symbol_count"] = value["total"] # Create advanced QR configuration try: advanced_config = AdvancedQRConfig(**advanced_params) except Exception as e: raise ValueError(f"Invalid advanced QR configuration: {e}") from e # Set defaults for QR generation error_level = qr_params.get("error", "M") version = qr_params.get("version", None) # Generate QR code(s) with advanced features generator = create_advanced_qr_generator() try: result = generator.generate_qr(content, advanced_config, error_level, version) except Exception as e: raise RuntimeError(f"Advanced QR generation failed: {e}") from e # Extract export configuration options export_config = kwargs.get("export_config", True) use_hash_naming = kwargs.get("use_hash_naming", False) config_format = kwargs.get("config_format", "json") # Handle output for single QR or sequence files_created = [] config_files_created = [] # Filter out advanced-only parameters from rendering config rendering_kwargs = {k: v for k, v in kwargs.items() if k not in advanced_keys} rendering_config = RenderingConfig.from_kwargs(**rendering_kwargs) if result.is_sequence and len(result.qr_codes) > 1: # Handle structured append sequence # Sequences require file path output, not streams if hasattr(out, "write"): raise ValueError( "Structured append sequences cannot be written to streams. " "Please provide a file path for multi-QR output." ) # Validate that out is a valid string path if not isinstance(out, (str, Path)): raise TypeError( f"Output must be a file path (str/Path) for sequences, " f"got {type(out).__name__}" ) base_path = str(out) # Generate sequence filenames if base_path.endswith(".svg"): base_name = base_path[:-4] extension = ".svg" else: base_name = base_path extension = "" # Generate each QR in sequence for i, qr in enumerate(result.qr_codes): if use_hash_naming: # Generate config with sequence metadata seq_config = rendering_config.model_copy() seq_config.metadata = seq_config.metadata or {} seq_config.metadata["sequence_index"] = i + 1 seq_config.metadata["sequence_total"] = len(result.qr_codes) config_hash = _generate_config_hash(seq_config) sequence_filename = f"qr_{config_hash[:8]}_seq{i + 1:02d}{extension}" sequence_path = Path(base_path).parent / sequence_filename else: # Standard sequence naming: base-03-01.svg, base-03-02.svg, etc. total = len(result.qr_codes) sequence_filename = f"{Path(base_name).name}-{total:02d}-{i + 1:02d}{extension}" sequence_path = Path(base_path).parent / sequence_filename # Generate SVG for this QR svg_content = generate_interactive_svg(qr, rendering_config) with open(sequence_path, "w", encoding="utf-8") as f: f.write(svg_content) files_created.append(str(sequence_path)) # Export config for each sequence item if requested if export_config: if use_hash_naming: # Add metadata to hash-named config hash_metadata = { "sequence_index": i + 1, "sequence_total": len(result.qr_codes), "content": content, "advanced_config": advanced_config.model_dump(), } config_path = _export_configuration( seq_config, sequence_path, config_format, hash_metadata ) else: # Add sequence metadata including content and advanced config sequence_metadata = { "sequence_index": i + 1, "sequence_total": len(result.qr_codes), "content": content, "advanced_config": advanced_config.model_dump(), } config_path = _export_configuration( rendering_config, sequence_path, config_format, sequence_metadata, ) if config_path: config_files_created.append(str(config_path)) else: # Single QR code output qr = result.qr_codes[0] # Generate SVG svg_content = generate_interactive_svg(qr, rendering_config) # Check if output is a stream or file path if hasattr(out, "write"): # Stream output (StringIO, file object, etc.) if hasattr(out, "mode") and "b" in getattr(out, "mode", ""): # Binary mode - write as UTF-8 bytes out.write(svg_content.encode("utf-8")) # type: ignore[arg-type] else: # Text mode out.write(svg_content) # type: ignore[call-overload] # For streams, we don't track file creation else: # File path output # Validate that out is a valid string path if not isinstance(out, (str, Path)): raise TypeError( f"Output must be a file path (str/Path) or file-like object, " f"got {type(out).__name__}" ) output_path = Path(str(out)) if use_hash_naming: config_hash = _generate_config_hash(rendering_config) new_filename = f"qr_{config_hash[:8]}.svg" output_path = output_path.parent / new_filename with open(output_path, "w", encoding="utf-8") as f: f.write(svg_content) files_created.append(str(output_path)) # Export configuration if requested if export_config: # Create metadata with content and advanced config metadata = { "content": content, "advanced_config": advanced_config.model_dump(), } config_path = _export_configuration(rendering_config, output_path, config_format, metadata) if config_path: config_files_created.append(str(config_path)) # Return comprehensive result return { "success": True, "files_created": files_created, "config_files_created": config_files_created, "files": files_created, # Backward compatibility alias "config_files": config_files_created, # Backward compatibility alias "warnings": result.warnings, "qr_count": len(result.qr_codes), "is_sequence": result.is_sequence, "metadata": result.metadata, "advanced_config": advanced_config.model_dump(), "fallback_used": getattr(result, "fallback_used", False), "export_config": export_config, }
[docs] def register_with_segno() -> bool: """Register the SegnoMMS plugin with Segno. This function attempts to register the write function as a plugin for the Segno library. Note that in modern Segno versions, plugin registration typically happens automatically via entry points defined in pyproject.toml. Returns: bool: True if registration successful or already registered, False otherwise """ try: import segno # Check if the plugin is already registered via entry points qr = segno.make("test") if hasattr(qr, "to_interactive_svg"): logging.info("SegnoMMS plugin already registered via entry points") return True # Try legacy plugin registration (for older Segno versions) try: import segno.plugins segno.plugins.register(write, "interactive_svg") logging.info("SegnoMMS plugin registered successfully with legacy API") return True except ImportError: # Modern Segno versions don't have segno.plugins # Entry point registration should handle this automatically logging.info("Using entry point registration (modern Segno)") return True except AttributeError: # Plugin method doesn't exist in this Segno version logging.warning("Legacy plugin registration not supported in this Segno version") return False except ImportError: logging.warning("Segno not available - plugin registration skipped") return False except Exception as e: logging.error(f"Failed to register SegnoMMS plugin with Segno: {e}") return False