feat(view): save 2D board view to file instead of base64 (#161)
* Feat: save 2D board view to file instead of returning base64 The 2D view was returning base64-encoded image data in JSON, which often exceeded token/message size limits. Now saves the rendered image (PNG/JPG/SVG) next to the PCB file and returns the file path. This makes the output usable by tools that can read image files directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(board): add opt-in responseMode param to get_board_2d_view Add a responseMode string parameter (enum: inline | file, default inline) so callers can choose how the rendered image is delivered. - inline (default, pre-PR behavior): image bytes are base64-encoded and returned in the imageData response field -- backward-compatible. - file: image is written next to the .kicad_pcb as <board>_2d_view.<ext> and filePath is returned -- resolves the MCP message-size limit problem on large boards. Rendering logic is shared between both modes; only response packaging differs. Updated tool schema (Python + TypeScript) and replaced the existing test file with 5 focused unit tests covering inline/file modes for PNG and SVG formats plus the default-is-inline contract. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,7 +73,12 @@ class BoardViewCommands:
|
||||
}
|
||||
|
||||
def get_board_2d_view(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Get a 2D image of the PCB"""
|
||||
"""Get a 2D image of the PCB.
|
||||
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
@@ -87,6 +92,7 @@ class BoardViewCommands:
|
||||
height = params.get("height", 600)
|
||||
format = params.get("format", "png")
|
||||
layers = params.get("layers", [])
|
||||
response_mode = params.get("responseMode", "inline")
|
||||
|
||||
# Create plot controller
|
||||
plotter = pcbnew.PLOT_CONTROLLER(self.board)
|
||||
@@ -124,36 +130,48 @@ class BoardViewCommands:
|
||||
|
||||
plotter.ClosePlot()
|
||||
|
||||
# Convert SVG to requested format
|
||||
# Determine output path next to the PCB file
|
||||
board_dir = os.path.dirname(self.board.GetFileName())
|
||||
board_name = os.path.splitext(os.path.basename(self.board.GetFileName()))[0]
|
||||
|
||||
# --- Render to bytes (shared for both response modes) ---
|
||||
if format == "svg":
|
||||
with open(temp_svg, "r") as f:
|
||||
svg_data = f.read()
|
||||
with open(temp_svg, "rb") as f:
|
||||
image_bytes = f.read()
|
||||
os.remove(temp_svg)
|
||||
return {"success": True, "imageData": svg_data, "format": "svg"}
|
||||
mime_format = "svg"
|
||||
else:
|
||||
# Use PIL to convert SVG to PNG/JPG
|
||||
from cairosvg import svg2png
|
||||
|
||||
png_data = svg2png(url=temp_svg, output_width=width, output_height=height)
|
||||
image_bytes = svg2png(url=temp_svg, output_width=width, output_height=height)
|
||||
os.remove(temp_svg)
|
||||
|
||||
if format == "jpg":
|
||||
# Convert PNG to JPG
|
||||
img = Image.open(io.BytesIO(png_data))
|
||||
jpg_buffer = io.BytesIO()
|
||||
img.convert("RGB").save(jpg_buffer, format="JPEG")
|
||||
jpg_data = jpg_buffer.getvalue()
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": base64.b64encode(jpg_data).decode("utf-8"),
|
||||
"format": "jpg",
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": True,
|
||||
"imageData": base64.b64encode(png_data).decode("utf-8"),
|
||||
"format": "png",
|
||||
}
|
||||
img = Image.open(io.BytesIO(image_bytes))
|
||||
buf = io.BytesIO()
|
||||
img.convert("RGB").save(buf, format="JPEG")
|
||||
image_bytes = buf.getvalue()
|
||||
mime_format = format
|
||||
|
||||
# --- Package response according to responseMode ---
|
||||
if response_mode == "file":
|
||||
output_path = os.path.join(board_dir, f"{board_name}_2d_view.{mime_format}")
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
return {
|
||||
"success": True,
|
||||
"format": mime_format,
|
||||
"filePath": output_path,
|
||||
"message": f"2D view saved to {output_path}",
|
||||
}
|
||||
else:
|
||||
# inline mode: base64-encode and return imageData
|
||||
image_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
||||
return {
|
||||
"success": True,
|
||||
"format": mime_format,
|
||||
"imageData": image_b64,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting board 2D view: {str(e)}")
|
||||
|
||||
Reference in New Issue
Block a user