feat(file-tool): add image_bundle and svg actions

This commit is contained in:
Apple
2026-02-15 02:33:42 -08:00
parent 36314a871f
commit aad5870e81
2 changed files with 152 additions and 1 deletions

View File

@@ -18,6 +18,8 @@ from typing import Dict, List, Any, Optional
from dataclasses import dataclass
from io import BytesIO, StringIO
from pathlib import PurePath
import xml.etree.ElementTree as ET
from xml.sax.saxutils import escape as xml_escape
from zipfile import ZIP_DEFLATED, ZipFile
logger = logging.getLogger(__name__)
@@ -331,7 +333,8 @@ TOOL_DEFINITIONS = [
"json_export", "yaml_export", "zip_bundle",
"text_create", "text_update", "markdown_create", "markdown_update",
"xml_export", "html_export",
"image_create", "image_edit", "image_convert"
"image_create", "image_edit", "image_convert", "image_bundle",
"svg_export", "svg_to_png"
],
"description": "Дія file tool"
},
@@ -640,6 +643,12 @@ class ToolManager:
return self._file_image_edit(args)
if action == "image_convert":
return self._file_image_convert(args)
if action == "image_bundle":
return self._file_image_bundle(args)
if action == "svg_export":
return self._file_svg_export(args)
if action == "svg_to_png":
return self._file_svg_to_png(args)
if action == "json_export":
return self._file_json_export(args)
if action == "yaml_export":
@@ -1178,6 +1187,142 @@ class ToolManager:
file_mime=self._mime_for_image_format(fmt),
)
def _file_image_bundle(self, args: Dict[str, Any]) -> ToolResult:
file_name = self._sanitize_file_name(args.get("file_name"), "images.zip", force_ext=".zip")
images = args.get("images") or args.get("entries") or []
if not isinstance(images, list) or not images:
return ToolResult(success=False, result=None, error="images must be non-empty array")
out = BytesIO()
with ZipFile(out, mode="w", compression=ZIP_DEFLATED) as zf:
for idx, item in enumerate(images, start=1):
if not isinstance(item, dict):
return ToolResult(success=False, result=None, error=f"images[{idx-1}] must be object")
src_b64 = item.get("file_base64")
if not src_b64:
return ToolResult(success=False, result=None, error=f"images[{idx-1}].file_base64 is required")
in_name = self._sanitize_file_name(item.get("file_name"), f"image_{idx}.bin")
zf.writestr(in_name, self._bytes_from_b64(src_b64))
payload = out.getvalue()
return ToolResult(
success=True,
result={"message": f"Image bundle created: {file_name}"},
file_base64=self._b64_from_bytes(payload),
file_name=file_name,
file_mime="application/zip",
)
@staticmethod
def _strip_ns(tag: str) -> str:
return tag.split("}", 1)[1] if "}" in tag else tag
@staticmethod
def _safe_int(value: Any, default: int = 0) -> int:
try:
text = str(value).strip()
if text.endswith("px"):
text = text[:-2]
return int(float(text))
except Exception:
return default
def _file_svg_export(self, args: Dict[str, Any]) -> ToolResult:
file_name = self._sanitize_file_name(args.get("file_name"), "image.svg", force_ext=".svg")
svg_raw = args.get("svg")
if svg_raw is not None:
payload = str(svg_raw).encode("utf-8")
return ToolResult(
success=True,
result={"message": f"SVG exported: {file_name}"},
file_base64=self._b64_from_bytes(payload),
file_name=file_name,
file_mime="image/svg+xml",
)
width = max(1, int(args.get("width") or 1024))
height = max(1, int(args.get("height") or 1024))
bg = str(args.get("background_color") or "white")
text = xml_escape(str(args.get("text") or ""))
text_color = str(args.get("text_color") or "black")
text_x = int(args.get("text_x") or 20)
text_y = int(args.get("text_y") or 40)
# Minimal deterministic SVG template for safe generation.
svg = (
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
f'viewBox="0 0 {width} {height}">'
f'<rect x="0" y="0" width="{width}" height="{height}" fill="{bg}" />'
f'<text x="{text_x}" y="{text_y}" fill="{text_color}">{text}</text>'
f"</svg>"
)
payload = svg.encode("utf-8")
return ToolResult(
success=True,
result={"message": f"SVG exported: {file_name}"},
file_base64=self._b64_from_bytes(payload),
file_name=file_name,
file_mime="image/svg+xml",
)
def _file_svg_to_png(self, args: Dict[str, Any]) -> ToolResult:
from PIL import Image, ImageColor, ImageDraw
file_name = self._sanitize_file_name(args.get("file_name"), "converted.png", force_ext=".png")
src_b64 = args.get("file_base64")
svg_text = args.get("svg")
if src_b64:
svg_text = self._bytes_from_b64(src_b64).decode("utf-8", errors="ignore")
if not svg_text:
return ToolResult(success=False, result=None, error="file_base64 or svg is required for svg_to_png")
try:
root = ET.fromstring(svg_text)
except Exception as exc:
return ToolResult(success=False, result=None, error=f"Invalid SVG: {exc}")
width = self._safe_int(root.attrib.get("width"), 1024)
height = self._safe_int(root.attrib.get("height"), 1024)
width = max(1, width)
height = max(1, height)
image = Image.new("RGBA", (width, height), color="white")
draw = ImageDraw.Draw(image)
for elem in root.iter():
tag = self._strip_ns(elem.tag)
if tag == "rect":
x = self._safe_int(elem.attrib.get("x"), 0)
y = self._safe_int(elem.attrib.get("y"), 0)
w = self._safe_int(elem.attrib.get("width"), width)
h = self._safe_int(elem.attrib.get("height"), height)
fill = elem.attrib.get("fill", "white")
try:
color = ImageColor.getrgb(fill)
except Exception:
color = (255, 255, 255)
draw.rectangle([x, y, x + max(0, w), y + max(0, h)], fill=color)
elif tag == "text":
x = self._safe_int(elem.attrib.get("x"), 0)
y = self._safe_int(elem.attrib.get("y"), 0)
fill = elem.attrib.get("fill", "black")
try:
color = ImageColor.getrgb(fill)
except Exception:
color = (0, 0, 0)
draw.text((x, y), elem.text or "", fill=color)
out = BytesIO()
image.convert("RGB").save(out, format="PNG")
payload = out.getvalue()
return ToolResult(
success=True,
result={"message": f"SVG converted to PNG: {file_name}"},
file_base64=self._b64_from_bytes(payload),
file_name=file_name,
file_mime="image/png",
)
def _file_yaml_export(self, args: Dict[str, Any]) -> ToolResult:
file_name = self._sanitize_file_name(args.get("file_name"), "export.yaml", force_ext=".yaml")
content = args.get("content")