init
This commit is contained in:
1
utils/__init__.py
Normal file
1
utils/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/config.cpython-311.pyc
Normal file
BIN
utils/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/logger.cpython-311.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-311.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/screenshot.cpython-311.pyc
Normal file
BIN
utils/__pycache__/screenshot.cpython-311.pyc
Normal file
Binary file not shown.
41
utils/config.py
Normal file
41
utils/config.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AppConfig:
|
||||
umi_ocr_url: str
|
||||
prefer_mss: bool = True
|
||||
default_region: Optional[tuple[int, int, int, int]] = None # left, top, width, height
|
||||
click_pause: float = 0.05
|
||||
|
||||
|
||||
def load_config(path: str | Path) -> AppConfig:
|
||||
p = Path(path)
|
||||
data: dict[str, Any] = yaml.safe_load(p.read_text(encoding="utf-8")) or {}
|
||||
|
||||
umi_ocr = data.get("umi_ocr", {}) or {}
|
||||
screenshot = data.get("screenshot", {}) or {}
|
||||
click = data.get("click", {}) or {}
|
||||
|
||||
region = screenshot.get("default_region")
|
||||
default_region: Optional[tuple[int, int, int, int]]
|
||||
if region is None:
|
||||
default_region = None
|
||||
else:
|
||||
if not (isinstance(region, (list, tuple)) and len(region) == 4):
|
||||
raise ValueError("config.yaml screenshot.default_region 必须是 null 或 [left, top, width, height]")
|
||||
default_region = (int(region[0]), int(region[1]), int(region[2]), int(region[3]))
|
||||
|
||||
return AppConfig(
|
||||
umi_ocr_url=str(umi_ocr.get("url", "http://127.0.0.1:1224/api/ocr")),
|
||||
prefer_mss=bool(screenshot.get("prefer_mss", True)),
|
||||
default_region=default_region,
|
||||
click_pause=float(click.get("pause", 0.05)),
|
||||
)
|
||||
|
||||
18
utils/logger.py
Normal file
18
utils/logger.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def setup_logging(level: int = logging.INFO, log_file: Optional[str] = None) -> logging.Logger:
|
||||
handlers: list[logging.Handler] = [logging.StreamHandler()]
|
||||
if log_file:
|
||||
handlers.append(logging.FileHandler(log_file, encoding="utf-8"))
|
||||
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
||||
handlers=handlers,
|
||||
)
|
||||
return logging.getLogger("yidaima")
|
||||
|
||||
79
utils/screenshot.py
Normal file
79
utils/screenshot.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaptureResult:
|
||||
image_bytes: bytes
|
||||
# 截图区域在“屏幕坐标系”中的位置(left, top, width, height)
|
||||
region: tuple[int, int, int, int]
|
||||
# 截图图片像素坐标 -> 屏幕坐标 的缩放系数
|
||||
scale_x: float
|
||||
scale_y: float
|
||||
|
||||
|
||||
def capture_screen(region: Optional[tuple[int, int, int, int]] = None, *, prefer_mss: bool = True) -> CaptureResult:
|
||||
"""
|
||||
截取屏幕(全屏或局部),返回 PNG bytes + region + 像素到屏幕坐标的缩放。
|
||||
|
||||
- region: (left, top, width, height) 使用屏幕坐标
|
||||
- scale_x/scale_y: 用于处理高 DPI 下 mss 取到的物理像素与 pyautogui 屏幕坐标不一致的问题
|
||||
"""
|
||||
if prefer_mss:
|
||||
try:
|
||||
return _capture_with_mss(region)
|
||||
except Exception:
|
||||
# 回退到 pyautogui(更贴近“屏幕坐标系”)
|
||||
return _capture_with_pyautogui(region)
|
||||
return _capture_with_pyautogui(region)
|
||||
|
||||
|
||||
def _capture_with_pyautogui(region: Optional[tuple[int, int, int, int]]) -> CaptureResult:
|
||||
import pyautogui
|
||||
|
||||
img = pyautogui.screenshot(region=region) # type: ignore[arg-type]
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="PNG")
|
||||
|
||||
if region is None:
|
||||
w, h = img.size
|
||||
region2 = (0, 0, int(w), int(h))
|
||||
else:
|
||||
region2 = (int(region[0]), int(region[1]), int(region[2]), int(region[3]))
|
||||
|
||||
return CaptureResult(image_bytes=buf.getvalue(), region=region2, scale_x=1.0, scale_y=1.0)
|
||||
|
||||
|
||||
def _capture_with_mss(region: Optional[tuple[int, int, int, int]]) -> CaptureResult:
|
||||
import mss
|
||||
import mss.tools
|
||||
import pyautogui
|
||||
|
||||
with mss.mss() as sct:
|
||||
if region is None:
|
||||
mon = sct.monitors[1] # 主屏
|
||||
left, top, width, height = int(mon["left"]), int(mon["top"]), int(mon["width"]), int(mon["height"])
|
||||
else:
|
||||
left, top, width, height = (int(region[0]), int(region[1]), int(region[2]), int(region[3]))
|
||||
|
||||
shot = sct.grab({"left": left, "top": top, "width": width, "height": height})
|
||||
png_bytes = mss.tools.to_png(shot.rgb, shot.size)
|
||||
|
||||
# DPI 缩放处理:mss 的像素尺寸可能与 pyautogui 的屏幕坐标尺寸不同
|
||||
screen_w, screen_h = pyautogui.size()
|
||||
mon = sct.monitors[1]
|
||||
mon_w, mon_h = int(mon["width"]), int(mon["height"])
|
||||
# 若 mss 返回的是物理像素,mon_w/mon_h 往往大于 pyautogui.size()
|
||||
scale_x = float(screen_w) / float(mon_w) if mon_w else 1.0
|
||||
scale_y = float(screen_h) / float(mon_h) if mon_h else 1.0
|
||||
|
||||
return CaptureResult(
|
||||
image_bytes=png_bytes,
|
||||
region=(left, top, width, height),
|
||||
scale_x=scale_x,
|
||||
scale_y=scale_y,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user