foxygit / RPA-Remisser Log in
commits tags

/rpa/desktop_capture.py · 4.16 KB

raw
"""Fånga ett skärmområde som referensbild till desktop_click_image /
desktop_wait_for_image.

    python -m rpa.desktop_capture             # auto-namn (tidsstämpel)
    python -m rpa.desktop_capture knappen_ok  # sparar data/desktop_images/knappen_ok.png

Öppnar ett halvgenomskinligt heltäckande lager – dra en ruta över det du vill
fånga, släpp musknappen. Esc avbryter. Skriver en rad JSON till stdout så
webui:t kan visa resultatet.

Windows-only (tkinter-overlay + PIL ImageGrab). Fångar hela det virtuella
skrivbordet (flera skärmar stöds).
"""

from __future__ import annotations

import json
import re
import sys
import unicodedata
from datetime import datetime

from . import config

_BAD = re.compile(r'[<>:"/\\|?*\x00-\x1f]')


def _safe_name(name: str) -> str:
    name = unicodedata.normalize("NFC", (name or "").strip())
    name = _BAD.sub("_", name).strip(". ")
    if name.lower().endswith(".png"):
        name = name[:-4]
    return name or datetime.now().strftime("bild_%Y%m%d_%H%M%S")


def capture(name: str | None = None) -> dict:
    try:
        import tkinter as tk
        from PIL import ImageGrab
    except Exception as exc:  # noqa: BLE001  # pragma: no cover
        return {"ok": False, "reason": f"tkinter/PIL saknas: {exc}"}

    # DPI-medveten så koordinaterna matchar de faktiska pixlarna.
    try:  # pragma: no cover - Windows-specifikt
        import ctypes

        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except Exception:  # noqa: BLE001
        pass

    try:  # virtuella skrivbordets bounds (alla skärmar)
        import ctypes

        u = ctypes.windll.user32
        vx, vy = u.GetSystemMetrics(76), u.GetSystemMetrics(77)
        vw, vh = u.GetSystemMetrics(78), u.GetSystemMetrics(79)
    except Exception:  # noqa: BLE001
        vx = vy = 0
        vw = vh = None

    state: dict = {"start": None, "bbox": None, "cancelled": False}

    root = tk.Tk()
    root.title("Fånga bild")
    if vw:
        root.overrideredirect(True)
        root.geometry(f"{vw}x{vh}+{vx}+{vy}")
    else:
        root.attributes("-fullscreen", True)
    root.attributes("-alpha", 0.3)
    root.attributes("-topmost", True)
    root.configure(bg="black")

    canvas = tk.Canvas(root, cursor="crosshair", bg="gray12", highlightthickness=0)
    canvas.pack(fill="both", expand=True)
    canvas.create_text(
        (vw or root.winfo_screenwidth()) // 2, 30,
        text="Dra en ruta över det du vill fånga  ·  Esc = avbryt",
        fill="white", font=("Segoe UI", 16),
    )
    rect = canvas.create_rectangle(0, 0, 0, 0, outline="#ff3b30", width=2)

    def on_press(e):
        state["start"] = (e.x, e.y)

    def on_drag(e):
        if state["start"]:
            x0, y0 = state["start"]
            canvas.coords(rect, x0, y0, e.x, e.y)

    def on_release(e):
        if not state["start"]:
            return
        x0, y0 = state["start"]
        x1, y1 = e.x, e.y
        state["bbox"] = (
            vx + min(x0, x1), vy + min(y0, y1),
            vx + max(x0, x1), vy + max(y0, y1),
        )
        root.destroy()

    def on_escape(_e):
        state["cancelled"] = True
        root.destroy()

    canvas.bind("<ButtonPress-1>", on_press)
    canvas.bind("<B1-Motion>", on_drag)
    canvas.bind("<ButtonRelease-1>", on_release)
    root.bind("<Escape>", on_escape)
    root.after(100, lambda: root.focus_force())
    root.mainloop()

    if state["cancelled"] or not state["bbox"]:
        return {"ok": False, "reason": "cancelled"}

    x0, y0, x1, y1 = state["bbox"]
    if x1 - x0 < 4 or y1 - y0 < 4:
        return {"ok": False, "reason": "för liten ruta"}

    config.DESKTOP_IMAGE_DIR.mkdir(parents=True, exist_ok=True)
    target = config.DESKTOP_IMAGE_DIR / f"{_safe_name(name)}.png"
    img = ImageGrab.grab(bbox=(x0, y0, x1, y1), all_screens=True)
    img.save(str(target))
    return {
        "ok": True,
        "name": target.name,
        "path": str(target),
        "width": img.width,
        "height": img.height,
    }


def main() -> None:
    name = sys.argv[1] if len(sys.argv) > 1 else None
    result = capture(name)
    print(json.dumps(result, ensure_ascii=False))
    sys.exit(0 if result.get("ok") else 1)


if __name__ == "__main__":
    main()