""" Board view command implementations for KiCAD interface """ import base64 import io import logging import os from typing import Any, Dict, List, Optional, Tuple import pcbnew from PIL import Image logger = logging.getLogger("kicad_interface") def _svg_to_png(svg_path: str, width: int, height: int) -> Optional[bytes]: """Convert SVG to PNG. No cffi dependency. Priority: 1. pymupdf (fitz) — bundled MuPDF renderer, pure Python, no system deps 2. Inkscape CLI — accurate KiCAD SVG rendering 3. ImageMagick convert — broad availability fallback Returns PNG bytes or None if all converters fail. """ import subprocess import tempfile try: import fitz doc = fitz.open(svg_path) page = doc[0] mat = fitz.Matrix(width / page.rect.width, height / page.rect.height) return page.get_pixmap(matrix=mat).tobytes("png") except Exception: pass out_path = os.path.join(tempfile.mkdtemp(), "out.png") try: r = subprocess.run( [ "inkscape", svg_path, "--export-type=png", f"--export-width={width}", f"--export-height={height}", f"--export-filename={out_path}", ], capture_output=True, timeout=60, ) if r.returncode == 0 and os.path.exists(out_path): with open(out_path, "rb") as f: return f.read() except (FileNotFoundError, subprocess.TimeoutExpired): pass try: r = subprocess.run( ["convert", "-density", "150", svg_path, "-resize", f"{width}x{height}", out_path], capture_output=True, timeout=60, ) if r.returncode == 0 and os.path.exists(out_path): with open(out_path, "rb") as f: return f.read() except (FileNotFoundError, subprocess.TimeoutExpired): pass return None class BoardViewCommands: """Handles board viewing operations""" def __init__(self, board: Optional[pcbnew.BOARD] = None): """Initialize with optional board instance""" self.board = board def get_board_info(self, params: Dict[str, Any]) -> Dict[str, Any]: """Get information about the current board""" try: if not self.board: return { "success": False, "message": "No board is loaded", "errorDetails": "Load or create a board first", } # Get board dimensions board_box = self.board.GetBoardEdgesBoundingBox() width_nm = board_box.GetWidth() height_nm = board_box.GetHeight() # Convert to mm width_mm = width_nm / 1000000 height_mm = height_nm / 1000000 # Get layer information layers = [] for layer_id in range(pcbnew.PCB_LAYER_ID_COUNT): if self.board.IsLayerEnabled(layer_id): layers.append( { "name": self.board.GetLayerName(layer_id), "type": self._get_layer_type_name(self.board.GetLayerType(layer_id)), "id": layer_id, } ) return { "success": True, "board": { "filename": self.board.GetFileName(), "size": {"width": width_mm, "height": height_mm, "unit": "mm"}, "layers": layers, "title": self.board.GetTitleBlock().GetTitle(), # Note: activeLayer removed - GetActiveLayer() doesn't exist in KiCAD 9.0 # Active layer is a UI concept not applicable to headless scripting }, } except Exception as e: logger.error(f"Error getting board info: {str(e)}") return { "success": False, "message": "Failed to get board information", "errorDetails": str(e), } def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]: """Render PCB to PNG/JPG/SVG via kicad-cli (no pcbnew/cffi dependency). responseMode controls how the image is returned: - "inline" (default): image bytes are base64-encoded and returned as ``imageData``. - "file": image is written next to the .kicad_pcb file and ``filePath`` is returned. """ import glob import shutil import subprocess import tempfile try: pcb_path = params.get("pcbPath") if not pcb_path: if self.board: pcb_path = self.board.GetFileName() if not pcb_path: return { "success": False, "message": "pcbPath required", "errorDetails": "Provide pcbPath or load a board first", } if not os.path.exists(pcb_path): return { "success": False, "message": f"PCB file not found: {pcb_path}", "errorDetails": pcb_path, } width = params.get("width", 1600) height = params.get("height", 1200) fmt = params.get("format", "png") if fmt not in ("png", "jpg", "svg"): return { "success": False, "message": f"Unsupported format '{fmt}'. Use 'png', 'jpg', or 'svg'.", "errorDetails": f"Got: {fmt}", } layers: List[str] = params.get("layers", []) response_mode = params.get("responseMode", "inline") kicad_cli = shutil.which("kicad-cli") or shutil.which("kicad-cli.exe") if not kicad_cli: return { "success": False, "message": "kicad-cli not found in PATH", "errorDetails": "Install KiCad and ensure kicad-cli is on PATH", } with tempfile.TemporaryDirectory() as tmpdir: # KiCad 10 changed `pcb export svg`: # - `--output` is now a FILE path, not a directory (a directory # fails with "Failed to create file '