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

292 lines
10 KiB
Python

"""
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 <json_soubor> [--write-geometry=<cesta>]
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("<<ListboxSelect>>", on_select)
# Automaticky vyber první duplicitu
if duplicity_paths:
lb.selection_set(0)
lb.event_generate("<<ListboxSelect>>")
# ── 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()