# -*- coding: utf-8 -*- """ Vollbild-Overlay zur Bereichswahl fuer Snapshot in der Empfang-Desktop-WebView. - Nur nach expliziter Nutzeraktion (Klick auf Snapshot) gestartet. - Keine Netz-/Log-Ausgabe von Bildinhalten oder Praxisdaten. - Windows: virtuelle Bildschirmflaeche; PIL ImageGrab fuer den gewaehlten Bereich. """ from __future__ import annotations import base64 import io import sys import threading import tkinter as tk from typing import Callable _LOCK = threading.Lock() _ACTIVE = False # Sehr kleine Auswahlen ignorieren (Klicks ohne Zieh-Bewegung). _MIN_SIDE_PX = 6 def _virtual_screen_win32() -> tuple[int, int, int, int] | None: if sys.platform != "win32": return None try: import ctypes user32 = ctypes.windll.user32 SM_XVIRTUALSCREEN = 76 SM_YVIRTUALSCREEN = 77 SM_CXVIRTUALSCREEN = 78 SM_CYVIRTUALSCREEN = 79 x = int(user32.GetSystemMetrics(SM_XVIRTUALSCREEN)) y = int(user32.GetSystemMetrics(SM_YVIRTUALSCREEN)) w = int(user32.GetSystemMetrics(SM_CXVIRTUALSCREEN)) h = int(user32.GetSystemMetrics(SM_CYVIRTUALSCREEN)) if w <= 0 or h <= 0: return None return (x, y, w, h) except Exception: return None def _grab_bbox_to_png_data_url(left: int, top: int, right: int, bottom: int) -> str | None: """Liefert data:image/png;base64,... oder None bei Fehler.""" try: from PIL import ImageGrab except Exception: return None L = min(left, right) T = min(top, bottom) R = max(left, right) B = max(top, bottom) if R - L < _MIN_SIDE_PX or B - T < _MIN_SIDE_PX: return None try: try: img = ImageGrab.grab(bbox=(L, T, R, B), all_screens=True) # type: ignore[call-arg] except TypeError: img = ImageGrab.grab(bbox=(L, T, R, B)) except Exception: return None try: buf = io.BytesIO() img.save(buf, format="PNG", optimize=True) raw = buf.getvalue() if not raw or len(raw) < 32: return None b64 = base64.b64encode(raw).decode("ascii") return f"data:image/png;base64,{b64}" except Exception: return None def _run_overlay(on_done: Callable[[str | None], None]) -> None: global _ACTIVE root = tk.Tk() root.title("") root.configure(cursor="crosshair", bg="#101820") try: root.attributes("-alpha", 0.28) except Exception: pass try: root.attributes("-topmost", True) except Exception: pass root.overrideredirect(True) box = _virtual_screen_win32() if box: vx, vy, vw, vh = box root.geometry(f"{vw}x{vh}+{vx}+{vy}") else: try: root.attributes("-fullscreen", True) except Exception: root.state("zoomed") canvas = tk.Canvas(root, highlightthickness=0, bg="#000010", cursor="crosshair") canvas.pack(fill=tk.BOTH, expand=True) hint = tk.Label( root, text="Bereich mit der Maus aufziehen. Esc oder Rechtsklick: Abbrechen.", fg="#e8eef4", bg="#1a2530", font=("Segoe UI", 10), padx=10, pady=6, ) hint.place(x=8, y=8) sel: dict[str, int | None] = {"x0": None, "y0": None, "rid": None} def finish(data_url: str | None) -> None: global _ACTIVE try: root.destroy() except Exception: pass with _LOCK: _ACTIVE = False try: on_done(data_url) except Exception: pass def on_press(event: tk.Event) -> None: # type: ignore[name-defined] if int(getattr(event, "num", 1) or 1) == 3: finish(None) return sel["x0"] = int(event.x_root) sel["y0"] = int(event.y_root) if sel["rid"] is not None: try: canvas.delete(sel["rid"]) except Exception: pass sel["rid"] = None def _scr_to_cv(sx: int, sy: int) -> tuple[int, int]: return int(sx - canvas.winfo_rootx()), int(sy - canvas.winfo_rooty()) def on_motion(event: tk.Event) -> None: # type: ignore[name-defined] x0, y0 = sel["x0"], sel["y0"] if x0 is None or y0 is None: return x1, y1 = int(event.x_root), int(event.y_root) if sel["rid"] is not None: try: canvas.delete(sel["rid"]) except Exception: pass cx0, cy0 = _scr_to_cv(int(x0), int(y0)) cx1, cy1 = _scr_to_cv(x1, y1) sel["rid"] = canvas.create_rectangle( cx0, cy0, cx1, cy1, outline="#4af", width=2, dash=(4, 4), ) def on_release(event: tk.Event) -> None: # type: ignore[name-defined] x0, y0 = sel["x0"], sel["y0"] if x0 is None or y0 is None: return x1, y1 = int(event.x_root), int(event.y_root) data = _grab_bbox_to_png_data_url(x0, y0, x1, y1) finish(data) def on_esc(_event: tk.Event | None = None) -> None: # type: ignore[name-defined] finish(None) canvas.bind("", on_press) canvas.bind("", on_motion) canvas.bind("", on_release) canvas.bind("", lambda e: finish(None)) root.bind("", on_esc) try: root.focus_force() except Exception: pass try: root.mainloop() except Exception: with _LOCK: _ACTIVE = False try: on_done(None) except Exception: pass def start_region_snapshot_capture( on_done: Callable[[str | None], None], ) -> str: """ Startet die Bereichswahl in einem eigenen Thread (eigenes Tk-Fenster). Returns: "started" | "busy" """ global _ACTIVE with _LOCK: if _ACTIVE: return "busy" _ACTIVE = True def _thread_main() -> None: global _ACTIVE try: _run_overlay(on_done) except Exception: with _LOCK: _ACTIVE = False try: on_done(None) except Exception: pass try: threading.Thread(target=_thread_main, daemon=True).start() except Exception: with _LOCK: _ACTIVE = False return "busy" return "started"