292 lines
10 KiB
Python
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()
|