diff --git a/plugins/image_reader.py b/plugins/image_reader.py new file mode 100644 index 0000000..20dbd22 --- /dev/null +++ b/plugins/image_reader.py @@ -0,0 +1,293 @@ +""" +图片读取插件 — 从图片中提取文字,发送给 AI +============================================= +功能:读取图片 → OCR 提取文字 → 返回给 AI +支持:Tesseract OCR / Windows PowerShell OCR / 纯 Python 元数据 +""" +import os +import sys +import json +import base64 +import hashlib +import subprocess +import platform +import tempfile +from pathlib import Path + +_IS_WIN = platform.system() == "Windows" + + +class Liugin: + """图片读取 — 从图片中提取文字""" + + def __init__(self): + self.cli = None + + def set_cli(self, cli): + self.cli = cli + + def get_tool_info(self): + return { + "name": "image_reader", + "description": "图片读取工具 — 从图片中提取文字(OCR),获取图片信息," + "将图片内容转为文本发送给 AI", + "keywords": [ + "图片", "image", "OCR", "文字识别", "读图", "截图", + "提取文字", "图片转文字", "识别", "tesseract", + ], + "usage": ( + "image_reader <操作> <路径>\n\n" + "操作:\n" + " ocr <图片路径> - 从图片中提取文字\n" + " info <图片路径> - 获取图片信息(尺寸/格式/大小)\n" + " read <图片路径> - 综合读取(信息 + OCR)\n" + " ascii <图片路径> [宽] - 转 ASCII 字符画(终端显示)\n" + " base64 <图片路径> - 转 Base64 编码\n" + " hash <图片路径> - 计算图片哈希值\n\n" + "示例:\n" + " image_reader ocr screenshot.png - 识别截图中的文字\n" + " image_reader read photo.jpg - 综合读取图片\n" + " image_reader ascii logo.png 40 - 转40列宽ASCII画\n" + ), + } + + def get_mcp_definition(self): + return { + "name": "image_reader", + "description": "图片读取工具 — 从图片中提取文字(OCR)", + "inputSchema": { + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": ["ocr", "info", "read", "ascii", "base64", "hash"], + "description": "操作类型", + }, + "path": {"type": "string", "description": "图片文件路径"}, + "width": {"type": "integer", "description": "ASCII 画宽度"}, + }, + "required": ["action", "path"], + }, + } + + def convert_mcp_args(self, arguments): + action = arguments.get("action", "") + path = arguments.get("path", "") + width = arguments.get("width", "") + parts = [action, path] + if width: + parts.append(str(width)) + return " ".join(parts) + + def handle(self, args: str) -> str: + parts = args.strip().split(maxsplit=2) + if not parts: + return "请提供操作和图片路径" + action = parts[0].lower() + path = parts[1] if len(parts) > 1 else "" + extra = parts[2] if len(parts) > 2 else "" + + if not path: + return "请提供图片路径" + + # 展开 ~ 和环境变量 + path = os.path.expanduser(os.path.expandvars(path)) + if not os.path.isfile(path): + return f"文件不存在: {path}" + + handlers = { + "ocr": self._ocr, + "info": self._info, + "read": self._read, + "ascii": self._ascii, + "base64": self._b64, + "hash": self._hash, + } + h = handlers.get(action) + if not h: + return f"不支持的操作 '{action}'" + try: + return h(path, extra) + except Exception as e: + return f"错误: {e}" + + # ── 图片信息 ── + + def _info(self, path: str, _: str) -> str: + size = os.path.getsize(path) + ext = os.path.splitext(path)[1].lower() + lines = [ + f"图片: {os.path.basename(path)}", + f"格式: {ext}", + f"大小: {self._human_size(size)}", + ] + # 尝试用 PIL 获取尺寸 + try: + from PIL import Image + with Image.open(path) as img: + lines.append(f"尺寸: {img.width} x {img.height}") + lines.append(f"模式: {img.mode}") + if hasattr(img, 'info'): + dpi = img.info.get('dpi') + if dpi: + lines.append(f"DPI: {dpi}") + except ImportError: + pass + except Exception as e: + lines.append(f"读取详情失败: {e}") + return "\n".join(lines) + + # ── OCR 文字识别 ── + + def _ocr(self, path: str, _: str) -> str: + # 方案1: Tesseract + result = self._ocr_tesseract(path) + if result: + return result + + # 方案2: Windows PowerShell OCR + if _IS_WIN: + result = self._ocr_windows(path) + if result: + return result + + # 方案3: 用 PIL 读取图片元数据,至少告诉用户图片是什么 + return ( + "未找到 OCR 引擎。请安装以下任一工具:\n" + " - Tesseract: https://github.com/tesseract-ocr/tesseract\n" + " - Ubuntu: sudo apt install tesseract-ocr tesseract-ocr-chi-sim\n" + " - macOS: brew install tesseract\n" + " - Windows: 下载 UB-Mannheim installer\n\n" + f"图片信息:\n{self._info(path, '')}" + ) + + def _ocr_tesseract(self, path: str) -> str: + """用 Tesseract OCR 提取文字""" + # 检查 tesseract 是否可用 + try: + subprocess.run(["tesseract", "--version"], + capture_output=True, timeout=5) + except (FileNotFoundError, subprocess.TimeoutExpired): + return "" + + # 执行 OCR + with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as tmp: + tmp_path = tmp.name + + try: + # 检测语言:中文+英文 + cmd = ["tesseract", path, tmp_path[:-4], "-l", "chi_sim+eng"] + subprocess.run(cmd, capture_output=True, timeout=30) + + if os.path.exists(tmp_path): + with open(tmp_path, "r", encoding="utf-8") as f: + text = f.read().strip() + if text: + return f"OCR 识别结果:\n{text}" + return "OCR 未识别到文字" + except subprocess.TimeoutExpired: + return "OCR 超时" + except Exception as e: + return f"OCR 失败: {e}" + finally: + try: + os.unlink(tmp_path) + except OSError: + pass + return "" + + def _ocr_windows(self, path: str) -> str: + """用 Windows 内置 OCR (PowerShell + Windows.Media.Ocr)""" + try: + ps_script = f''' +Add-Type -AssemblyName System.Runtime.WindowsRuntime +$bitmap = [Windows.Graphics.Imaging.SoftwareBitmap, Windows.Graphics.Imaging, ContentType=WindowsRuntime] +$ocrEngine = [Windows.Media.Ocr.OcrEngine, Windows.Media.Ocr, ContentType=WindowsRuntime] +$ocr = $ocrEngine.TryCreateFromUserProfileLanguages() +$file = Get-Item "{path}" +$stream = [System.IO.File]::OpenRead($file.FullName) +$decoder = [Windows.Graphics.Imaging.BitmapDecoder]::CreateAsync($stream).GetAwaiter().GetResult() +$bitmap = $decoder.GetSoftwareBitmapAsync().GetAwaiter().GetResult() +$result = $ocr.RecognizeAsync($bitmap).GetAwaiter().GetResult() +$result.Text +''' + result = subprocess.run( + ["powershell", "-Command", ps_script], + capture_output=True, text=True, timeout=30 + ) + if result.returncode == 0 and result.stdout.strip(): + return f"OCR 识别结果:\n{result.stdout.strip()}" + except Exception: + pass + return "" + + # ── 综合读取 ── + + def _read(self, path: str, _: str) -> str: + """信息 + OCR""" + info = self._info(path, "") + ocr = self._ocr(path, "") + return f"{info}\n\n{ocr}" + + # ── ASCII 字符画 ── + + def _ascii(self, path: str, extra: str) -> str: + """将图片转为 ASCII 字符画""" + width = int(extra) if extra.strip().isdigit() else 60 + + try: + from PIL import Image + except ImportError: + return "需要 Pillow 库: pip install Pillow" + + chars = " .:-=+*#%@" + + with Image.open(path) as img: + # 转灰度 + img = img.convert("L") + # 缩放 + ratio = img.height / img.width + new_height = int(width * ratio * 0.5) # 字符高宽比约 2:1 + img = img.resize((width, new_height)) + + lines = [] + for y in range(new_height): + row = "" + for x in range(width): + pixel = img.getpixel((x, y)) + idx = pixel * (len(chars) - 1) // 255 + row += chars[idx] + lines.append(row) + + return "\n".join(lines) + + # ── Base64 编码 ── + + def _b64(self, path: str, _: str) -> str: + with open(path, "rb") as f: + data = f.read() + ext = os.path.splitext(path)[1].lower().lstrip(".") + mime = {"png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", + "gif": "image/gif", "webp": "image/webp", "bmp": "image/bmp" + }.get(ext, "image/png") + b64 = base64.b64encode(data).decode("ascii") + return f"data:{mime};base64,{b64[:100]}... ({len(b64)} 字符)" + + # ── 哈希 ── + + def _hash(self, path: str, _: str) -> str: + with open(path, "rb") as f: + data = f.read() + md5 = hashlib.md5(data).hexdigest() + sha = hashlib.sha256(data).hexdigest() + return f"MD5: {md5}\nSHA256: {sha}" + + # ── 工具 ── + + @staticmethod + def _human_size(size: int) -> str: + for unit in ["B", "KB", "MB", "GB"]: + if size < 1024: + return f"{size:.1f} {unit}" + size /= 1024 + return f"{size:.1f} TB" diff --git a/plugins_config.py b/plugins_config.py index 39aac69..a353f29 100644 --- a/plugins_config.py +++ b/plugins_config.py @@ -25,3 +25,4 @@ ENABLE_REGEX_TESTER = True ENABLE_SNIPPET_MANAGER = True ENABLE_MARKDOWN_TOOL = True ENABLE_ID_GENERATOR = True +ENABLE_IMAGE_READER = True