""" 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 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("", _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("", preskocit) # Ctrl+Enter vždy schválí root.bind("", 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("<>", 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))