463 lines
19 KiB
Python
463 lines
19 KiB
Python
"""
|
|
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_soubor>
|
|
|
|
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("<Button-1>", _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("<Escape>", preskocit)
|
|
# Ctrl+Enter vždy schválí
|
|
root.bind("<Control-Return>", 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("<<ListboxSelect>>", 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))
|