Files
aza/AzA march 2026/aza_empfang_region_snapshot.py

237 lines
6.4 KiB
Python
Raw Normal View History

2026-05-16 20:33:36 +02:00
# -*- 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("<ButtonPress-1>", on_press)
canvas.bind("<B1-Motion>", on_motion)
canvas.bind("<ButtonRelease-1>", on_release)
canvas.bind("<ButtonPress-3>", lambda e: finish(None))
root.bind("<Escape>", 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"