any-picture-format
当用户明确要求"转换图片格式"、"修改图片格式"、"图片格式转换"时使用。支持任意格式图片到目标格式的转换,包括:本地文件/网络URL/剪贴板图片输入,PNG/JPEG/WEBP 等常见格式输出,单文件或批量处理模式。核心特点:自动检测输入格式、支持透明度处理、批量处理保持原始文件名结构。⚠️ 不适用:用户只是想调整图片大小/裁剪(应使用图片编辑工具)、只是想查看图片信息(应直接使用文件查看器)、没有明确"格式转换"意图。
Install
npx skills add https://github.com/huangwb8/skills/tree/main/skills/beta/any-picture-format
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install huangwb8-skills@llmmart
git clone https://github.com/huangwb8/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole huangwb8/skills collection as a plugin from our marketplace. Git is the plain clone.
README
Any Picture Format
这个 skill 用来把图片转换成你需要的目标格式,适合单文件、批量目录、URL 图片和剪贴板图片的格式转换;如果你只是想裁剪、缩放或查看图片信息,就不该用它。
用法
最推荐用法
请使用 any-picture-format skill 将图片格式转换为目标格式。
输入:`/path/to/image.png` 或图片 URL 或剪贴板图片;目标格式如 `jpeg` / `png` / `webp`
输出:转换后的图片文件,默认保留原文件并生成新文件
进阶用法
请使用 any-picture-format skill 批量转换一个目录里的图片。
输入:图片目录 `/path/to/images`;目标格式 `webp`
输出:批量转换后的图片文件
另外,还有下列参数约束:
- 递归处理:是
- 输出策略:覆盖原文件
- 质量参数:90
能做什么
- 支持本地文件、网络 URL、剪贴板图片三种输入来源。
- 支持
PNG、JPEG/JPG、WEBP、GIF、BMP、TIFF、ICO等常见输出格式。 - 会根据格式差异处理透明度、质量参数和批量目录结构。
- 默认策略是“保留原文件,新生成目标格式文件”。
- 不适合裁剪、缩放、加水印或做复杂图像编辑。
使用示例
示例 1:转换单张图片
请使用 any-picture-format skill 将这张图片转成 JPEG。
输入:`/tmp/demo.png`
输出:`/tmp/demo.jpg`
示例 2:从 URL 转换为 PNG
请使用 any-picture-format skill 转换这个网络图片。
输入:`https://example.com/demo.webp`
输出:PNG 文件
另外,还有下列参数约束:
- 输出策略:新建文件
示例 3:批量转换整个目录
请使用 any-picture-format skill 批量转换图片目录。
输入:`./assets/raw-images`,目标格式 `webp`
输出:转换后的图片文件
另外,还有下列参数约束:
- 递归处理:是
- 质量参数:85
输出
- 单文件转换时,默认在原文件旁生成同名新文件,例如
image.png -> image.jpg。 - 批量转换时,默认保留原目录结构和原文件名,只改变扩展名。
- 默认不会覆盖原文件;只有显式要求
overwrite时才会覆盖。 - 如果是剪贴板或 URL 输入,技能会先落地临时输入,再生成目标输出。
配置
- 配置文件:
any-picture-format/config.yaml - 默认输出格式:
PNG - 默认输出策略:
new - 默认质量参数:
85 - 批量递归默认值:
false - 高价值配置项:
defaults.output_formatdefaults.output_strategydefaults.qualityoutput_formats
备选用法(脚本/硬编码)
如果你已经明确知道输入路径和目标格式,直接使用脚本会更快。
验证输入
python3 any-picture-format/scripts/convert.py validate --source /path/to/image.png
转换单张图片
python3 any-picture-format/scripts/convert.py convert \
--source /path/to/image.png \
--format JPEG \
--output /path/to/image.jpg \
--strategy new \
--quality 90
批量转换目录
python3 any-picture-format/scripts/convert.py batch \
--directory /path/to/images \
--format WEBP \
--recursive \
--strategy new \
--quality 85
常见问题
Q:PNG 转 JPEG 后透明背景怎么办?
A:JPEG 不支持透明度,通常会自动合成为纯色背景;如果你需要保留透明度,优先输出为 PNG 或 WEBP。
Q:会覆盖原文件吗?
A:默认不会。只有明确要求 overwrite,或脚本里传入 --strategy overwrite 时才会覆盖。
Q:批量转换时某个文件失败怎么办?
A:默认配置 continue_on_error: true,会继续处理其它文件;你可以根据最终报告定位失败项。
Q:这个 skill 能顺手做裁剪或缩放吗?
A:不能。它的职责是“格式转换”,不是通用图像编辑。
Skill manifest
Any Picture Format
目标
当用户明确要求"转换图片格式"、"修改图片格式"、"图片格式转换"时使用。支持任意格式图片到目标格式的转换,包括:本地文件/网络URL/剪贴板图片输入,PNG/JPEG/WEBP 等常见格式输出,单文件或批量处理模式。核心特点:自动检测输入格式、支持透明度处理、批量处理保持原始文件名结构。⚠️ 不适用:用户只是想调整图片大小/裁剪(应使用图片编辑工具)、只是想查看图片信息(应直接使用文件查看器)、没有明确"格式转换"意图。
流程
输入
输入为本地图片路径、HTTP/HTTPS URL 或 clipboard;目标格式可选,未指定时使用配置中的默认格式 PNG,批量模式还需目录和递归选项。可选输入包括输出路径、new/overwrite 策略、质量参数和配置文件中的格式列表;只在用户明确要求格式转换时触发。
执行步骤
核心功能
输入来源
| 来源类型 | 说明 | 示例 |
|---|---|---|
| 本地文件 | 支持所有常见图片格式(PNG/JPEG/WEBP/GIF/BMP/TIFF/ICO/HEIC等) | /path/to/image.png |
| 网络URL | 自动下载后转换,支持HTTP/HTTPS | https://example.com/image.webp |
| 剪贴板 | 直接读取系统剪贴板中的图片 | 用户刚截图后 |
输出格式
默认支持的目标格式(可在config.yaml中扩展):
- PNG:无损压缩,支持透明度
- JPEG:有损压缩,文件体积小
- WEBP:现代Web格式,体积与质量平衡
- GIF:支持动画(静态图片转GIF时首帧)
- BMP:无压缩Windows位图
- TIFF:专业用途,支持多页
输出策略
| 策略 | 说明 | 文件名变化 |
|---|---|---|
| 新建 | 保留原文件,生成新文件 | image.png → image.jpg |
| 覆盖 | 替换原文件 | image.png → image.png(格式已变) |
工作流程
1. 需求确认
AI需要确认以下信息(可从用户请求推断):
- 输入来源:文件路径/URL/剪贴板
- 目标格式:输出格式(默认PNG)
- 输出策略:新建/覆盖/指定路径(默认新建)
- 处理范围:单文件或批量目录
2. 输入验证
使用脚本验证输入:
python3 scripts/convert.py validate --source <输入>
验证内容:
- 输入来源是否可访问
- 文件是否为有效图片
- URL是否可下载
3. 执行转换
单文件转换
python3 scripts/convert.py convert \
--source <输入> \
--format <目标格式> \
--output <输出路径> \
--strategy <new/overwrite>
批量转换
python3 scripts/convert.py batch \
--directory <目录> \
--format <目标格式> \
--recursive \ # 递归处理子目录
--strategy <新建/覆盖>
4. 结果验证
转换后验证:
- 输出文件是否存在
- 文件格式是否正确
- 文件大小是否合理
- 图片质量是否可接受
脚本接口
scripts/convert.py
核心转换脚本,提供以下子命令:
validate - 验证输入
python3 scripts/convert.py validate --source <输入>
返回:
valid: true/falseformat: 检测到的输入格式size: 图片尺寸error: 错误信息(如有)
convert - 单文件转换
python3 scripts/convert.py convert \
--source <输入> \
--format <目标格式> \
--output <输出路径> \
--strategy <new/overwrite> \
--quality <质量参数> # 可选,1-100
返回:
success: true/falseinput_path: 输入文件路径output_path: 输出文件路径size_before: 转换前大小size_after: 转换后大小error: 错误信息(如有)
batch - 批量转换
python3 scripts/convert.py batch \
--directory <目录> \
--format <目标格式> \
--recursive \
--strategy <新建/覆盖> \
--quality <质量参数>
返回:
total: 总文件数success: 成功转换数failed: 失败数results: 每个文件的详细结果errors: 错误信息列表
AI执行指南
场景1:用户要求转换单个图片
用户:"把这个PNG转成JPEG"
AI执行:
1. 确认输入文件(从上下文获取或询问)
2. 调用 validate 验证
3. 调用 convert 转换(默认strategy=新建)
4. 报告结果(文件路径、大小变化)
场景2:批量转换文件夹
用户:"把images文件夹里的图片都转成WEBP"
AI执行:
1. 确认目录和格式
2. 调用 batch 批量转换
3. 汇总结果(成功/失败统计)
4. 报告任何失败的文件
场景3:URL图片转换
用户:"把 https://example.com/image.webp 转成PNG"
AI执行:
1. 调用 validate 验证URL可访问性
2. 调用 convert 转换(脚本会自动下载)
3. 报告结果
场景4:剪贴板图片转换
用户:"把刚才的截图转成JPEG"
AI执行:
1. 调用 convert --source clipboard
2. 指定输出路径(询问或使用默认)
3. 报告保存位置
配置参数
见config.yaml,可配置:
- 默认输出格式
- 默认输出策略
- 默认质量参数
- 支持的格式列表
输出
输出报告
转换完成后应向用户报告:
✓ 转换完成
输入:image.png (2.3 MB)
输出:image.jpg (456 KB)
压缩比:80%
保存位置:/path/to/image.jpg
批量转换时:
✓ 批量转换完成
总计:25个文件
成功:23个
失败:2个
失败文件:
- corrupted.png:图片损坏
- locked.jpg:权限不足
输出目录:/path/to/output/
输出管理
BenszAPI 任务工作区
校验
转换前运行 scripts/convert.py validate,确认来源可访问且为有效图片;转换后核对输出存在、格式与目标一致、大小合理、质量可接受,并在批量结果中报告成功数、失败数和每个错误。
失败与恢复
特殊处理
透明度处理
- PNG→JPEG:自动将透明背景替换为白色
- 带透明度的图片:保留透明度需选择PNG/WEBP格式
动画处理
- GIF→其他格式:使用 PIL 默认行为(提取首帧)
- 其他格式→GIF:生成静态 GIF(不支持动画生成)
如需动画处理,请使用专门的 GIF 编辑工具。
质量参数
- JPEG/WEBP:quality参数控制压缩质量(1-100,默认85)
- PNG:使用压缩级别(0-9,默认6)
错误处理
常见错误及处理:
| 错误 | 原因 | 处理 |
|---|---|---|
| 文件不存在 | 路径错误 | 询问用户确认路径 |
| 不支持的格式 | 输入格式无效 | 提示支持的格式列表 |
| 转换失败 | 图片损坏 | 提示原文件可能损坏 |
| 权限错误 | 无写入权限 | 提示更改输出路径 |
| URL无法访问 | 网络问题或URL失效 | 提示检查URL或网络 |
约束
公共硬约束
本块由 docs/templates/skill-common-constraints.md 统一维护;每个 SKILL.md 的 ## 约束 必须逐字同步本块,不得在副本中改写公共规则。
- 任务需要落盘时,使用唯一的
./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/根目录;共享材料放入shared/,Skill 专属材料放入该 Skill 的input/、output/、log/。 - 正式交付物、源代码和正式计划按项目约定保存,不写入任务工作区;未经授权不覆盖、删除、迁移或远程写入。
- 项目维护变更检查 BAC 可用性并记录需求、AI 产出、工具结果、文件改动和验证摘要;BAC 只做过程审计,不替代署名、责任或合规判断。
- 不记录 API Key、访问令牌、密码、Cookie、环境/凭据文件、私有 Prompt、身份信息、本地用户名、主机名或不必要的大体积原始数据。
- 文件路径必须规范化并限制在授权项目范围内;外部 URL、子进程和网络访问遵循最小权限,防止路径遍历、SSRF 和命令注入。
- Skill 版本唯一记录在自身
config.yaml:skill_info.version;公开 API、协议、目录或配置变更同步文档与CHANGELOG.md。 bensz-collect-bugs是一个 Agent Skill;仅将 Bensz Agent Skill 或 Bensz 基础设施本身的设计缺陷交给它。先脱敏写入~/.bensz-skills/bugs/,当前任务不中断,只有用户明确要求才公开上报,禁止直接修改用户已安装的 Skill 源码。
Files (skills)
-
scripts
-
convert.py 17.1 KB
#!/usr/bin/env python3 """ Any Picture Format - 图片格式转换脚本 支持任意来源(本地文件/URL/剪贴板)的图片转换为目标格式。 """ import argparse import json import os import sys import tempfile from pathlib import Path from typing import Dict, Any, Optional, List from urllib.parse import urlparse from urllib.request import urlopen, Request try: from PIL import Image, ImageFile, UnidentifiedImageError import yaml except ImportError as e: print(json.dumps({ "success": False, "error": f"Missing required dependency: {e}", "message": "Please install: pip install Pillow pyyaml" })) sys.exit(1) # 加载配置 CONFIG_PATH = Path(__file__).resolve().parents[1] / "config.yaml" def load_config() -> Dict[str, Any]: """加载配置文件""" if CONFIG_PATH.exists(): with open(CONFIG_PATH, "r", encoding="utf-8") as f: return yaml.safe_load(f) return {} CONFIG = load_config() def format_size(size_bytes: int) -> str: """格式化文件大小""" for unit in ["B", "KB", "MB", "GB"]: if size_bytes < 1024: return f"{size_bytes:.1f} {unit}" size_bytes /= 1024 return f"{size_bytes:.1f} TB" def get_config(key: str, default: Any = None) -> Any: """获取配置值,支持嵌套路径如 'defaults.output_format'""" keys = key.split(".") value = CONFIG for k in keys: if isinstance(value, dict): value = value.get(k) else: return default return value if value is not None else default class ImageConverter: """图片转换器""" def __init__(self, source: str, output_format: str, output_path: Optional[str] = None, strategy: str = "new", quality: Optional[int] = None): self.source = source self.output_format = output_format.upper().replace("JPG", "JPEG") self.output_path = output_path self.strategy = strategy self.quality = quality or get_config("defaults.quality", 85) # 硬编码白色作为透明背景色(PNG→JPEG 转换场景) self.transparency_color = (255, 255, 255) self._input_path = None self._temp_file = None def _load_from_url(self, url: str) -> str: """从URL下载图片""" timeout = get_config("defaults.download_timeout", 30) max_size = get_config("validation.max_file_size", 104857600) # 100MB # 使用系统默认临时目录 temp_dir_config = get_config("defaults.temp_dir", "") if temp_dir_config: temp_dir = Path(temp_dir_config) else: temp_dir = Path(tempfile.gettempdir()) / "any-picture-format" temp_dir.mkdir(parents=True, exist_ok=True) # 验证 URL 安全性(SSRF 防护) parsed = urlparse(url) # 拒绝非 HTTP/HTTPS 协议 if parsed.scheme not in ("http", "https"): raise ValueError(f"不支持的协议: {parsed.scheme}") # 拒绝内网地址 hostname = parsed.hostname if hostname: hostname_lower = hostname.lower() # 拒绝 localhost 和本地回环地址 if hostname_lower in ("localhost", "127.0.0.1", "0.0.0.0", "::1"): raise ValueError(f"不允许访问内网地址: {hostname}") # 拒绝私有 IP 地址范围 if (hostname_lower.startswith("127.") or hostname_lower.startswith("10.") or hostname_lower.startswith("192.168.") or (hostname_lower.startswith("172.16.") or (hostname_lower.startswith("172.") and len(hostname_lower.split(".")) >= 2 and 16 <= int(hostname_lower.split(".")[1]) <= 31))): raise ValueError(f"不允许访问内网地址: {hostname}") # 解析URL获取文件名 filename = os.path.basename(parsed.path) or "downloaded_image" # 如果没有扩展名,添加 .tmp 后缀 if not Path(filename).suffix: filename = f"{filename}.tmp" temp_file = temp_dir / filename # 下载文件(带大小限制) req = Request(url, headers={"User-Agent": "Mozilla/5.0"}) with urlopen(req, timeout=timeout) as response: content_length = response.getheader("Content-Length") if content_length and int(content_length) > max_size: raise ValueError(f"文件过大: {content_length} 字节") downloaded = 0 with open(temp_file, "wb") as f: while True: chunk = response.read(8192) if not chunk: break downloaded += len(chunk) if downloaded > max_size: raise ValueError(f"文件过大: {downloaded} 字节,超过限制 {max_size} 字节") f.write(chunk) self._temp_file = str(temp_file) return str(temp_file) def _load_from_clipboard(self) -> str: """从剪贴板加载图片""" try: import pyperclip from PIL import ImageGrab except ImportError: raise ImportError("Clipboard support requires: pip install pyperclip Pillow") image = ImageGrab.grabclipboard() if image is None: raise ValueError("剪贴板中没有图片") # 使用系统默认临时目录 temp_dir_config = get_config("defaults.temp_dir", "") if temp_dir_config: temp_dir = Path(temp_dir_config) else: temp_dir = Path(tempfile.gettempdir()) / "any-picture-format" temp_dir.mkdir(parents=True, exist_ok=True) temp_file = temp_dir / "clipboard.png" image.save(temp_file, "PNG") self._temp_file = str(temp_file) return str(temp_file) def _get_input_path(self) -> str: """获取输入文件路径""" if self.source.lower() == "clipboard": return self._load_from_clipboard() elif self.source.startswith(("http://", "https://")): return self._load_from_url(self.source) else: # 规范化路径(解析 .. 和符号链接) path = Path(self.source).resolve() if not path.exists(): raise FileNotFoundError(f"文件不存在: {self.source}") if not path.is_file(): raise ValueError(f"不是文件: {self.source}") return str(path) def _determine_output_path(self, input_path: str) -> str: """确定输出文件路径""" if self.output_path: return self.output_path input_path_obj = Path(input_path) if self.strategy == "overwrite": return str(input_path_obj.with_suffix(f".{self.output_format.lower()}")) else: # new # 生成新文件名:替换扩展名 return str(input_path_obj.with_suffix(f".{self.output_format.lower()}")) def _convert_transparency(self, image: Image.Image) -> Image.Image: """处理透明度(用于不支持透明的格式)""" format_config = get_config(f"format_config.{self.output_format}", {}) if not format_config.get("supports_transparency", True): if image.mode in ("RGBA", "LA", "P"): # 创建白色背景 background = Image.new("RGB", image.size, self.transparency_color) if image.mode == "P": image = image.convert("RGBA") background.paste(image, mask=image.split()[-1] if image.mode == "RGBA" else None) return background return image def convert(self) -> Dict[str, Any]: """执行转换""" try: # 加载输入 self._input_path = self._get_input_path() # 打开图片 ImageFile.LOAD_TRUNCATED_IMAGES = True with Image.open(self._input_path) as img: original_size = os.path.getsize(self._input_path) original_format = img.format or "UNKNOWN" # 处理透明度 img = self._convert_transparency(img) # 确定输出路径 output_path = self._determine_output_path(self._input_path) output_dir = Path(output_path).parent output_dir.mkdir(parents=True, exist_ok=True) # 准备保存参数 save_kwargs = {} if self.output_format in ("JPEG", "WEBP"): save_kwargs["quality"] = self.quality elif self.output_format == "PNG": save_kwargs["compress_level"] = get_config("defaults.png_compression", 6) # 转换模式 if self.output_format == "JPEG": if img.mode != "RGB": img = img.convert("RGB") elif self.output_format == "PNG": if img.mode not in ("RGB", "RGBA", "LA", "L", "P"): img = img.convert("RGBA") # 保存 img.save(output_path, self.output_format, **save_kwargs) # 获取结果 output_size = os.path.getsize(output_path) return { "success": True, "input_path": self._input_path, "output_path": output_path, "format": { "from": original_format, "to": self.output_format }, "size": { "before": original_size, "after": output_size, "before_human": format_size(original_size), "after_human": format_size(output_size), "compression_ratio": round((1 - output_size / original_size) * 100, 1) if original_size > 0 else 0 }, "dimensions": { "width": img.width, "height": img.height } } except UnidentifiedImageError: return { "success": False, "error": "无法识别为图片文件", "error_type": "UnidentifiedImageError", "suggestion": "请确认文件是有效的图片格式(PNG/JPEG/WEBP/GIF/BMP/TIFF/ICO/HEIC)" } except Exception as e: return { "success": False, "error": str(e), "error_type": type(e).__name__ } finally: # 清理临时文件 if self._temp_file and Path(self._temp_file).exists(): try: os.remove(self._temp_file) except (OSError, PermissionError): # 清理失败不影响主流程 pass class BatchConverter: """批量转换器""" def __init__(self, directory: str, output_format: str, recursive: bool = False, strategy: str = "new", quality: Optional[int] = None): self.directory = Path(directory) self.output_format = output_format.upper().replace("JPG", "JPEG") self.recursive = recursive self.strategy = strategy self.quality = quality def _get_image_files(self) -> List[Path]: """获取目录中的所有图片文件""" image_extensions = {".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif", ".ico", ".heic"} if self.recursive: files = list(self.directory.rglob("*")) else: files = list(self.directory.glob("*")) return [f for f in files if f.suffix.lower() in image_extensions and f.is_file()] def convert(self) -> Dict[str, Any]: """执行批量转换""" files = self._get_image_files() if not files: return { "success": True, "total": 0, "success_count": 0, "failed_count": 0, "results": [], "message": "目录中没有找到图片文件" } results = [] errors = [] for file_path in files: converter = ImageConverter( source=str(file_path), output_format=self.output_format, strategy=self.strategy, quality=self.quality ) result = converter.convert() if result.get("success"): results.append({ "file": str(file_path), "output": result["output_path"], "size_change": result["size"] }) else: errors.append({ "file": str(file_path), "error": result.get("error"), "error_type": result.get("error_type") }) return { "success": True, "total": len(files), "success_count": len(results), "failed_count": len(errors), "results": results, "errors": errors } def validate_input(source: str) -> Dict[str, Any]: """验证输入""" try: if source.lower() == "clipboard": return { "valid": True, "source_type": "clipboard", "message": "剪贴板输入(将在转换时验证)" } if source.startswith(("http://", "https://")): return { "valid": True, "source_type": "url", "format": "unknown", "message": "URL输入(将下载后验证)" } path = Path(source) if not path.exists(): return { "valid": False, "error": "文件不存在" } if not path.is_file(): return { "valid": False, "error": "不是文件" } # 尝试打开图片 try: with Image.open(path) as img: return { "valid": True, "source_type": "file", "format": img.format or "UNKNOWN", "size": os.path.getsize(path), "size_human": format_size(os.path.getsize(path)), "dimensions": { "width": img.width, "height": img.height } } except Exception as e: return { "valid": False, "error": f"无法识别为图片: {e}" } except Exception as e: return { "valid": False, "error": str(e) } def main(): parser = argparse.ArgumentParser(description="图片格式转换工具") subparsers = parser.add_subparsers(dest="command", help="子命令") # validate 命令 validate_parser = subparsers.add_parser("validate", help="验证输入") validate_parser.add_argument("--source", required=True, help="输入来源(文件路径/URL/clipboard)") # convert 命令 convert_parser = subparsers.add_parser("convert", help="转换单个图片") convert_parser.add_argument("--source", required=True, help="输入来源") convert_parser.add_argument("--format", required=True, help="目标格式") convert_parser.add_argument("--output", help="输出路径") convert_parser.add_argument("--strategy", default="new", choices=["new", "overwrite"], help="输出策略") convert_parser.add_argument("--quality", type=int, help="质量参数(1-100)") # batch 命令 batch_parser = subparsers.add_parser("batch", help="批量转换") batch_parser.add_argument("--directory", required=True, help="目录路径") batch_parser.add_argument("--format", required=True, help="目标格式") batch_parser.add_argument("--recursive", action="store_true", help="递归处理子目录") batch_parser.add_argument("--strategy", default="new", choices=["new", "overwrite"], help="输出策略") batch_parser.add_argument("--quality", type=int, help="质量参数(1-100)") args = parser.parse_args() if args.command == "validate": result = validate_input(args.source) elif args.command == "convert": converter = ImageConverter( source=args.source, output_format=args.format, output_path=args.output, strategy=args.strategy, quality=args.quality ) result = converter.convert() elif args.command == "batch": batch_converter = BatchConverter( directory=args.directory, output_format=args.format, recursive=args.recursive, strategy=args.strategy, quality=args.quality ) result = batch_converter.convert() else: parser.print_help() sys.exit(1) # 输出JSON结果 print(json.dumps(result, ensure_ascii=False, indent=2)) # 返回适当的退出码 sys.exit(0 if result.get("success", False) else 1) if __name__ == "__main__": main()
-
-
CHANGELOG.md 1.8 KB
# Any Picture Format 变更记录 ## [Unreleased] ### Changed(变更) - 规范化 `SKILL.md` 正文骨架,补齐输入、输出、校验、失败恢复和公共约束摘要;any-picture-format 的既有功能语义保持不变。 ## [0.2.0] - 2026-01-21 ### Fixed(修复) - **安全问题**:修复SSRF漏洞,添加URL白名单验证,拒绝内网地址访问 - **安全问题**:修复路径遍历漏洞,使用Path.resolve()规范化路径 - **错误处理**:捕获UnidentifiedImageError,返回友好的用户错误消息 - **代码质量**:修复裸except,使用具体异常类型(OSError, PermissionError) ### Changed(变更) - **配置简化**:移除transparency_color配置项,硬编码为白色(255, 255, 255) - **配置简化**:移除naming.batch_pattern配置项(未使用) - **配置简化**:移除error_handling.error_report_name配置项(未使用) - **配置简化**:移除error_handling.create_error_report配置项(未使用) - **策略简化**:移除custom输出策略,只支持new和overwrite - **跨平台**:临时文件目录使用系统默认(tempfile.gettempdir()),不再硬编码/tmp ### Changed(文档) - 更新SKILL.md:移除custom策略说明 - 更新SKILL.md:明确说明GIF动画处理使用PIL默认行为 - 更新SKILL.md:移除透明背景颜色配置说明 --- ## [0.1.0] - 2026-01-21 ### Added(新增) - 初始化技能,实现图片格式转换核心功能 - 支持本地文件、网络URL、剪贴板三种输入来源 - 支持PNG/JPEG/WEBP/GIF/BMP/TIFF/ICO等格式互转 - 实现单文件转换和批量文件夹转换 - 自动处理透明度(PNG→JPEG时自动替换背景) - 可配置质量参数、输出策略、文件命名规则 - 提供Python脚本接口(scripts/convert.py) - 添加验证功能(validate子命令) -
config.yaml 2.2 KB
# 技能基本信息 skill_info: name: any-picture-format version: 0.2.0 description: "任意格式图片转换工具,支持本地文件/URL/剪贴板输入,批量处理" author: "Bensz Conan" category: "数据处理" # 默认配置 defaults: # 默认输出格式 output_format: "PNG" # 默认输出策略: new/overwrite output_strategy: "new" # 默认质量参数 (用于JPEG/WEBP,范围1-100) quality: 85 # 默认PNG压缩级别 (范围0-9) png_compression: 6 # 批量处理时是否递归子目录 recursive: false # URL下载超时时间(秒) download_timeout: 30 # 临时文件目录 (留空使用系统默认) temp_dir: "" # 支持的输入格式(空列表表示自动检测) input_formats: [] # 支持的输出格式 output_formats: - PNG - JPEG - JPG - WEBP - GIF - BMP - TIFF - ICO # 格式特定配置 format_config: JPEG: quality_range: [1, 100] default_quality: 85 supports_transparency: false description: "有损压缩,文件体积小,适合照片" PNG: compression_range: [0, 9] default_compression: 6 supports_transparency: true description: "无损压缩,支持透明度,适合图形" WEBP: quality_range: [1, 100] default_quality: 85 supports_transparency: true description: "现代Web格式,体积与质量平衡" GIF: supports_transparency: true supports_animation: true description: "支持动画,但色彩限制为256色" BMP: supports_transparency: false description: "无压缩Windows位图,文件体积大" TIFF: supports_transparency: true supports_multipage: true description: "专业用途,支持多页和元数据" ICO: supports_transparency: true description: "Windows图标格式,通常用于favicon" # 质量检查 validation: # 最小文件大小(字节),小于此值警告 min_file_size: 1024 # 最大文件大小(字节),大于此值警告 max_file_size: 104857600 # 100MB # 转换后最小尺寸(像素) min_dimensions: width: 1 height: 1 # 转换后最大尺寸(像素) max_dimensions: width: 30000 height: 30000 # 错误处理 error_handling: # 单个文件失败时是否继续批量处理 continue_on_error: true -
README.md 3.9 KB
# Any Picture Format 这个 skill 用来把图片转换成你需要的目标格式,适合单文件、批量目录、URL 图片和剪贴板图片的格式转换;如果你只是想裁剪、缩放或查看图片信息,就不该用它。 ## 用法 ### 最推荐用法 ```text 请使用 any-picture-format skill 将图片格式转换为目标格式。 输入:`/path/to/image.png` 或图片 URL 或剪贴板图片;目标格式如 `jpeg` / `png` / `webp` 输出:转换后的图片文件,默认保留原文件并生成新文件 ``` ### 进阶用法 ```text 请使用 any-picture-format skill 批量转换一个目录里的图片。 输入:图片目录 `/path/to/images`;目标格式 `webp` 输出:批量转换后的图片文件 另外,还有下列参数约束: - 递归处理:是 - 输出策略:覆盖原文件 - 质量参数:90 ``` ## 能做什么 - 支持本地文件、网络 URL、剪贴板图片三种输入来源。 - 支持 `PNG`、`JPEG/JPG`、`WEBP`、`GIF`、`BMP`、`TIFF`、`ICO` 等常见输出格式。 - 会根据格式差异处理透明度、质量参数和批量目录结构。 - 默认策略是“保留原文件,新生成目标格式文件”。 - 不适合裁剪、缩放、加水印或做复杂图像编辑。 ## 使用示例 ### 示例 1:转换单张图片 ```text 请使用 any-picture-format skill 将这张图片转成 JPEG。 输入:`/tmp/demo.png` 输出:`/tmp/demo.jpg` ``` ### 示例 2:从 URL 转换为 PNG ```text 请使用 any-picture-format skill 转换这个网络图片。 输入:`https://example.com/demo.webp` 输出:PNG 文件 另外,还有下列参数约束: - 输出策略:新建文件 ``` ### 示例 3:批量转换整个目录 ```text 请使用 any-picture-format skill 批量转换图片目录。 输入:`./assets/raw-images`,目标格式 `webp` 输出:转换后的图片文件 另外,还有下列参数约束: - 递归处理:是 - 质量参数:85 ``` ## 输出 - 单文件转换时,默认在原文件旁生成同名新文件,例如 `image.png -> image.jpg`。 - 批量转换时,默认保留原目录结构和原文件名,只改变扩展名。 - 默认不会覆盖原文件;只有显式要求 `overwrite` 时才会覆盖。 - 如果是剪贴板或 URL 输入,技能会先落地临时输入,再生成目标输出。 ## 配置 - 配置文件:`any-picture-format/config.yaml` - 默认输出格式:`PNG` - 默认输出策略:`new` - 默认质量参数:`85` - 批量递归默认值:`false` - 高价值配置项: - `defaults.output_format` - `defaults.output_strategy` - `defaults.quality` - `output_formats` ## 备选用法(脚本/硬编码) 如果你已经明确知道输入路径和目标格式,直接使用脚本会更快。 ### 验证输入 ```bash python3 any-picture-format/scripts/convert.py validate --source /path/to/image.png ``` ### 转换单张图片 ```bash python3 any-picture-format/scripts/convert.py convert \ --source /path/to/image.png \ --format JPEG \ --output /path/to/image.jpg \ --strategy new \ --quality 90 ``` ### 批量转换目录 ```bash python3 any-picture-format/scripts/convert.py batch \ --directory /path/to/images \ --format WEBP \ --recursive \ --strategy new \ --quality 85 ``` ## 常见问题 ### Q:PNG 转 JPEG 后透明背景怎么办? A:JPEG 不支持透明度,通常会自动合成为纯色背景;如果你需要保留透明度,优先输出为 `PNG` 或 `WEBP`。 ### Q:会覆盖原文件吗? A:默认不会。只有明确要求 `overwrite`,或脚本里传入 `--strategy overwrite` 时才会覆盖。 ### Q:批量转换时某个文件失败怎么办? A:默认配置 `continue_on_error: true`,会继续处理其它文件;你可以根据最终报告定位失败项。 ### Q:这个 skill 能顺手做裁剪或缩放吗? A:不能。它的职责是“格式转换”,不是通用图像编辑。 -
SKILL.md 9.2 KB
--- name: any-picture-format description: 当用户明确要求转换、修改或批量转换图片格式时使用。支持常见本地或网络图片输入与常见目标格式输出。⚠️ 不适用:仅调整尺寸/裁剪、查看图片信息,或没有明确格式转换意图。 metadata: author: Bensz Conan short-description: 任意格式图片转换工具 keywords: - any-picture-format - 图片格式转换 - PNG转JPEG - WEBP转换 - 批量图片转换 - 图片格式标准化 --- # Any Picture Format ## 目标 当用户明确要求"转换图片格式"、"修改图片格式"、"图片格式转换"时使用。支持任意格式图片到目标格式的转换,包括:本地文件/网络URL/剪贴板图片输入,PNG/JPEG/WEBP 等常见格式输出,单文件或批量处理模式。核心特点:自动检测输入格式、支持透明度处理、批量处理保持原始文件名结构。⚠️ 不适用:用户只是想调整图片大小/裁剪(应使用图片编辑工具)、只是想查看图片信息(应直接使用文件查看器)、没有明确"格式转换"意图。 ## 流程 ### 输入 输入为本地图片路径、HTTP/HTTPS URL 或 `clipboard`;目标格式可选,未指定时使用配置中的默认格式 `PNG`,批量模式还需目录和递归选项。可选输入包括输出路径、`new/overwrite` 策略、质量参数和配置文件中的格式列表;只在用户明确要求格式转换时触发。 ### 执行步骤 #### 核心功能 ##### 输入来源 | 来源类型 | 说明 | 示例 | |---------|------|------| | **本地文件** | 支持所有常见图片格式(PNG/JPEG/WEBP/GIF/BMP/TIFF/ICO/HEIC等) | `/path/to/image.png` | | **网络URL** | 自动下载后转换,支持HTTP/HTTPS | `https://example.com/image.webp` | | **剪贴板** | 直接读取系统剪贴板中的图片 | 用户刚截图后 | ##### 输出格式 默认支持的目标格式(可在config.yaml中扩展): - **PNG**:无损压缩,支持透明度 - **JPEG**:有损压缩,文件体积小 - **WEBP**:现代Web格式,体积与质量平衡 - **GIF**:支持动画(静态图片转GIF时首帧) - **BMP**:无压缩Windows位图 - **TIFF**:专业用途,支持多页 ##### 输出策略 | 策略 | 说明 | 文件名变化 | |------|------|-----------| | **新建** | 保留原文件,生成新文件 | `image.png` → `image.jpg` | | **覆盖** | 替换原文件 | `image.png` → `image.png`(格式已变) | #### 工作流程 ##### 1. 需求确认 AI需要确认以下信息(可从用户请求推断): - **输入来源**:文件路径/URL/剪贴板 - **目标格式**:输出格式(默认PNG) - **输出策略**:新建/覆盖/指定路径(默认新建) - **处理范围**:单文件或批量目录 ##### 2. 输入验证 使用脚本验证输入: ```bash python3 scripts/convert.py validate --source <输入> ``` 验证内容: - 输入来源是否可访问 - 文件是否为有效图片 - URL是否可下载 ##### 3. 执行转换 ###### 单文件转换 ```bash python3 scripts/convert.py convert \ --source <输入> \ --format <目标格式> \ --output <输出路径> \ --strategy <new/overwrite> ``` ###### 批量转换 ```bash python3 scripts/convert.py batch \ --directory <目录> \ --format <目标格式> \ --recursive \ # 递归处理子目录 --strategy <新建/覆盖> ``` ##### 4. 结果验证 转换后验证: - 输出文件是否存在 - 文件格式是否正确 - 文件大小是否合理 - 图片质量是否可接受 #### 脚本接口 ##### scripts/convert.py 核心转换脚本,提供以下子命令: ###### validate - 验证输入 ```bash python3 scripts/convert.py validate --source <输入> ``` 返回: - `valid`: true/false - `format`: 检测到的输入格式 - `size`: 图片尺寸 - `error`: 错误信息(如有) ###### convert - 单文件转换 ```bash python3 scripts/convert.py convert \ --source <输入> \ --format <目标格式> \ --output <输出路径> \ --strategy <new/overwrite> \ --quality <质量参数> # 可选,1-100 ``` 返回: - `success`: true/false - `input_path`: 输入文件路径 - `output_path`: 输出文件路径 - `size_before`: 转换前大小 - `size_after`: 转换后大小 - `error`: 错误信息(如有) ###### batch - 批量转换 ```bash python3 scripts/convert.py batch \ --directory <目录> \ --format <目标格式> \ --recursive \ --strategy <新建/覆盖> \ --quality <质量参数> ``` 返回: - `total`: 总文件数 - `success`: 成功转换数 - `failed`: 失败数 - `results`: 每个文件的详细结果 - `errors`: 错误信息列表 #### AI执行指南 ##### 场景1:用户要求转换单个图片 ``` 用户:"把这个PNG转成JPEG" AI执行: 1. 确认输入文件(从上下文获取或询问) 2. 调用 validate 验证 3. 调用 convert 转换(默认strategy=新建) 4. 报告结果(文件路径、大小变化) ``` ##### 场景2:批量转换文件夹 ``` 用户:"把images文件夹里的图片都转成WEBP" AI执行: 1. 确认目录和格式 2. 调用 batch 批量转换 3. 汇总结果(成功/失败统计) 4. 报告任何失败的文件 ``` ##### 场景3:URL图片转换 ``` 用户:"把 https://example.com/image.webp 转成PNG" AI执行: 1. 调用 validate 验证URL可访问性 2. 调用 convert 转换(脚本会自动下载) 3. 报告结果 ``` ##### 场景4:剪贴板图片转换 ``` 用户:"把刚才的截图转成JPEG" AI执行: 1. 调用 convert --source clipboard 2. 指定输出路径(询问或使用默认) 3. 报告保存位置 ``` #### 配置参数 见config.yaml,可配置: - 默认输出格式 - 默认输出策略 - 默认质量参数 - 支持的格式列表 ### 输出 #### 输出报告 转换完成后应向用户报告: ``` ✓ 转换完成 输入:image.png (2.3 MB) 输出:image.jpg (456 KB) 压缩比:80% 保存位置:/path/to/image.jpg ``` 批量转换时: ``` ✓ 批量转换完成 总计:25个文件 成功:23个 失败:2个 失败文件: - corrupted.png:图片损坏 - locked.jpg:权限不足 输出目录:/path/to/output/ ``` ### 输出管理 #### BenszAPI 任务工作区 ### 校验 转换前运行 `scripts/convert.py validate`,确认来源可访问且为有效图片;转换后核对输出存在、格式与目标一致、大小合理、质量可接受,并在批量结果中报告成功数、失败数和每个错误。 ### 失败与恢复 #### 特殊处理 ##### 透明度处理 - **PNG→JPEG**:自动将透明背景替换为白色 - **带透明度的图片**:保留透明度需选择PNG/WEBP格式 ##### 动画处理 - **GIF→其他格式**:使用 PIL 默认行为(提取首帧) - **其他格式→GIF**:生成静态 GIF(不支持动画生成) 如需动画处理,请使用专门的 GIF 编辑工具。 ##### 质量参数 - **JPEG/WEBP**:quality参数控制压缩质量(1-100,默认85) - **PNG**:使用压缩级别(0-9,默认6) #### 错误处理 常见错误及处理: | 错误 | 原因 | 处理 | |------|------|------| | 文件不存在 | 路径错误 | 询问用户确认路径 | | 不支持的格式 | 输入格式无效 | 提示支持的格式列表 | | 转换失败 | 图片损坏 | 提示原文件可能损坏 | | 权限错误 | 无写入权限 | 提示更改输出路径 | | URL无法访问 | 网络问题或URL失效 | 提示检查URL或网络 | ## 约束 <!-- BEGIN COMMON CONSTRAINTS --> <!-- Source-Hash: sha256:15120201e9e0c7569517261d57ecefb63ac279c26ed13876f8e95b6dc35854d3 --> <!-- Template-ID: skill-common-constraints; Template-Version: 1; Sync-Policy: exact-block --> ### 公共硬约束 本块由 `docs/templates/skill-common-constraints.md` 统一维护;每个 `SKILL.md` 的 `## 约束` 必须逐字同步本块,不得在副本中改写公共规则。 - 任务需要落盘时,使用唯一的 `./.bensz-api/task-{yyyymmdd-hhmm}-{简短描述}/` 根目录;共享材料放入 `shared/`,Skill 专属材料放入该 Skill 的 `input/`、`output/`、`log/`。 - 正式交付物、源代码和正式计划按项目约定保存,不写入任务工作区;未经授权不覆盖、删除、迁移或远程写入。 - 项目维护变更检查 BAC 可用性并记录需求、AI 产出、工具结果、文件改动和验证摘要;BAC 只做过程审计,不替代署名、责任或合规判断。 - 不记录 API Key、访问令牌、密码、Cookie、环境/凭据文件、私有 Prompt、身份信息、本地用户名、主机名或不必要的大体积原始数据。 - 文件路径必须规范化并限制在授权项目范围内;外部 URL、子进程和网络访问遵循最小权限,防止路径遍历、SSRF 和命令注入。 - Skill 版本唯一记录在自身 `config.yaml:skill_info.version`;公开 API、协议、目录或配置变更同步文档与 `CHANGELOG.md`。 - `bensz-collect-bugs` 是一个 Agent Skill;仅将 Bensz Agent Skill 或 Bensz 基础设施本身的设计缺陷交给它。先脱敏写入 `~/.bensz-skills/bugs/`,当前任务不中断,只有用户明确要求才公开上报,禁止直接修改用户已安装的 Skill 源码。 <!-- End of canonical common constraints. --> <!-- END COMMON CONSTRAINTS -->
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.