z230
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user