Files
ordinaceprojekt/Medevio/60 ScansProcessing/Testy/main_viewer.py
T
2026-06-02 09:40:05 +02:00

463 lines
19 KiB
Python

"""
Hlavní okno pro zpracování naskenovaných dokumentů.
Kombinuje náhled originálu, náhled duplicit, listbox duplicit a rename panel.
Spouští se jako subprocess z extract_patient_info_novy_test.py.
Argumenty: main_viewer.py <json_soubor>
JSON vstup: {
"original": "cesta/k/originalu.pdf",
"duplicity": ["cesta1.pdf", ...], # plné cesty
"labels": ["název1.pdf", ...], # zobrazované názvy
"nazev": "navrzeny_nazev.pdf",
"info_lines": ["✓ Medicus: ...", ...],
"varianty": ["varianta1.pdf", ...]
}
JSON výstup (stdout): { "value": "schvaleny nazev" } nebo { "value": null }
"""
import json
import sys
from pathlib import Path
import tkinter as tk
from tkinter import font as tkfont
if sys.platform == "win32":
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except Exception:
pass
# ── PDF rendering ─────────────────────────────────────────────────────────────
def _open_doc(path_str: str):
import fitz
from PIL import Image
p = Path(path_str)
if not p.exists():
return None, None
suffix = p.suffix.lower()
if suffix in (".jpg", ".jpeg", ".png"):
return None, Image.open(p)
return fitz.open(str(p)), None
def _render(doc, pil_img, page_n: int, max_w: int, max_h: int):
import fitz
from PIL import Image
if doc is not None:
page = doc[page_n]
zoom = min(max_w / page.rect.width, max_h / page.rect.height)
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
elif pil_img is not None:
img = pil_img.copy()
img.thumbnail((max_w, max_h))
return img
return None
class PdfPane:
def __init__(self, parent, max_w: int, max_h: int, bg: str = "#1e1e2e", label_text: str = ""):
self.max_w = max_w
self.max_h = max_h
self.doc = None
self.pil_img = None
self.page_n = 0
self.page_count = 1
self.photo_ref = None
self.frame = tk.Frame(parent, bg=bg, width=max_w, height=max_h)
self.frame.pack_propagate(False)
if label_text:
tk.Label(self.frame, text=label_text, bg=bg, fg="#888",
font=("Segoe UI", 8, "bold")).pack(pady=(2, 0))
self.lbl_title = tk.Label(self.frame, text="", bg=bg, fg="#aaa",
font=("Segoe UI", 8), wraplength=max_w - 10)
self.lbl_title.pack(pady=(2, 0))
self.lbl_img = tk.Label(self.frame, bg=bg)
self.lbl_img.pack(fill="both", expand=True)
frame_nav = tk.Frame(self.frame, bg=bg)
frame_nav.pack(pady=2)
self.lbl_page = tk.Label(frame_nav, text="", bg=bg, fg="#aaa", font=("Segoe UI", 8))
self.lbl_page.pack(side="left", padx=4)
self.btn_prev = tk.Button(frame_nav, text="", bg="#333", fg="#fff",
relief="flat", padx=4, font=("Segoe UI", 8),
command=self._prev)
self.btn_prev.pack(side="left", padx=1)
self.btn_next = tk.Button(frame_nav, text="", bg="#333", fg="#fff",
relief="flat", padx=4, font=("Segoe UI", 8),
command=self._next)
self.btn_next.pack(side="left", padx=1)
def load(self, path_str: str, title: str = ""):
from PIL import ImageTk
if self.doc:
try: self.doc.close()
except Exception: pass
self.doc, self.pil_img = _open_doc(path_str)
self.page_count = len(self.doc) if self.doc else 1
self.lbl_title.config(text=title or Path(path_str).name, fg="#ccc")
self._show(0)
def clear(self, msg: str = ""):
if self.doc:
try: self.doc.close()
except Exception: pass
self.doc = None
self.pil_img = None
self.lbl_img.config(image="")
self.lbl_title.config(text=msg, fg="#555")
self.lbl_page.config(text="")
self.btn_prev.config(state="disabled")
self.btn_next.config(state="disabled")
self.photo_ref = None
def _show(self, n: int):
from PIL import ImageTk
self.page_n = n
img = _render(self.doc, self.pil_img, n, self.max_w - 10, self.max_h - 60)
if img:
self.photo_ref = ImageTk.PhotoImage(img)
self.lbl_img.config(image=self.photo_ref)
self.lbl_page.config(text=f"{n+1} / {self.page_count}" if self.page_count > 1 else "")
self.btn_prev.config(state="normal" if n > 0 else "disabled")
self.btn_next.config(state="normal" if n < self.page_count - 1 else "disabled")
def _prev(self):
if self.page_n > 0: self._show(self.page_n - 1)
def _next(self):
if self.page_n < self.page_count - 1: self._show(self.page_n + 1)
def close(self):
if self.doc:
try: self.doc.close()
except Exception: pass
# ── Pomocné funkce pro layout ─────────────────────────────────────────────────
def _get_layout() -> dict:
"""Načte layout oken pro aktuální hostname z layout_settings.json."""
import json as _json, socket as _socket
settings_file = Path(__file__).parent / "layout_settings.json"
hostname = _socket.gethostname().upper()
_default = {"duplicity_viewer": None}
if not settings_file.exists():
return _default
try:
settings = _json.loads(settings_file.read_text(encoding="utf-8"))
return settings.get(hostname, _default)
except Exception:
return _default
# ── Hlavní funkce ─────────────────────────────────────────────────────────────
def show(
original_path: str = "",
duplicity_paths: list = None,
labels: list = None,
nazev: str = "",
info_lines: list = None,
varianty: list = None,
) -> str | None:
"""
Zobrazí hlavní viewer přímo (bez subprocesů).
Vrátí schválený název souboru (bez .pdf) nebo None.
"""
duplicity_paths = duplicity_paths or []
labels = labels or [Path(p).name for p in duplicity_paths]
info_lines = info_lines or []
varianty = varianty or []
result = {"value": None}
try:
from PIL import Image, ImageTk
import fitz
except ImportError as e:
print(f"[main_viewer] Chybí knihovna: {e}", file=sys.stderr)
return None
# ── Layout ────────────────────────────────────────────────────────────────
try:
lw = _get_layout().get("duplicity_viewer") or {}
except Exception:
lw = {}
WIN_X = lw.get("x", 0)
WIN_Y = lw.get("y", 0)
WIN_W = lw.get("w", 3840)
WIN_H = lw.get("h", 1700)
BOTTOM_H = 260 # výška spodního panelu
TOP_H = WIN_H - BOTTOM_H - 8
GAP = 4 # mezera mezi náhledy (zmenšená)
LISTBOX_W = 560 # širší listbox duplicit
PANE_W = (WIN_W - LISTBOX_W - GAP - 20) // 2
BG = "#1a1a1a" # jednotné tmavé pozadí pro celé okno
COL_INFO = int(WIN_W * 0.15)
COL_MID = int(WIN_W * 0.45)
COL_VAR = WIN_W - COL_INFO - COL_MID
# ── Okno ──────────────────────────────────────────────────────────────────
root = tk.Tk()
root.tk.call("encoding", "system", "utf-8")
root.title("Zpracování dokumentu")
root.configure(bg=BG)
root.geometry(f"{WIN_W}x{WIN_H}+{WIN_X}+{WIN_Y}")
root.resizable(True, True)
# ═══════════════════════════════════════════════════════════════════════════
# HORNÍ ČÁST — náhledy + listbox
# ═══════════════════════════════════════════════════════════════════════════
frame_top = tk.Frame(root, bg=BG, height=TOP_H)
frame_top.pack(side="top", fill="x", expand=False)
frame_top.pack_propagate(False)
# Náhled originálu
pane_orig = PdfPane(frame_top, max_w=PANE_W, max_h=TOP_H,
bg=BG, label_text="ORIGINÁL")
pane_orig.frame.pack(side="left", fill="y", padx=(6, GAP), pady=4)
# Náhled duplicity
pane_dup = PdfPane(frame_top, max_w=PANE_W, max_h=TOP_H,
bg=BG, label_text="DUPLICITA")
pane_dup.frame.pack(side="left", fill="y", padx=(GAP, GAP), pady=4)
# Listbox duplicit vpravo
frame_lb = tk.Frame(frame_top, bg=BG, width=LISTBOX_W)
frame_lb.pack(side="left", fill="y", padx=(3, 6), pady=4)
frame_lb.pack_propagate(False)
tk.Label(frame_lb, text="Existující dokumenty:", anchor="w",
bg=BG, fg="#cc4444", font=("Segoe UI", 9, "bold")).pack(
fill="x", padx=6, pady=(8, 2))
sb_dup = tk.Scrollbar(frame_lb, orient="vertical")
txt_dup = tk.Text(
frame_lb, yscrollcommand=sb_dup.set,
font=("Segoe UI", 9), bg=BG, fg="#ddd",
bd=0, highlightthickness=0, relief="flat",
wrap="word", cursor="hand2", state="normal",
selectbackground="#ffffff", selectforeground="#000000",
)
sb_dup.config(command=txt_dup.yview)
sb_dup.pack(side="right", fill="y")
txt_dup.pack(side="left", fill="both", expand=True, padx=(6, 0))
# Tag pro výběr (zvýraznění vybraného řádku)
txt_dup.tag_config("selected", background="#ffffff", foreground="#000000")
txt_dup.tag_config("normal", background=BG, foreground="#ddd")
def _shorten_dup_label(name: str) -> str:
import re as _re
m = _re.match(r"\d{9,10}\s+(\d{4}-\d{2}-\d{2})\s+[^[]+(\[.+)", name)
if m:
return f"{m.group(1)} {m.group(2)}"
return name
dup_line_indices = [] # (line_start, line_end) pro každou duplicitu
if not duplicity_paths:
txt_dup.insert("end", "(žádné duplicity)")
txt_dup.config(state="disabled")
else:
for i, label in enumerate(labels):
short = _shorten_dup_label(Path(label).name if not Path(label).exists() else label)
line_start = txt_dup.index("end")
txt_dup.insert("end", short + "\n\n")
line_end = txt_dup.index("end")
dup_line_indices.append((line_start, line_end))
txt_dup.tag_add("normal", line_start, line_end)
txt_dup.config(state="disabled")
selected_dup = [None]
def _dup_click(event):
txt_dup.config(state="normal")
idx = txt_dup.index(f"@{event.x},{event.y}")
for i, (ls, le) in enumerate(dup_line_indices):
if txt_dup.compare(ls, "<=", idx) and txt_dup.compare(idx, "<", le):
# Odznač předchozí
if selected_dup[0] is not None:
prev_ls, prev_le = dup_line_indices[selected_dup[0]]
txt_dup.tag_remove("selected", prev_ls, prev_le)
txt_dup.tag_add("normal", prev_ls, prev_le)
# Označ nový
txt_dup.tag_remove("normal", ls, le)
txt_dup.tag_add("selected", ls, le)
selected_dup[0] = i
txt_dup.config(state="disabled")
# Načti PDF
if i < len(duplicity_paths):
pane_dup.load(duplicity_paths[i],
title=labels[i] if i < len(labels) else "")
return
txt_dup.config(state="disabled")
txt_dup.bind("<Button-1>", _dup_click)
# Oddělovač
tk.Frame(root, bg="#333", height=2).pack(fill="x")
# ═══════════════════════════════════════════════════════════════════════════
# SPODNÍ ČÁST — info | entry+tlačítka | návrhy
# ═══════════════════════════════════════════════════════════════════════════
frame_bot = tk.Frame(root, bg=BG, height=BOTTOM_H)
frame_bot.pack(side="top", fill="both", expand=True)
frame_bot.pack_propagate(False)
# ── Vlevo: info o pacientovi (15%) ────────────────────────────────────────
frame_info = tk.Frame(frame_bot, bg=BG, width=COL_INFO)
frame_info.pack(side="left", fill="y", padx=(10, 4), pady=8)
frame_info.pack_propagate(False)
tk.Label(frame_info, text="Informace o pacientovi", anchor="w",
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
for line in info_lines:
color = "#cc4444" if line.startswith("") or line.startswith("") else \
"#44cc44" if line.startswith("") else "#aaa"
tk.Label(frame_info, text=line, anchor="w", bg=BG,
fg=color, font=("Segoe UI", 9), wraplength=COL_INFO - 20,
justify="left").pack(fill="x", pady=1)
# ── Uprostřed: název + tlačítka (45%) ────────────────────────────────────
frame_mid = tk.Frame(frame_bot, bg=BG, width=COL_MID)
frame_mid.pack(side="left", fill="y", padx=(4, 20), pady=8)
frame_mid.pack_propagate(False)
tk.Label(frame_mid, text="Název souboru (bez .pdf):", anchor="w",
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
nazev_bez = nazev[:-4] if nazev.endswith(".pdf") else nazev
# Multiline Text widget — zalamuje dlouhé názvy
txt = tk.Text(frame_mid, font=("Segoe UI", 10), bg="#ffffff", fg="#000000",
insertbackground="#000000", relief="flat", height=3,
wrap="word", padx=6, pady=4, bd=0, highlightthickness=0)
txt.pack(fill="x", pady=(4, 0))
txt.insert("1.0", nazev_bez)
txt.mark_set("insert", "end")
txt.focus_set()
def get_txt_value() -> str:
return txt.get("1.0", "end").strip()
frame_btn = tk.Frame(frame_mid, bg=BG)
frame_btn.pack(pady=(6, 0))
def schvalit(event=None):
result["value"] = get_txt_value()
pane_orig.close()
pane_dup.close()
root.destroy()
def preskocit(event=None):
result["value"] = None
pane_orig.close()
pane_dup.close()
root.destroy()
tk.Button(frame_btn, text="✓ Schválit (Enter)", command=schvalit,
bg="#2a7a2a", fg="white", font=("Segoe UI", 10, "bold"),
padx=16, pady=6, relief="flat").pack(side="left", padx=8)
tk.Button(frame_btn, text="✗ Přeskočit (Esc)", command=preskocit,
bg="#7a2a2a", fg="white", font=("Segoe UI", 10),
padx=16, pady=6, relief="flat").pack(side="left", padx=8)
# Enter schvalí jen pokud není focus v Text widgetu (tam Enter = nový řádek)
root.bind("<Escape>", preskocit)
# Ctrl+Enter vždy schválí
root.bind("<Control-Return>", schvalit)
# ── Vpravo: návrhy od Claudea (40%) ──────────────────────────────────────
frame_right = tk.Frame(frame_bot, bg=BG, width=COL_VAR)
frame_right.pack(side="left", fill="y", padx=(4, 10), pady=8)
frame_right.pack_propagate(False)
tk.Label(frame_right, text="Návrhy pojmenování (kliknutím vyberte):", anchor="w",
bg=BG, fg="#888", font=("Segoe UI", 8, "bold")).pack(fill="x")
sb_var = tk.Scrollbar(frame_right, orient="vertical")
lb_var = tk.Listbox(
frame_right, yscrollcommand=sb_var.set,
font=("Segoe UI", 10), selectmode="single", activestyle="none",
bg=BG, fg="#ddd", bd=0, highlightthickness=0,
selectbackground="#ffffff", selectforeground="#000000",
cursor="hand2",
)
sb_var.config(command=lb_var.yview)
sb_var.pack(side="right", fill="y")
lb_var.pack(side="left", fill="both", expand=True)
for v in varianty:
v_bez = v[:-4] if v.endswith(".pdf") else v
lb_var.insert(tk.END, v_bez)
def on_varianta(event):
sel = lb_var.curselection()
if sel:
txt.delete("1.0", "end")
txt.insert("1.0", lb_var.get(sel[0]))
lb_var.bind("<<ListboxSelect>>", on_varianta)
# ═══════════════════════════════════════════════════════════════════════════
# Načtení dokumentů
# ═══════════════════════════════════════════════════════════════════════════
if original_path and Path(original_path).exists():
pane_orig.load(original_path, title=Path(original_path).name)
else:
pane_orig.clear(msg="(originál nedostupný)")
pane_dup.clear(msg="← vyberte duplicitu" if duplicity_paths else "(žádné duplicity)")
# Automaticky zobraz první duplicitu
if duplicity_paths:
pane_dup.load(duplicity_paths[0], title=labels[0] if labels else "")
if dup_line_indices:
txt_dup.config(state="normal")
ls, le = dup_line_indices[0]
txt_dup.tag_remove("normal", ls, le)
txt_dup.tag_add("selected", ls, le)
selected_dup[0] = 0
txt_dup.config(state="disabled")
# ── Zobrazení ─────────────────────────────────────────────────────────────
root.protocol("WM_DELETE_WINDOW", preskocit)
root.lift()
root.attributes("-topmost", True)
root.after(1500, lambda: root.attributes("-topmost", False))
root.mainloop()
return result["value"]
if __name__ == "__main__":
# Subprocess mód — čte data z JSON souboru předaného jako argument
if len(sys.argv) < 2:
print(json.dumps({"value": None}))
sys.exit(0)
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
value = show(
original_path = data.get("original") or "",
duplicity_paths = data.get("duplicity") or [],
labels = data.get("labels") or [],
nazev = data.get("nazev") or "",
info_lines = data.get("info_lines") or [],
varianty = data.get("varianty") or [],
)
print(json.dumps({"value": value}, ensure_ascii=False))