237 lines
6.4 KiB
Python
237 lines
6.4 KiB
Python
# -*- 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"
|