""" Kombinovaný viewer: náhled originálu + náhled duplicit + listbox duplicit. Nahrazuje preview_viewer.py pro hlavní dokument. Spouští se jako subprocess z extract_patient_info_novy_test.py. Argumenty: duplicity_viewer.py [--write-geometry=] JSON vstup: { "original": "cesta/k/originalu.pdf", # povinné "duplicity": ["cesta1.pdf", ...], # volitelné, může být prázdné "labels": ["název1.pdf", ...] # zobrazované názvy v listboxu } Výstup geometrie (spodní hrana) do --write-geometry souboru pro rename_dialog. """ import json import sys from pathlib import Path import tkinter as tk if sys.platform == "win32": try: from ctypes import windll windll.shcore.SetProcessDpiAwareness(1) except Exception: pass import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") def _open_doc(path_str: str): """Otevře PDF nebo obrázek, vrátí (fitz.Document nebo None, PIL.Image nebo None).""" 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_page(doc, pil_img, page_n: int, max_w: int, max_h: int): """Vykreslí stránku, vrátí PIL.Image.""" 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: """Jeden panel s náhledem PDF + navigací stránek.""" def __init__(self, parent, max_w: int, max_h: int, bg: str = "#222"): 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) self.frame.pack_propagate(False) self.lbl_title = tk.Label(self.frame, text="", bg=bg, fg="#aaa", font=("Segoe UI", 9), wraplength=max_w - 10) self.lbl_title.pack(pady=(4, 2)) 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=4) self.lbl_page = tk.Label(frame_nav, text="", bg=bg, fg="#ccc", font=("Segoe UI", 9)) self.lbl_page.pack(side="left", padx=6) self.btn_prev = tk.Button(frame_nav, text="◄", bg="#444", fg="#fff", relief="flat", padx=6, command=self._prev) self.btn_prev.pack(side="left", padx=2) self.btn_next = tk.Button(frame_nav, text="►", bg="#444", fg="#fff", relief="flat", padx=6, command=self._next) self.btn_next.pack(side="left", padx=2) 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="#ddd") 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="#666") 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_page(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 def main(): if len(sys.argv) < 2: sys.exit(1) data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) original_path = data.get("original") or "" duplicity_paths = data.get("duplicity") or [] labels = data.get("labels") or [Path(p).name for p in duplicity_paths] write_geom = None for arg in sys.argv: if arg.startswith("--write-geometry="): write_geom = Path(arg.split("=", 1)[1]) try: from PIL import Image, ImageTk import fitz except ImportError as e: print(f"[duplicity_viewer] Chybí knihovna: {e}", file=sys.stderr) sys.exit(2) # ── Layout z JSON ───────────────────────────────────────────────────────── sys.path.insert(0, str(Path(__file__).parent)) try: from window_layout import get_layout layout = get_layout() lw = 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", 2200) WIN_H = lw.get("h", 1080) LISTBOX_W = 380 PANE_W = (WIN_W - LISTBOX_W - 20) // 2 PANE_H = WIN_H - 40 # ── Okno ────────────────────────────────────────────────────────────────── root = tk.Tk() root.tk.call("encoding", "system", "utf-8") root.title("Náhled dokumentů") root.configure(bg="#1a1a1a") root.geometry(f"{WIN_W}x{WIN_H}+{WIN_X}+{WIN_Y}") # ── Tři sloupce ─────────────────────────────────────────────────────────── # Vlevo: originál pane_orig = PdfPane(root, max_w=PANE_W, max_h=PANE_H, bg="#1e1e2e") pane_orig.frame.pack(side="left", fill="both", expand=False, padx=(6, 3), pady=6) # Uprostřed: duplicita pane_dup = PdfPane(root, max_w=PANE_W, max_h=PANE_H, bg="#2e1e1e") pane_dup.frame.pack(side="left", fill="both", expand=False, padx=(3, 3), pady=6) # Vpravo: listbox frame_right = tk.Frame(root, bg="#1a1a1a", width=LISTBOX_W) frame_right.pack(side="left", fill="y", padx=(3, 6), pady=6) frame_right.pack_propagate(False) tk.Label(frame_right, text="Existující dokumenty:", anchor="w", bg="#1a1a1a", fg="#cc4444", font=("Segoe UI", 9, "bold")).pack( fill="x", padx=4, pady=(8, 2)) sb = tk.Scrollbar(frame_right, orient="vertical") lb = tk.Listbox( frame_right, yscrollcommand=sb.set, font=("Segoe UI", 8), selectmode="single", activestyle="dotbox", bg="#2a1a1a", fg="#ddd", selectbackground="#cc4444", selectforeground="#fff", cursor="hand2", ) sb.config(command=lb.yview) sb.pack(side="right", fill="y") lb.pack(side="left", fill="both", expand=True, padx=(4, 0)) if not duplicity_paths: lb.insert(tk.END, "(žádné duplicity)") lb.config(state="disabled") else: for label in labels: lb.insert(tk.END, Path(label).name if Path(label).exists() else label) # ── Načti originál ──────────────────────────────────────────────────────── 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 vlevo" if duplicity_paths else "(žádné duplicity)") # ── Výběr duplicity ─────────────────────────────────────────────────────── def on_select(event): sel = lb.curselection() if not sel: return idx = sel[0] p = duplicity_paths[idx] pane_dup.load(p, title=labels[idx] if idx < len(labels) else Path(p).name) lb.bind("<>", on_select) # Automaticky vyber první duplicitu if duplicity_paths: lb.selection_set(0) lb.event_generate("<>") # ── Zavření ─────────────────────────────────────────────────────────────── def on_close(): pane_orig.close() pane_dup.close() root.destroy() root.protocol("WM_DELETE_WINDOW", on_close) # ── Geometrie pro rename_dialog ─────────────────────────────────────────── root.update_idletasks() if write_geom: _y = root.winfo_y() _h = root.winfo_height() write_geom.write_text( json.dumps({"x": WIN_X, "y": _y, "w": WIN_W, "h": _h}), encoding="utf-8" ) root.lift() root.attributes("-topmost", True) root.after(1500, lambda: root.attributes("-topmost", False)) root.mainloop() if __name__ == "__main__": main()