z230
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -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()
|
||||
@@ -0,0 +1,763 @@
|
||||
"""
|
||||
Zpracování naskenovaných PDF — nová verze.
|
||||
1. Preview originálu + Claude Vision API
|
||||
2. Rename dialog
|
||||
3. 5 variant komprese → uživatel vybere
|
||||
4. Uložit do Processed, smazat originál
|
||||
"""
|
||||
import base64
|
||||
import gc
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
if sys.platform == "win32":
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
import anthropic
|
||||
from pdf2image import convert_from_path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
from Knihovny.najdi_dropbox import get_dropbox_root
|
||||
|
||||
FB_CONFIG = {
|
||||
'dsn': r'reporter:c:\medicus\medicus.fdb',
|
||||
'user': 'SYSDBA',
|
||||
'password': 'masterkey',
|
||||
'charset': 'win1250',
|
||||
}
|
||||
|
||||
def _load_env():
|
||||
env_path = Path(__file__).parent.parent / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if "=" in line and not line.startswith("#"):
|
||||
k, v = line.split("=", 1)
|
||||
os.environ[k.strip()] = v.strip()
|
||||
|
||||
_load_env()
|
||||
|
||||
def _find_poppler() -> str | None:
|
||||
candidates = [
|
||||
r"C:/Poppler/Library/bin",
|
||||
str(Path.home() / r"scoop\apps\poppler\current\Library\bin"),
|
||||
str(Path.home() / r"scoop\apps\poppler\current\bin"),
|
||||
]
|
||||
for p in candidates:
|
||||
if Path(p).exists():
|
||||
return p
|
||||
pdfinfo = shutil.which("pdfinfo")
|
||||
if pdfinfo:
|
||||
return str(Path(pdfinfo).parent)
|
||||
return None
|
||||
|
||||
POPPLER_PATH = _find_poppler()
|
||||
CORRECTIONS = True # True = corrections.json se načítá a ukládá; False = ignorovat
|
||||
|
||||
_DROPBOX = Path(get_dropbox_root())
|
||||
TO_PROCESS = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\KeZpracování"
|
||||
PROCESSED = _DROPBOX / r"Ordinace\Dokumentace_ke_zpracování\Ricoh Fi-8040\Zpracováno"
|
||||
CORRECTIONS_FILE = Path(__file__).parent / "corrections.json"
|
||||
NAMING_RULES_FILE = Path(__file__).parent / "naming_rules.md"
|
||||
DOKUMENTACE = _DROPBOX / r"Ordinace\Dokumentace_zpracovaná"
|
||||
|
||||
import threading
|
||||
|
||||
_dokumentace_index: set[str] = set()
|
||||
_dokumentace_ready = threading.Event()
|
||||
|
||||
def _load_dokumentace_index_bg():
|
||||
if DOKUMENTACE.exists():
|
||||
names = {f.name for f in DOKUMENTACE.iterdir() if f.is_file()}
|
||||
else:
|
||||
names = set()
|
||||
global _dokumentace_index
|
||||
_dokumentace_index = names
|
||||
_dokumentace_ready.set()
|
||||
print(f" Index dokumentace: {len(names)} souborů načteno.")
|
||||
|
||||
def start_dokumentace_index():
|
||||
t = threading.Thread(target=_load_dokumentace_index_bg, daemon=True)
|
||||
t.start()
|
||||
|
||||
VIEWER = Path(__file__).parent / "preview_viewer.py"
|
||||
RENAME_DIALOG = Path(__file__).parent / "rename_dialog.py"
|
||||
VARIANT_PICKER = Path(__file__).parent / "variant_picker.py"
|
||||
|
||||
# 5 kompresních variant
|
||||
COMPRESS_VARIANTS = [
|
||||
("300 DPI / q90", 300, 90),
|
||||
("200 DPI / q85", 200, 85),
|
||||
("150 DPI / q80", 150, 80),
|
||||
("120 DPI / q75", 120, 75),
|
||||
( "96 DPI / q70", 96, 70),
|
||||
]
|
||||
|
||||
|
||||
# ─── Komprese jedné varianty ──────────────────────────────────────────────────
|
||||
|
||||
def set_single_page_view(pdf_path: Path):
|
||||
from pikepdf import Pdf, Name
|
||||
with Pdf.open(str(pdf_path), allow_overwriting_input=True) as pdf:
|
||||
pdf.Root.PageLayout = Name("/SinglePage")
|
||||
pdf.Root.PageMode = Name("/UseNone")
|
||||
pdf.save()
|
||||
|
||||
|
||||
def compress_to_temp(pdf_path: Path, dpi: int, quality: int) -> Path:
|
||||
import fitz
|
||||
src = fitz.open(str(pdf_path))
|
||||
mat = fitz.Matrix(dpi / 72.0, dpi / 72.0)
|
||||
out = fitz.open()
|
||||
for page in src:
|
||||
pix = page.get_pixmap(matrix=mat, colorspace=fitz.csRGB)
|
||||
img_bytes = pix.tobytes("jpeg", jpg_quality=quality)
|
||||
img_doc = fitz.open("pdf", fitz.open("jpeg", img_bytes).convert_to_pdf())
|
||||
rect = page.rect
|
||||
np = out.new_page(width=rect.width, height=rect.height)
|
||||
np.show_pdf_page(np.rect, img_doc, 0)
|
||||
src.close()
|
||||
tmp = Path(tempfile.mktemp(suffix=".pdf"))
|
||||
out.save(tmp, deflate=True, garbage=4)
|
||||
out.close()
|
||||
return tmp
|
||||
|
||||
|
||||
# ─── Medicus ověření ─────────────────────────────────────────────────────────
|
||||
|
||||
def _medicus_connect():
|
||||
try:
|
||||
import fdb
|
||||
return fdb.connect(**FB_CONFIG)
|
||||
except Exception as e:
|
||||
print(f" [Medicus] Nepřipojeno: {e}")
|
||||
return None
|
||||
|
||||
def _lookup_by_rc(cur, rc_digits: str) -> dict | None:
|
||||
cur.execute(
|
||||
"SELECT IDPAC, PRIJMENI, JMENO, RODCIS FROM KAR "
|
||||
"WHERE REPLACE(RODCIS, '/', '') = ?", (rc_digits,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
return {"idpac": row[0], "prijmeni": row[1].strip(), "jmeno": row[2].strip(), "rodcis": row[3].strip()}
|
||||
return None
|
||||
|
||||
def _rc_candidates(rc: str) -> list[str]:
|
||||
similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"}
|
||||
candidates = set()
|
||||
for i in range(len(rc)):
|
||||
candidates.add(rc[:i] + rc[i+1:])
|
||||
for i in range(len(rc) + 1):
|
||||
candidates.add(rc[:i] + "0" + rc[i:])
|
||||
for i, ch in enumerate(rc):
|
||||
if ch in similar:
|
||||
candidates.add(rc[:i] + similar[ch] + rc[i+1:])
|
||||
candidates.discard(rc)
|
||||
return sorted(c for c in candidates if len(c) in (9, 10))
|
||||
|
||||
def _rc_checksum_ok(rc: str) -> bool:
|
||||
digits = re.sub(r"\D", "", rc)
|
||||
if len(digits) == 10:
|
||||
return int(digits) % 11 == 0
|
||||
return True
|
||||
|
||||
def _parse_jmeno_prijmeni(name_str: str) -> tuple[str, str] | None:
|
||||
"""Parsuje 'Příjmení, Jméno' nebo 'Příjmení Jméno' -> (prijmeni, jmeno)."""
|
||||
name_str = name_str.strip()
|
||||
if "," in name_str:
|
||||
parts = [p.strip() for p in name_str.split(",", 1)]
|
||||
if len(parts) == 2 and parts[0] and parts[1]:
|
||||
return parts[0], parts[1]
|
||||
parts = name_str.split()
|
||||
if len(parts) >= 2:
|
||||
return parts[0], " ".join(parts[1:])
|
||||
return None
|
||||
|
||||
def _lookup_by_name(cur, prijmeni: str, jmeno: str) -> list[dict]:
|
||||
"""Vyhledá pacienty v KAR podle příjmení a prvního slova jména."""
|
||||
jmeno_first = jmeno.split()[0] if jmeno.split() else jmeno
|
||||
cur.execute(
|
||||
"SELECT IDPAC, PRIJMENI, JMENO, RODCIS FROM KAR "
|
||||
"WHERE UPPER(PRIJMENI) = UPPER(?)",
|
||||
(prijmeni,)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
result = []
|
||||
for row in rows:
|
||||
db_jmeno = (row[2] or "").strip().upper()
|
||||
if db_jmeno.startswith(jmeno_first.upper()):
|
||||
result.append({
|
||||
"idpac": row[0],
|
||||
"prijmeni": row[1].strip(),
|
||||
"jmeno": row[2].strip(),
|
||||
"rodcis": row[3].strip(),
|
||||
})
|
||||
return result
|
||||
|
||||
def verify_patient_by_name(name_str: str) -> dict:
|
||||
"""Vyhledá pacienta v Medicus podle jména — fallback když RČ chybí."""
|
||||
parsed = _parse_jmeno_prijmeni(name_str)
|
||||
if not parsed:
|
||||
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||
prijmeni, jmeno = parsed
|
||||
con = _medicus_connect()
|
||||
if con is None:
|
||||
return {"status": "offline", "patient": None, "rc_corrected": None}
|
||||
try:
|
||||
cur = con.cursor()
|
||||
matches = _lookup_by_name(cur, prijmeni, jmeno)
|
||||
if not matches:
|
||||
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||
if len(matches) == 1:
|
||||
return {"status": "by_name", "patient": matches[0], "rc_corrected": None}
|
||||
return {"status": "by_name_multi", "patient": matches[0], "rc_corrected": None, "all_matches": matches}
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
def verify_patient(rc_raw: str) -> dict:
|
||||
rc = re.sub(r"\D", "", rc_raw or "")
|
||||
if not rc:
|
||||
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||
con = _medicus_connect()
|
||||
if con is None:
|
||||
return {"status": "offline", "patient": None, "rc_corrected": None}
|
||||
try:
|
||||
cur = con.cursor()
|
||||
patient = _lookup_by_rc(cur, rc)
|
||||
if patient:
|
||||
return {"status": "ok", "patient": patient, "rc_corrected": None}
|
||||
candidates = _rc_candidates(rc)
|
||||
matches = [(c, _lookup_by_rc(cur, c)) for c in candidates]
|
||||
matches = [(c, p) for c, p in matches if p]
|
||||
if not matches:
|
||||
return {"status": "not_found", "patient": None, "rc_corrected": None}
|
||||
matches.sort(key=lambda x: (0 if _rc_checksum_ok(x[0]) else 1))
|
||||
best_rc, best_patient = matches[0]
|
||||
return {"status": "fuzzy", "patient": best_patient, "rc_corrected": best_rc, "all_matches": matches}
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
def check_duplicates(rc: str, datum: str) -> list[str]:
|
||||
if not rc or not datum:
|
||||
return []
|
||||
# Počkej max 15s na dokončení indexu (typicky hotovo za dobu volání Claude)
|
||||
_dokumentace_ready.wait(timeout=15)
|
||||
prefix = f"{rc} {datum}"
|
||||
return [name for name in _dokumentace_index if name.startswith(prefix)]
|
||||
|
||||
|
||||
# ─── EKG zpracování ──────────────────────────────────────────────────────────
|
||||
|
||||
_EKG_FLAG = "rotated-by-script"
|
||||
|
||||
|
||||
def _is_ekg(pdf_path: Path) -> bool:
|
||||
"""Detekuje EKG PDF podle metadat — PDFCreator 2.4.x je specifický pro EKG přístroj."""
|
||||
if pdf_path.suffix.lower() != ".pdf":
|
||||
return False
|
||||
try:
|
||||
import fitz
|
||||
doc = fitz.open(str(pdf_path))
|
||||
meta = doc.metadata
|
||||
doc.close()
|
||||
haystack = " ".join(filter(None, [
|
||||
meta.get("creator", ""), meta.get("producer", "")
|
||||
])).lower()
|
||||
return "pdfcreator" in haystack
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _ekg_rotate_if_needed(pdf_path: Path):
|
||||
"""Otočí první stránku o 90° CW a odstraní případnou druhou stránku."""
|
||||
import fitz
|
||||
doc = fitz.open(str(pdf_path))
|
||||
meta = doc.metadata
|
||||
keywords = meta.get("keywords", "") or ""
|
||||
if _EKG_FLAG in keywords:
|
||||
doc.close()
|
||||
return
|
||||
page = doc[0]
|
||||
page.set_rotation((page.rotation + 90) % 360)
|
||||
if doc.page_count > 1:
|
||||
doc.delete_page(1)
|
||||
meta["keywords"] = (keywords + " " + _EKG_FLAG).strip()
|
||||
doc.set_metadata(meta)
|
||||
tmp = pdf_path.with_suffix(".tmp.pdf")
|
||||
doc.save(tmp, deflate=True)
|
||||
doc.close()
|
||||
os.replace(tmp, pdf_path)
|
||||
print(" [EKG] Stránka otočena o 90°.")
|
||||
|
||||
|
||||
def _ekg_ocr(pdf_path: Path) -> str:
|
||||
import fitz
|
||||
import pytesseract
|
||||
from PIL import Image as _PILImage
|
||||
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"
|
||||
doc = fitz.open(str(pdf_path))
|
||||
pix = doc[0].get_pixmap(dpi=300)
|
||||
img = _PILImage.frombytes("RGB", [pix.width, pix.height], pix.samples)
|
||||
doc.close()
|
||||
return pytesseract.image_to_string(img, lang="ces", config="--psm 6")
|
||||
|
||||
|
||||
def _ekg_extract_rc(text: str) -> str | None:
|
||||
m = re.search(r"(\d{6})\s*/?\s*(\d{3,4})", text)
|
||||
if not m:
|
||||
return None
|
||||
return m.group(1) + m.group(2).zfill(4)
|
||||
|
||||
|
||||
def _ekg_extract_date(text: str) -> str | None:
|
||||
"""Vrátí datum ve formátu YYYY-MM-DD nebo None."""
|
||||
m = re.search(r"(\d{1,2})[\.,]\s*(\d{1,2})[\.,]\s*(\d{4})", text)
|
||||
if m:
|
||||
d, mo, y = m.groups()
|
||||
return f"{y}-{mo.zfill(2)}-{d.zfill(2)}"
|
||||
for pat in [r"\b(\d{2})(\d{2})(\d{4})\b", r"\b(\d{2})(\d{1})(\d{4})\b"]:
|
||||
for m in re.finditer(pat, text):
|
||||
d, mo, y = m.groups()
|
||||
if 1 <= int(d) <= 31 and 1 <= int(mo) <= 12 and 1900 <= int(y) <= 2100:
|
||||
return f"{y}-{mo.zfill(2)}-{d.zfill(2)}"
|
||||
return None
|
||||
|
||||
|
||||
def extract_info_ekg(pdf_path: Path) -> dict:
|
||||
"""EKG větev: rotace in-place, Tesseract OCR, Medicus ověření."""
|
||||
_ekg_rotate_if_needed(pdf_path)
|
||||
|
||||
print(" [EKG] OCR přes Tesseract...")
|
||||
raw_text = _ekg_ocr(pdf_path)
|
||||
print(f"\n--- EKG OCR TEXT ---\n{raw_text}\n--- KONEC ---\n")
|
||||
|
||||
rc_ocr = _ekg_extract_rc(raw_text)
|
||||
date_iso = _ekg_extract_date(raw_text)
|
||||
print(f" [EKG] RČ: {rc_ocr or 'NENALEZENO'} | Datum: {date_iso or 'NENALEZENO'}")
|
||||
|
||||
print(f" [EKG] Ověřuji v Medicus (RČ: {rc_ocr or '?'})...")
|
||||
verif = verify_patient(rc_ocr or "")
|
||||
rc_final = rc_ocr
|
||||
if verif["status"] == "fuzzy" and verif.get("rc_corrected"):
|
||||
rc_final = verif["rc_corrected"]
|
||||
print(f" [EKG] RČ opraveno: {rc_ocr} → {rc_final}")
|
||||
|
||||
patient = verif.get("patient")
|
||||
name_part = f"{patient['prijmeni']}, {patient['jmeno']}" if patient else ""
|
||||
|
||||
if rc_final and date_iso:
|
||||
nazev = f"{rc_final} {date_iso}{(' ' + name_part) if name_part else ''} [EKG] [bez hodnocení].pdf"
|
||||
else:
|
||||
nazev = None
|
||||
|
||||
return {
|
||||
"rodne_cislo": rc_final,
|
||||
"datum_zpravy": date_iso,
|
||||
"nazev_souboru": nazev,
|
||||
"_verif": verif,
|
||||
"_rc_ocr": rc_ocr or "",
|
||||
}
|
||||
|
||||
|
||||
# ─── Korekce (few-shot příklady) ─────────────────────────────────────────────
|
||||
|
||||
def load_corrections() -> list[dict]:
|
||||
if not CORRECTIONS_FILE.exists():
|
||||
return []
|
||||
content = CORRECTIONS_FILE.read_text(encoding="utf-8")
|
||||
idx = content.find('[')
|
||||
if idx < 0:
|
||||
return []
|
||||
try:
|
||||
result = json.loads(content[idx:])
|
||||
# Pokud byl soubor poškozený (garbage před [), přepiš ho čistě
|
||||
if idx > 0:
|
||||
CORRECTIONS_FILE.write_text(
|
||||
json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
return result
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
def save_correction(original: str, corrected: str):
|
||||
if not CORRECTIONS:
|
||||
return
|
||||
corrections = load_corrections()
|
||||
for c in corrections:
|
||||
if c["original"] == original and c["corrected"] == corrected:
|
||||
return
|
||||
corrections.append({"original": original, "corrected": corrected})
|
||||
CORRECTIONS_FILE.write_text(
|
||||
json.dumps(corrections, ensure_ascii=False, indent=2), encoding="utf-8"
|
||||
)
|
||||
print(f" ✓ Korekce uložena ({len(corrections)} celkem)")
|
||||
|
||||
def load_naming_rules() -> str:
|
||||
if NAMING_RULES_FILE.exists():
|
||||
content = NAMING_RULES_FILE.read_text(encoding="utf-8").strip()
|
||||
if content:
|
||||
return f"Pravidla pro pojmenování souborů (dodržuj vždy):\n{content}\n\n"
|
||||
return ""
|
||||
|
||||
def build_corrections_prompt() -> str:
|
||||
if not CORRECTIONS:
|
||||
return ""
|
||||
corrections = load_corrections()
|
||||
if not corrections:
|
||||
return ""
|
||||
lines = ["Příklady korekcí z minulých běhů (uč se z nich):"]
|
||||
for c in corrections[-10:]:
|
||||
lines.append(f' - špatně: "{c["original"]}"')
|
||||
lines.append(f' správně: "{c["corrected"]}"')
|
||||
return "\n".join(lines) + "\n\n"
|
||||
|
||||
|
||||
# ─── Claude Vision API ────────────────────────────────────────────────────────
|
||||
|
||||
def extract_info(pdf_path: Path, known_patient: str | None = None, known_rc: str | None = None) -> dict:
|
||||
print(" Převádím na obrázek...")
|
||||
suffix = pdf_path.suffix.lower()
|
||||
if suffix in (".jpg", ".jpeg", ".png"):
|
||||
from PIL import Image
|
||||
img = Image.open(pdf_path)
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format="JPEG", quality=95)
|
||||
img.close()
|
||||
else:
|
||||
from PIL import Image as _PILImage
|
||||
_PILImage.MAX_IMAGE_PIXELS = None # vypni limit pro naše vlastní PDFs
|
||||
images = convert_from_path(str(pdf_path), poppler_path=POPPLER_PATH, dpi=300)
|
||||
buf = io.BytesIO()
|
||||
images[0].save(buf, format="JPEG", quality=95)
|
||||
del images
|
||||
gc.collect()
|
||||
image_b64 = base64.standard_b64encode(buf.getvalue()).decode("utf-8")
|
||||
|
||||
if known_patient and known_rc:
|
||||
# Identita pacienta je známa z názvu souboru — Claude se soustředí jen na obsah zprávy
|
||||
patient_hint = (
|
||||
f"Pacient je již znám: RČ={known_rc}, jméno={known_patient}. "
|
||||
f"Pole \"jmeno\" nastav na \"{known_patient}\" a \"rodne_cislo\" na \"{known_rc}\". "
|
||||
f"Soustřeď se hlavně na datum zprávy, typ dokumentu a klinickou poznámku.\n"
|
||||
)
|
||||
nazev_format = (
|
||||
f"\"{known_rc} {{datum_zpravy}} {known_patient} [{{typ_dokumentu}}] [{{poznamka}}].pdf\""
|
||||
)
|
||||
else:
|
||||
patient_hint = ""
|
||||
nazev_format = (
|
||||
"\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" "
|
||||
"(jméno bez titulu, RČ bez lomítka)"
|
||||
)
|
||||
|
||||
prompt = (
|
||||
load_naming_rules() +
|
||||
build_corrections_prompt() +
|
||||
patient_hint +
|
||||
"Toto je naskenovaná lékařská zpráva v češtině. "
|
||||
"Vrať JSON s těmito poli:\n"
|
||||
"- \"jmeno\": celé jméno pacienta (příjmení + jméno + případný titul)\n"
|
||||
"- \"rodne_cislo\": rodné číslo pacienta BEZ lomítka (pouze číslice)\n"
|
||||
"- \"datum_zpravy\": datum zprávy ve formátu YYYY-MM-DD\n"
|
||||
"- \"typ_dokumentu\": typ dokumentu — "
|
||||
"\"LZ {oddělení}\" = ambulantní/lékařská zpráva (např. \"LZ chirurgie\", \"LZ kardiologie\", \"LZ plicní\", \"LZ ORL\"); "
|
||||
"\"PZ {oddělení}\" = propouštěcí zpráva z hospitalizace (např. \"PZ interna\", \"PZ neurologie\"). "
|
||||
"Jiné typy: \"Laboratoř\", \"CT břicha\", \"MRI páteře\", \"kolonoskopie\", "
|
||||
"\"operační protokol oční\", \"poukaz FT\", \"diagnostická mamografie\" atd.\n"
|
||||
"- \"poznamka\": krátká klinická poznámka česky, max 80 znaků. "
|
||||
"DŮLEŽITÉ: pokud zpráva obsahuje sekci \"Závěr:\" nebo \"Závěr vyšetření:\", "
|
||||
"použij VÝHRADNĚ obsah této sekce — je nejdůležitější. "
|
||||
"Teprve pokud závěr chybí, shrň obsah z celé zprávy.\n"
|
||||
f"- \"nazev_souboru\": název souboru ve formátu {nazev_format}\n"
|
||||
"- \"rotace\": o kolik stupňů CCW je třeba otočit obrázek aby byl text čitelně na výšku nebo šířku "
|
||||
"(hodnoty: 0, 90, 180, 270). Pokud je text již správně orientovaný, vrať 0.\n\n"
|
||||
"Pokud pole nenajdeš, použij null. Nepiš nic jiného než JSON."
|
||||
)
|
||||
|
||||
print(" Volám Claude Vision API...")
|
||||
try:
|
||||
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
|
||||
response = client.messages.create(
|
||||
model="claude-sonnet-4-6",
|
||||
max_tokens=400,
|
||||
messages=[{"role": "user", "content": [
|
||||
{"type": "image", "source": {"type": "base64", "media_type": "image/jpeg", "data": image_b64}},
|
||||
{"type": "text", "text": prompt},
|
||||
]}],
|
||||
)
|
||||
usage = response.usage
|
||||
print(f" Tokeny: {usage.input_tokens} in + {usage.output_tokens} out = ${usage.input_tokens*3/1e6 + usage.output_tokens*15/1e6:.4f}")
|
||||
|
||||
raw = response.content[0].text.strip()
|
||||
if raw.startswith("```"):
|
||||
raw = raw.split("```")[1]
|
||||
if raw.startswith("json"):
|
||||
raw = raw[4:]
|
||||
try:
|
||||
return json.loads(raw.strip())
|
||||
except json.JSONDecodeError:
|
||||
print(f" VAROVÁNÍ: nelze parsovat JSON: {raw!r}")
|
||||
return {"nazev_souboru": None, "raw": raw}
|
||||
except Exception as e:
|
||||
print(f" VAROVÁNÍ: Claude API selhalo ({e}) — otevírám dialog pro ruční vyplnění.")
|
||||
return {"nazev_souboru": None}
|
||||
|
||||
|
||||
# ─── Subprocess helpers ───────────────────────────────────────────────────────
|
||||
|
||||
def open_preview(pdf_path: Path) -> tuple[subprocess.Popen, Path]:
|
||||
geom_file = Path(tempfile.mktemp(suffix=".json"))
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(VIEWER), str(pdf_path), f"--write-geometry={geom_file}"],
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
return proc, geom_file
|
||||
|
||||
|
||||
def read_preview_bottom(geom_file: Path, timeout: float = 5.0) -> int:
|
||||
import time
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if geom_file.exists():
|
||||
geom = json.loads(geom_file.read_text(encoding="utf-8"))
|
||||
geom_file.unlink(missing_ok=True)
|
||||
return geom["y"] + geom["h"] + 30 # +30 pro title bar
|
||||
time.sleep(0.1)
|
||||
geom_file.unlink(missing_ok=True)
|
||||
return None
|
||||
|
||||
|
||||
def run_rename_dialog(nazev: str, info_lines: list, below_y: int = None) -> str | None:
|
||||
tmp = Path(tempfile.mktemp(suffix=".json"))
|
||||
tmp.write_text(json.dumps({"nazev": nazev, "info_lines": info_lines}, ensure_ascii=False), encoding="utf-8")
|
||||
args = [sys.executable, str(RENAME_DIALOG), str(tmp)]
|
||||
if below_y is not None:
|
||||
args.append(f"--below-y={below_y}")
|
||||
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
|
||||
proc = subprocess.run(args, capture_output=True, text=True, encoding="utf-8", env=env)
|
||||
tmp.unlink(missing_ok=True)
|
||||
if proc.stderr.strip():
|
||||
print(f" [rename_dialog] CHYBA:\n{proc.stderr.strip()}")
|
||||
out = proc.stdout.strip()
|
||||
return json.loads(out).get("value") if out else None
|
||||
|
||||
|
||||
def run_variant_picker(variants_data: list) -> str | None:
|
||||
tmp = Path(tempfile.mktemp(suffix=".json"))
|
||||
tmp.write_text(json.dumps(variants_data, ensure_ascii=False), encoding="utf-8")
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(VARIANT_PICKER), str(tmp)],
|
||||
capture_output=True, text=True, encoding="utf-8",
|
||||
)
|
||||
tmp.unlink(missing_ok=True)
|
||||
if proc.returncode != 0 or not proc.stdout.strip():
|
||||
print(f" [variant_picker] returncode={proc.returncode}")
|
||||
if proc.stderr.strip():
|
||||
print(f" [variant_picker] CHYBA:\n{proc.stderr.strip()}")
|
||||
out = proc.stdout.strip()
|
||||
return json.loads(out).get("chosen") if out else None
|
||||
|
||||
|
||||
# ─── Detekce split názvu ──────────────────────────────────────────────────────
|
||||
|
||||
# Vzor: "7952090443 Kalousová, Eva split_001.pdf"
|
||||
_SPLIT_RE = re.compile(r"^(\d{9,10})\s+(.+?)\s+split_\d+\.pdf$", re.IGNORECASE)
|
||||
|
||||
def _parse_split_filename(name: str) -> tuple[str, str] | None:
|
||||
"""Vrátí (rc_digits, 'Příjmení, Jméno') nebo None."""
|
||||
m = _SPLIT_RE.match(name)
|
||||
if m:
|
||||
return m.group(1), m.group(2)
|
||||
return None
|
||||
|
||||
|
||||
# ─── Hlavní flow ──────────────────────────────────────────────────────────────
|
||||
|
||||
def process_file(pdf_path: Path):
|
||||
print(f"\nSoubor: {pdf_path.name}")
|
||||
|
||||
# Spusť načítání indexu dokumentace na pozadí — hotovo za dobu volání Claude/OCR
|
||||
start_dokumentace_index()
|
||||
|
||||
is_ekg = _is_ekg(pdf_path)
|
||||
split = None
|
||||
|
||||
if is_ekg:
|
||||
# EKG větev: rotace in-place PŘED preview, pak Tesseract OCR + Medicus
|
||||
print(" [EKG] Detekován EKG soubor (PDFCreator).")
|
||||
info = extract_info_ekg(pdf_path)
|
||||
nazev = info.get("nazev_souboru") or pdf_path.name
|
||||
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
|
||||
verif = info["_verif"]
|
||||
rc_ocr = info["_rc_ocr"]
|
||||
|
||||
# 1. Otevři preview (pro EKG: soubor je již otočen)
|
||||
preview, geom_file = open_preview(pdf_path)
|
||||
below_y = read_preview_bottom(geom_file)
|
||||
|
||||
if not is_ekg:
|
||||
# 2. Zjisti RČ a jméno — buď z názvu (split soubor) nebo přes Claude Vision API
|
||||
split = _parse_split_filename(pdf_path.name)
|
||||
if split:
|
||||
rc_from_scan, name_from_filename = split
|
||||
print(f" Split soubor — RČ z názvu: {rc_from_scan}, jméno: {name_from_filename}")
|
||||
info = extract_info(pdf_path, known_patient=name_from_filename, known_rc=rc_from_scan)
|
||||
nazev = info.get("nazev_souboru") or pdf_path.name
|
||||
nazev = re.sub(r"^\d{9,10}\s+", f"{rc_from_scan} ", nazev)
|
||||
else:
|
||||
info = extract_info(pdf_path)
|
||||
nazev = info.get("nazev_souboru") or pdf_path.name
|
||||
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
|
||||
|
||||
# 3. Medicus ověření + fuzzy matching RČ
|
||||
print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...")
|
||||
verif = verify_patient(rc_from_scan)
|
||||
rc_ocr = rc_from_scan
|
||||
|
||||
# Oprava RČ při fuzzy matchi (jen pro nesplit soubory — u split máme RC spolehlivé)
|
||||
if not split and verif["status"] == "fuzzy" and verif.get("rc_corrected") and nazev:
|
||||
nazev = nazev.replace(rc_from_scan, verif["rc_corrected"], 1)
|
||||
print(f" → RČ opraveno: {rc_from_scan} → {verif['rc_corrected']}")
|
||||
|
||||
# Fallback: RČ nenalezeno → zkus vyhledat podle jména z Claude
|
||||
if verif["status"] == "not_found" and info.get("jmeno"):
|
||||
jmeno_z_claude = info["jmeno"]
|
||||
print(f" RČ nenalezeno, zkouším vyhledat dle jména: {jmeno_z_claude}")
|
||||
verif_name = verify_patient_by_name(jmeno_z_claude)
|
||||
if verif_name["status"] in ("by_name", "by_name_multi"):
|
||||
verif = verif_name
|
||||
rc_z_medicus = re.sub(r"\D", "", verif["patient"]["rodcis"])
|
||||
p = verif["patient"]
|
||||
print(f" → Nalezeno dle jména: {p['prijmeni']} {p['jmeno']} | RČ {p['rodcis']}")
|
||||
if verif_name["status"] == "by_name_multi":
|
||||
print(f" ⚠ Více shod ({len(verif_name['all_matches'])}) — přijat první výsledek")
|
||||
# Aktualizuj RČ v navrženém názvu
|
||||
if nazev and rc_z_medicus:
|
||||
nazev = re.sub(r"^null\s*", rc_z_medicus + " ", nazev)
|
||||
if not nazev.startswith(rc_z_medicus):
|
||||
nazev = re.sub(r"^\S+\s*", rc_z_medicus + " ", nazev)
|
||||
|
||||
# Info řádky pro dialog
|
||||
status = verif["status"]
|
||||
patient = verif.get("patient")
|
||||
info_lines = []
|
||||
if is_ekg:
|
||||
info_lines.append("⚡ EKG soubor — Tesseract OCR")
|
||||
elif split:
|
||||
info_lines.append(f"⚡ Split soubor — identita z názvu: {name_from_filename} | RČ {rc_from_scan}")
|
||||
if status == "ok":
|
||||
info_lines.append(f"✓ Medicus: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "fuzzy":
|
||||
info_lines.append(f"⚠ RČ ze skenu '{rc_ocr}' → opraveno na {verif['rc_corrected']}")
|
||||
info_lines.append(f" Pacient: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "by_name":
|
||||
info_lines.append(f"✓ Nalezeno dle jména: {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "by_name_multi":
|
||||
count = len(verif.get("all_matches", []))
|
||||
info_lines.append(f"⚠ Nalezeno dle jména ({count} shod, 1. výsledek): {patient['prijmeni']} {patient['jmeno']} | RČ {patient['rodcis']}")
|
||||
elif status == "not_found":
|
||||
info_lines.append(f"✗ RČ '{rc_ocr}' nenalezeno v Medicus")
|
||||
else:
|
||||
info_lines.append("— Medicus nedostupný (offline)")
|
||||
|
||||
# Duplicity
|
||||
rc_final = re.sub(r"\D", "", verif["patient"]["rodcis"] if patient else rc_from_scan)
|
||||
duplicity = check_duplicates(rc_final, info.get("datum_zpravy") or "")
|
||||
if duplicity:
|
||||
info_lines.append(f"⚠ DUPLICITA: {', '.join(duplicity)}")
|
||||
|
||||
if not info_lines:
|
||||
info_lines = ["[uprav ručně]"]
|
||||
print(" Otevírám dialog pro schválení názvu...")
|
||||
final_name = run_rename_dialog(nazev, info_lines, below_y=below_y)
|
||||
|
||||
preview.terminate()
|
||||
stderr_out = preview.stderr.read().decode("utf-8", errors="replace").strip() if preview.stderr else ""
|
||||
if stderr_out:
|
||||
print(f" [preview] CHYBA: {stderr_out}")
|
||||
|
||||
if not final_name:
|
||||
print(" Přeskočeno.")
|
||||
return
|
||||
|
||||
if not final_name.endswith(".pdf"):
|
||||
final_name += ".pdf"
|
||||
final_name = re.sub(r'[<>:"/\\|?*]', '', final_name)
|
||||
|
||||
if nazev and final_name != nazev:
|
||||
save_correction(nazev, final_name)
|
||||
|
||||
print(f" Schválený název: {final_name}")
|
||||
|
||||
# 4. Generuj kompresní varianty (originál + 5 variant)
|
||||
print(" Generuji kompresní varianty...")
|
||||
temp_files = []
|
||||
orig_kb = round(pdf_path.stat().st_size / 1024)
|
||||
variants_data = [{"path": str(pdf_path), "label": "Originál", "size_kb": orig_kb}]
|
||||
for label, dpi, quality in COMPRESS_VARIANTS:
|
||||
tmp = compress_to_temp(pdf_path, dpi, quality)
|
||||
size_kb = round(tmp.stat().st_size / 1024)
|
||||
temp_files.append(tmp)
|
||||
variants_data.append({"path": str(tmp), "label": label, "size_kb": size_kb})
|
||||
print(f" {label}: {size_kb} kB")
|
||||
|
||||
# 5. Vyber variantu
|
||||
print(" Vyber variantu v okně...")
|
||||
chosen = run_variant_picker(variants_data)
|
||||
|
||||
if not chosen:
|
||||
print(" Žádná varianta nevybrána, přeskakuji.")
|
||||
for t in temp_files:
|
||||
t.unlink(missing_ok=True)
|
||||
return
|
||||
|
||||
# 6. Ulož do Processed
|
||||
PROCESSED.mkdir(exist_ok=True)
|
||||
dest = PROCESSED / final_name
|
||||
if dest.exists():
|
||||
print(f" Přepisuji existující: {dest.name}")
|
||||
shutil.copy2(chosen, dest)
|
||||
set_single_page_view(dest)
|
||||
pdf_path.unlink()
|
||||
print(f" ✓ Uloženo: {dest.name}")
|
||||
|
||||
for t in temp_files:
|
||||
t.unlink(missing_ok=True) # originál mezi temp_files není, je bezpečné
|
||||
|
||||
|
||||
def process_folder(folder: Path):
|
||||
files = sorted(f for f in folder.iterdir() if f.suffix.lower() in (".pdf", ".jpg", ".jpeg", ".png"))
|
||||
if not files:
|
||||
print(f"Žádné soubory v: {folder}")
|
||||
return
|
||||
print(f"Nalezeno {len(files)} soubor(ů).")
|
||||
for f in files:
|
||||
try:
|
||||
process_file(f)
|
||||
except Exception as e:
|
||||
print(f" CHYBA: {e}")
|
||||
print("\nHotovo.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PROCESSED.mkdir(exist_ok=True)
|
||||
TO_PROCESS.mkdir(exist_ok=True)
|
||||
|
||||
target = Path(sys.argv[1]) if len(sys.argv) > 1 else TO_PROCESS
|
||||
|
||||
if target.is_file():
|
||||
process_file(target)
|
||||
elif target.is_dir():
|
||||
process_folder(target)
|
||||
else:
|
||||
print("Použití: python extract_patient_info_novy.py [soubor.pdf nebo složka]")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Konverze JPG/PNG → PDF se správnou orientací stránky (A4).
|
||||
|
||||
Řeší:
|
||||
- EXIF orientaci (fotky z telefonu/skeneru bývají otočené)
|
||||
- Správné umístění na A4 stránce (na výšku nebo na šířku dle obsahu)
|
||||
- Zachování kvality
|
||||
|
||||
Použití:
|
||||
python jpg_to_pdf.py soubor.jpg
|
||||
python jpg_to_pdf.py soubor.jpg vystup.pdf
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
# A4 rozměry v mm
|
||||
A4_W_MM = 210
|
||||
A4_H_MM = 297
|
||||
MARGIN_MM = 0 # bez okraje, tisk si řeší Acrobat (Fit to Print)
|
||||
|
||||
|
||||
def fix_orientation(img: Image.Image) -> Image.Image:
|
||||
"""Opraví rotaci podle EXIF dat (tag 274)."""
|
||||
return ImageOps.exif_transpose(img)
|
||||
|
||||
|
||||
def image_to_pdf(src: Path, dst: Path, dpi: int = 150, quality: int = 80, rotate_ccw: int = 0):
|
||||
img = Image.open(src)
|
||||
print(f" Originál: {img.size[0]}×{img.size[1]} px, mode={img.mode}, format={img.format}")
|
||||
|
||||
# 1. Oprav EXIF orientaci
|
||||
img = fix_orientation(img)
|
||||
print(f" Po EXIF korekci: {img.size[0]}×{img.size[1]} px")
|
||||
|
||||
# 2. Rotace dle parametru (od Claude nebo ručně)
|
||||
if rotate_ccw and rotate_ccw != 0:
|
||||
img = img.rotate(rotate_ccw, expand=True)
|
||||
print(f" Po rotaci {rotate_ccw}° CCW: {img.size[0]}×{img.size[1]} px")
|
||||
|
||||
# 2. Převeď na RGB (PDF nepodporuje RGBA/P)
|
||||
if img.mode in ("RGBA", "P", "LA"):
|
||||
img = img.convert("RGB")
|
||||
|
||||
# 3. Urči orientaci stránky podle poměru stran obrázku
|
||||
img_w, img_h = img.size
|
||||
if img_w > img_h:
|
||||
# Obrázek na šířku → stránka na šířku (A4 landscape)
|
||||
page_w_mm, page_h_mm = A4_H_MM, A4_W_MM
|
||||
print(f" Orientace stránky: na šířku (landscape)")
|
||||
else:
|
||||
# Obrázek na výšku → stránka na výšku (A4 portrait)
|
||||
page_w_mm, page_h_mm = A4_W_MM, A4_H_MM
|
||||
print(f" Orientace stránky: na výšku (portrait)")
|
||||
|
||||
# 4. Vypočti cílovou velikost s okrajem (mm → px při daném DPI)
|
||||
mm_to_px = dpi / 25.4
|
||||
max_w_px = int((page_w_mm - 2 * MARGIN_MM) * mm_to_px)
|
||||
max_h_px = int((page_h_mm - 2 * MARGIN_MM) * mm_to_px)
|
||||
|
||||
# 5. Škáluj obrázek na stránku (zachovej poměr stran)
|
||||
img.thumbnail((max_w_px, max_h_px), Image.LANCZOS)
|
||||
print(f" Výsledná velikost obrázku: {img.size[0]}×{img.size[1]} px")
|
||||
|
||||
# 6. Vlož obrázek na bílé A4 plátno
|
||||
page_w_px = int(page_w_mm * mm_to_px)
|
||||
page_h_px = int(page_h_mm * mm_to_px)
|
||||
canvas = Image.new("RGB", (page_w_px, page_h_px), "white")
|
||||
|
||||
offset_x = (page_w_px - img.size[0]) // 2
|
||||
offset_y = (page_h_px - img.size[1]) // 2
|
||||
canvas.paste(img, (offset_x, offset_y))
|
||||
|
||||
# 7. Ulož jako PDF
|
||||
canvas.save(dst, "PDF", resolution=dpi, quality=quality)
|
||||
print(f" ✓ Uloženo: {dst.name} ({dst.stat().st_size // 1024} KB)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.platform == "win32":
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("Použití: python jpg_to_pdf.py soubor.jpg [vystup.pdf] [rotace_ccw]")
|
||||
print(" rotace_ccw: 0 / 90 / 180 / 270 (výchozí: 0)")
|
||||
sys.exit(1)
|
||||
|
||||
src = Path(sys.argv[1])
|
||||
if not src.exists():
|
||||
print(f"Soubor nenalezen: {src}")
|
||||
sys.exit(1)
|
||||
|
||||
dst = Path(sys.argv[2]) if len(sys.argv) > 2 else src.with_suffix(".pdf")
|
||||
rotate_ccw = int(sys.argv[3]) if len(sys.argv) > 3 else 0
|
||||
|
||||
print(f"Konvertuji: {src.name} → {dst.name}")
|
||||
image_to_pdf(src, dst, rotate_ccw=rotate_ccw)
|
||||
@@ -0,0 +1,462 @@
|
||||
"""
|
||||
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))
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Test skript — zobrazí počet monitorů, jejich rozlišení a který je primární.
|
||||
"""
|
||||
import sys
|
||||
|
||||
try:
|
||||
from screeninfo import get_monitors
|
||||
monitors = get_monitors()
|
||||
print(f"Počet monitorů: {len(monitors)}\n")
|
||||
for i, m in enumerate(monitors):
|
||||
primary = " ← PRIMÁRNÍ" if getattr(m, "is_primary", False) else ""
|
||||
print(f" Monitor {i+1}: {m.width}x{m.height} | pozice x={m.x}, y={m.y}{primary} | název: {getattr(m, 'name', '?')}")
|
||||
except ImportError:
|
||||
print("Knihovna 'screeninfo' není nainstalována — instaluji...")
|
||||
import subprocess
|
||||
subprocess.check_call([sys.executable, "-m", "pip", "install", "screeninfo", "--break-system-packages", "-q"])
|
||||
from screeninfo import get_monitors
|
||||
monitors = get_monitors()
|
||||
print(f"Počet monitorů: {len(monitors)}\n")
|
||||
for i, m in enumerate(monitors):
|
||||
primary = " ← PRIMÁRNÍ" if getattr(m, "is_primary", False) else ""
|
||||
print(f" Monitor {i+1}: {m.width}x{m.height} | pozice x={m.x}, y={m.y}{primary} | název: {getattr(m, 'name', '?')}")
|
||||
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Standalone PDF/obrázek náhled — spouští se jako subprocess z extract_patient_info.py.
|
||||
Argumenty: preview_viewer.py <soubor> [--delete-on-close]
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(1)
|
||||
|
||||
pdf_path = Path(sys.argv[1])
|
||||
delete_on_close = "--delete-on-close" in sys.argv
|
||||
|
||||
try:
|
||||
from PIL import Image, ImageTk
|
||||
import fitz
|
||||
except ImportError as e:
|
||||
print(f"[preview_viewer] Chybí knihovna: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
suffix = pdf_path.suffix.lower()
|
||||
if suffix in (".jpg", ".jpeg", ".png"):
|
||||
pil_img = Image.open(pdf_path)
|
||||
doc = None
|
||||
else:
|
||||
doc = fitz.open(str(pdf_path))
|
||||
pil_img = None
|
||||
|
||||
root = tk.Tk()
|
||||
root.tk.call("encoding", "system", "utf-8")
|
||||
|
||||
sh = root.winfo_screenheight()
|
||||
|
||||
# Zjisti cílovou šířku z layoutu (pokud existuje), jinak výchozí 700px
|
||||
try:
|
||||
import sys as _sys_tmp
|
||||
_sys_tmp.path.insert(0, str(Path(__file__).parent))
|
||||
from window_layout import get_layout as _get_layout
|
||||
_lw = _get_layout().get("preview_viewer")
|
||||
RENDER_W = (_lw.get("w", 700) - 20) if _lw else 700
|
||||
RENDER_H = (_lw.get("h", sh) - 80) if _lw else (sh - 150)
|
||||
except Exception:
|
||||
RENDER_W = 700
|
||||
RENDER_H = sh - 150
|
||||
|
||||
page_count = len(doc) if doc else 1
|
||||
current = [0]
|
||||
photo_ref = [None]
|
||||
|
||||
def render(n) -> Image.Image:
|
||||
if doc is not None:
|
||||
page = doc[n]
|
||||
zoom = min(RENDER_W / page.rect.width, RENDER_H / page.rect.height)
|
||||
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
return Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
|
||||
else:
|
||||
img = pil_img.copy()
|
||||
img.thumbnail((RENDER_W, RENDER_H), Image.LANCZOS)
|
||||
return img
|
||||
|
||||
def on_close():
|
||||
if doc:
|
||||
try:
|
||||
doc.close()
|
||||
except Exception:
|
||||
pass
|
||||
if delete_on_close:
|
||||
try:
|
||||
pdf_path.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
root.destroy()
|
||||
|
||||
root.title(pdf_path.stem)
|
||||
root.protocol("WM_DELETE_WINDOW", on_close)
|
||||
|
||||
lbl_img = tk.Label(root)
|
||||
lbl_img.pack()
|
||||
|
||||
frame_nav = tk.Frame(root)
|
||||
frame_nav.pack(pady=4)
|
||||
|
||||
lbl_page = tk.Label(frame_nav, font=("Segoe UI", 9))
|
||||
lbl_page.pack(side="left", padx=10)
|
||||
|
||||
def show(n):
|
||||
current[0] = n
|
||||
img = render(n)
|
||||
photo_ref[0] = ImageTk.PhotoImage(img)
|
||||
lbl_img.config(image=photo_ref[0])
|
||||
lbl_page.config(text=f"Strana {n + 1} / {page_count}")
|
||||
btn_prev.config(state="normal" if n > 0 else "disabled")
|
||||
btn_next.config(state="normal" if n < page_count - 1 else "disabled")
|
||||
|
||||
btn_prev = tk.Button(frame_nav, text="◄ Předchozí", command=lambda: show(current[0] - 1))
|
||||
btn_prev.pack(side="left")
|
||||
btn_next = tk.Button(frame_nav, text="Další ►", command=lambda: show(current[0] + 1))
|
||||
btn_next.pack(side="left")
|
||||
|
||||
show(0)
|
||||
root.update_idletasks()
|
||||
try:
|
||||
import sys as _sys
|
||||
_sys.path.insert(0, str(Path(__file__).parent))
|
||||
from window_layout import get_layout, apply_geometry
|
||||
_layout = get_layout()
|
||||
|
||||
def _fallback_prev():
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
x = (sw - w) // 2
|
||||
root.geometry(f"+{x}+0")
|
||||
|
||||
apply_geometry(root, _layout, "preview_viewer", fallback_fn=_fallback_prev)
|
||||
except Exception:
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
x = (sw - w) // 2
|
||||
root.geometry(f"+{x}+0")
|
||||
|
||||
# Zapiš geometrii do souboru pokud byl předán argument --write-geometry=<cesta>
|
||||
import json as _json
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--write-geometry="):
|
||||
geom_path = Path(arg.split("=", 1)[1])
|
||||
root.update_idletasks()
|
||||
_x = root.winfo_x()
|
||||
_y = root.winfo_y()
|
||||
_w = root.winfo_width()
|
||||
_h = root.winfo_height()
|
||||
geom_path.write_text(_json.dumps({"x": _x, "y": _y, "w": _w, "h": _h}), encoding="utf-8")
|
||||
break
|
||||
|
||||
root.lift()
|
||||
root.attributes("-topmost", True)
|
||||
root.after(1500, lambda: root.attributes("-topmost", False))
|
||||
root.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Standalone dialog pro schválení / opravu názvu souboru.
|
||||
Spouští se jako subprocess z extract_patient_info.py.
|
||||
Argumenty: rename_dialog.py <json_soubor>
|
||||
JSON vstup: { "nazev": "...", "info_lines": [...] }
|
||||
JSON výstup: { "value": "..." } nebo { "value": null }
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
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 main():
|
||||
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"))
|
||||
nazev = data.get("nazev") or ""
|
||||
info_lines = data.get("info_lines") or []
|
||||
|
||||
result = {"value": None}
|
||||
|
||||
root = tk.Tk()
|
||||
root.title("Schválení názvu souboru")
|
||||
root.resizable(True, False)
|
||||
root.attributes("-topmost", True)
|
||||
|
||||
pad = {"padx": 12, "pady": 6}
|
||||
|
||||
frame_info = tk.Frame(root, bg="#f0f0f0", bd=1, relief="sunken")
|
||||
frame_info.pack(fill="x", **pad)
|
||||
for line in info_lines:
|
||||
color = "#b00000" if line.startswith("⚠") else "#004080" if line.startswith("✓") else "#333"
|
||||
tk.Label(frame_info, text=line, anchor="w", bg="#f0f0f0",
|
||||
fg=color, font=("Segoe UI", 10)).pack(fill="x", padx=8, pady=1)
|
||||
|
||||
tk.Label(root, text="Název souboru (bez .pdf):", anchor="w",
|
||||
font=("Segoe UI", 9, "bold")).pack(fill="x", padx=12, pady=(10, 2))
|
||||
|
||||
nazev_bez = nazev[:-4] if nazev.endswith(".pdf") else nazev
|
||||
var = tk.StringVar(value=nazev_bez)
|
||||
entry = tk.Entry(root, textvariable=var, font=("Segoe UI", 10), width=135)
|
||||
entry.pack(fill="x", padx=12, pady=(0, 10))
|
||||
entry.icursor(tk.END)
|
||||
entry.focus_set()
|
||||
|
||||
frame_btn = tk.Frame(root)
|
||||
frame_btn.pack(pady=(0, 12))
|
||||
|
||||
def schvalit(event=None):
|
||||
result["value"] = var.get().strip()
|
||||
root.destroy()
|
||||
|
||||
def preskocit(event=None):
|
||||
result["value"] = None
|
||||
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).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).pack(side="left", padx=8)
|
||||
|
||||
root.bind("<Return>", schvalit)
|
||||
root.bind("<Escape>", preskocit)
|
||||
|
||||
root.update_idletasks()
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
h = root.winfo_height()
|
||||
x = (sw - w) // 2
|
||||
|
||||
# Skutečná použitelná výška (bez taskbaru)
|
||||
import ctypes, ctypes.wintypes
|
||||
rect = ctypes.wintypes.RECT()
|
||||
ctypes.windll.user32.SystemParametersInfoW(48, 0, ctypes.byref(rect), 0)
|
||||
work_bottom = rect.bottom # spodní hrana work area
|
||||
|
||||
# Pozice pod preview oknem pokud byl předán argument --below-y=N
|
||||
below_y = None
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--below-y="):
|
||||
below_y = int(arg.split("=", 1)[1])
|
||||
break
|
||||
|
||||
if below_y is not None and below_y + h + 10 <= work_bottom:
|
||||
y = below_y # vejde se pod preview
|
||||
else:
|
||||
y = max(0, work_bottom - h - 10) # přilepit těsně nad taskbar
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
root.lift()
|
||||
root.focus_force()
|
||||
root.after(100, lambda: root.focus_force())
|
||||
root.after(200, lambda: root.attributes("-topmost", True))
|
||||
root.mainloop()
|
||||
|
||||
print(json.dumps({"value": result["value"]}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,221 @@
|
||||
"""
|
||||
Standalone dialog pro schválení / opravu názvu souboru.
|
||||
Spouští se jako subprocess z extract_patient_info_novy_test.py.
|
||||
Argumenty: rename_dialog_test.py <json_soubor>
|
||||
JSON vstup: {
|
||||
"nazev": "...",
|
||||
"info_lines": [...],
|
||||
"duplicity": [...], # seznam názvů existujících souborů pro stejné RC+datum
|
||||
"varianty": [...] # seznam návrhů názvu od Claude (unikátní, seřazené od nejlepší)
|
||||
}
|
||||
JSON výstup: { "value": "..." } nebo { "value": null }
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
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
|
||||
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 main():
|
||||
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"))
|
||||
nazev = data.get("nazev") or ""
|
||||
info_lines = data.get("info_lines") or []
|
||||
duplicity = data.get("duplicity") or []
|
||||
varianty = data.get("varianty") or []
|
||||
|
||||
result = {"value": None}
|
||||
|
||||
root = tk.Tk()
|
||||
root.title("Schválení názvu souboru")
|
||||
root.resizable(True, False)
|
||||
root.attributes("-topmost", True)
|
||||
|
||||
pad = {"padx": 12, "pady": 4}
|
||||
font_ui = ("Segoe UI", 10)
|
||||
font_bold = ("Segoe UI", 9, "bold")
|
||||
font_small = ("Segoe UI", 9)
|
||||
|
||||
# ── 1. Info panel (Medicus status) ────────────────────────────────────────
|
||||
frame_info = tk.Frame(root, bg="#f0f0f0", bd=1, relief="sunken")
|
||||
frame_info.pack(fill="x", **pad)
|
||||
for line in info_lines:
|
||||
color = "#b00000" if line.startswith("⚠") or line.startswith("✗") else \
|
||||
"#2a7a2a" if line.startswith("✓") else "#333"
|
||||
tk.Label(frame_info, text=line, anchor="w", bg="#f0f0f0",
|
||||
fg=color, font=font_ui).pack(fill="x", padx=8, pady=1)
|
||||
|
||||
# ── 2. Listbox 1 — duplicity ──────────────────────────────────────────────
|
||||
if duplicity:
|
||||
tk.Label(root, text="⚠ Již existující dokumenty pro toto datum:",
|
||||
anchor="w", fg="#b00000", font=font_bold).pack(fill="x", padx=12, pady=(8, 2))
|
||||
|
||||
frame_dup = tk.Frame(root)
|
||||
frame_dup.pack(fill="x", padx=12, pady=(0, 4))
|
||||
|
||||
sb_dup = tk.Scrollbar(frame_dup, orient="vertical")
|
||||
lb_duplicity = tk.Listbox(
|
||||
frame_dup,
|
||||
yscrollcommand=sb_dup.set,
|
||||
font=font_small,
|
||||
height=min(len(duplicity), 4),
|
||||
selectmode="single",
|
||||
activestyle="dotbox",
|
||||
bg="#fff8f0",
|
||||
selectbackground="#e0b000",
|
||||
selectforeground="#000",
|
||||
)
|
||||
sb_dup.config(command=lb_duplicity.yview)
|
||||
sb_dup.pack(side="right", fill="y")
|
||||
lb_duplicity.pack(side="left", fill="x", expand=True)
|
||||
|
||||
for d in duplicity:
|
||||
lb_duplicity.insert(tk.END, d)
|
||||
|
||||
# OnClick — připraveno pro budoucí funkcionalitu
|
||||
def on_duplicita_click(event):
|
||||
pass # TODO: budoucí akce při výběru duplicity
|
||||
|
||||
lb_duplicity.bind("<<ListboxSelect>>", on_duplicita_click)
|
||||
|
||||
# ── 3. Listbox 2 — návrhy Claudea ─────────────────────────────────────────
|
||||
nazev_bez = nazev[:-4] if nazev.endswith(".pdf") else nazev
|
||||
var = tk.StringVar(value=nazev_bez)
|
||||
|
||||
if varianty:
|
||||
tk.Label(root, text="Návrhy pojmenování (kliknutím vyberte):",
|
||||
anchor="w", font=font_bold).pack(fill="x", padx=12, pady=(8, 2))
|
||||
|
||||
frame_var = tk.Frame(root)
|
||||
frame_var.pack(fill="x", padx=12, pady=(0, 4))
|
||||
|
||||
sb_var = tk.Scrollbar(frame_var, orient="vertical")
|
||||
lb_varianty = tk.Listbox(
|
||||
frame_var,
|
||||
yscrollcommand=sb_var.set,
|
||||
font=font_small,
|
||||
height=min(len(varianty), 6),
|
||||
selectmode="single",
|
||||
activestyle="dotbox",
|
||||
bg="#f0f8ff",
|
||||
selectbackground="#2a7a2a",
|
||||
selectforeground="#fff",
|
||||
)
|
||||
sb_var.config(command=lb_varianty.yview)
|
||||
sb_var.pack(side="right", fill="y")
|
||||
lb_varianty.pack(side="left", fill="x", expand=True)
|
||||
|
||||
for v in varianty:
|
||||
v_bez = v[:-4] if v.endswith(".pdf") else v
|
||||
lb_varianty.insert(tk.END, v_bez)
|
||||
|
||||
# Klik → přepsat Entry
|
||||
def on_varianta_click(event):
|
||||
sel = lb_varianty.curselection()
|
||||
if sel:
|
||||
var.set(lb_varianty.get(sel[0]))
|
||||
|
||||
lb_varianty.bind("<<ListboxSelect>>", on_varianta_click)
|
||||
|
||||
# ── 4. Entry — definitivní název ──────────────────────────────────────────
|
||||
tk.Label(root, text="Název souboru (bez .pdf):", anchor="w",
|
||||
font=font_bold).pack(fill="x", padx=12, pady=(10, 2))
|
||||
|
||||
entry = tk.Entry(root, textvariable=var, font=font_ui, width=135)
|
||||
entry.pack(fill="x", padx=12, pady=(0, 10))
|
||||
entry.icursor(tk.END)
|
||||
entry.focus_set()
|
||||
|
||||
# ── 5. Tlačítka ───────────────────────────────────────────────────────────
|
||||
frame_btn = tk.Frame(root)
|
||||
frame_btn.pack(pady=(0, 12))
|
||||
|
||||
def schvalit(event=None):
|
||||
result["value"] = var.get().strip()
|
||||
root.destroy()
|
||||
|
||||
def preskocit(event=None):
|
||||
result["value"] = None
|
||||
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).pack(side="left", padx=8)
|
||||
tk.Button(frame_btn, text="✗ Přeskočit (Esc)", command=preskocit,
|
||||
bg="#7a2a2a", fg="white", font=font_ui,
|
||||
padx=16, pady=6).pack(side="left", padx=8)
|
||||
|
||||
root.bind("<Return>", schvalit)
|
||||
root.bind("<Escape>", preskocit)
|
||||
|
||||
# ── Pozicování okna ───────────────────────────────────────────────────────
|
||||
root.update_idletasks()
|
||||
sw = root.winfo_screenwidth()
|
||||
w = root.winfo_width()
|
||||
h = root.winfo_height()
|
||||
x = (sw - w) // 2
|
||||
|
||||
try:
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from window_layout import get_layout, apply_geometry
|
||||
_layout = get_layout()
|
||||
|
||||
def _fallback_dlg():
|
||||
import ctypes, ctypes.wintypes
|
||||
rect = ctypes.wintypes.RECT()
|
||||
ctypes.windll.user32.SystemParametersInfoW(48, 0, ctypes.byref(rect), 0)
|
||||
work_bottom = rect.bottom
|
||||
below_y = None
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--below-y="):
|
||||
below_y = int(arg.split("=", 1)[1])
|
||||
break
|
||||
if below_y is not None and below_y + h + 10 <= work_bottom:
|
||||
y = below_y
|
||||
else:
|
||||
y = max(0, work_bottom - h - 10)
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
apply_geometry(root, _layout, "rename_dialog", fallback_fn=_fallback_dlg)
|
||||
except Exception:
|
||||
import ctypes, ctypes.wintypes
|
||||
rect = ctypes.wintypes.RECT()
|
||||
ctypes.windll.user32.SystemParametersInfoW(48, 0, ctypes.byref(rect), 0)
|
||||
work_bottom = rect.bottom
|
||||
below_y = None
|
||||
for arg in sys.argv:
|
||||
if arg.startswith("--below-y="):
|
||||
below_y = int(arg.split("=", 1)[1])
|
||||
break
|
||||
if below_y is not None and below_y + h + 10 <= work_bottom:
|
||||
y = below_y
|
||||
else:
|
||||
y = max(0, work_bottom - h - 10)
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
root.lift()
|
||||
root.focus_force()
|
||||
root.after(100, lambda: root.focus_force())
|
||||
root.after(200, lambda: root.attributes("-topmost", True))
|
||||
root.mainloop()
|
||||
|
||||
print(json.dumps({"value": result["value"]}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
Testovací skript pro ladění rename_dialog.py — spusť přímo, okno se otevře
|
||||
a můžeš ověřit kódování češtiny (ž, š, č, ř, á, é, í, ó, ú, ů, ď, ť, ň).
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
RENAME_DIALOG = Path(__file__).parent / "rename_dialog.py"
|
||||
|
||||
TEST_NAZEV = "7209160057 2026-04-20 Procházka, David [žádost o předání zdravotních informací] [pro nového PL MUDr. Drahomíra Krivosudskář]"
|
||||
|
||||
TEST_INFO_LINES = [
|
||||
"✓ Medicus: Procházka David | RČ 720916/0057",
|
||||
"⚠ Zkouška češtiny: ž š č ř á é í ó ú ů ď ť ň",
|
||||
"— další řádek s háčky a čárkami: přiřadit, výříznout, Krivosudskář",
|
||||
]
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mktemp(suffix=".json"))
|
||||
tmp.write_text(
|
||||
json.dumps({"nazev": TEST_NAZEV, "info_lines": TEST_INFO_LINES}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(RENAME_DIALOG), str(tmp)],
|
||||
capture_output=True, text=True, encoding="utf-8", env=env,
|
||||
)
|
||||
tmp.unlink(missing_ok=True)
|
||||
|
||||
print("=== stdout ===")
|
||||
print(proc.stdout)
|
||||
if proc.stderr.strip():
|
||||
print("=== stderr ===")
|
||||
print(proc.stderr)
|
||||
|
||||
out = proc.stdout.strip()
|
||||
if out:
|
||||
val = json.loads(out).get("value")
|
||||
print(f"\nVrácená hodnota: {val!r}")
|
||||
else:
|
||||
print("\nDialog zavřen bez potvrzení.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Jedno okno pro výběr kompresní varianty PDF.
|
||||
Nahoře tlačítka 1–N pro přepínání, tlačítko "Tohle beru" pro potvrzení.
|
||||
Argumenty: variant_picker.py <json_soubor>
|
||||
JSON vstup: [{"path": "...", "label": "150 DPI / q80", "size_kb": 139}, ...]
|
||||
JSON výstup (stdout): {"chosen": "cesta/k/souboru"}
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import tkinter as tk
|
||||
from PIL import Image, ImageTk
|
||||
import fitz
|
||||
|
||||
|
||||
def show(variants: list) -> str | None:
|
||||
"""Zobrazí picker přímo (bez subprocesů). Vrátí cestu k vybrané variantě nebo None."""
|
||||
chosen = {"path": None}
|
||||
docs = [fitz.open(v["path"]) for v in variants]
|
||||
current = [0]
|
||||
photo_ref = [None]
|
||||
|
||||
root = tk.Tk()
|
||||
root.tk.call("encoding", "system", "utf-8")
|
||||
root.attributes("-topmost", True)
|
||||
|
||||
sh = root.winfo_screenheight()
|
||||
sw = root.winfo_screenwidth()
|
||||
win_h = sh - 80 # odečteme taskbar + title bar
|
||||
img_h = win_h - 160
|
||||
img_w = sw // 2 # šířka okna = polovina monitoru
|
||||
|
||||
x = (sw - img_w) // 2
|
||||
root.geometry(f"{img_w}x{win_h}+{x}+0")
|
||||
root.resizable(False, False)
|
||||
|
||||
# ── Horní panel s tlačítky variant ──
|
||||
frame_top = tk.Frame(root, bg="#222")
|
||||
frame_top.pack(fill="x")
|
||||
|
||||
btn_variants = []
|
||||
current_page = [0]
|
||||
|
||||
def show(n, page_n=0):
|
||||
current[0] = n
|
||||
current_page[0] = page_n
|
||||
doc = docs[n]
|
||||
page = doc[page_n]
|
||||
zoom = min(img_w / page.rect.width, img_h / page.rect.height)
|
||||
pix = page.get_pixmap(matrix=fitz.Matrix(zoom, zoom))
|
||||
img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
|
||||
photo_ref[0] = ImageTk.PhotoImage(img)
|
||||
lbl_img.config(image=photo_ref[0])
|
||||
page_count = len(doc)
|
||||
root.title(f"Varianta {n+1}: {variants[n]['label']} ({variants[n]['size_kb']} kB) — strana {page_n+1}/{page_count}")
|
||||
for i, b in enumerate(btn_variants):
|
||||
b.config(bg="#2a5a9a" if i == n else "#444")
|
||||
btn_prev_page.config(state="normal" if page_n > 0 else "disabled")
|
||||
btn_next_page.config(state="normal" if page_n < page_count - 1 else "disabled")
|
||||
|
||||
for i, v in enumerate(variants):
|
||||
b = tk.Button(
|
||||
frame_top,
|
||||
text=f"{i+1}. {v['label']}\n{v['size_kb']} kB",
|
||||
font=("Segoe UI", 9, "bold"),
|
||||
bg="#444", fg="white",
|
||||
relief="flat", padx=8, pady=6,
|
||||
command=lambda n=i: show(n),
|
||||
)
|
||||
b.pack(side="left", padx=2, pady=4)
|
||||
btn_variants.append(b)
|
||||
|
||||
# ── Tlačítka Beru / Přeskočit — stejný styl jako varianty ──
|
||||
def beru():
|
||||
chosen["path"] = variants[current[0]]["path"]
|
||||
root.destroy()
|
||||
|
||||
def preskocit():
|
||||
root.destroy()
|
||||
|
||||
tk.Button(
|
||||
frame_top,
|
||||
text="✓ Tohle beru\n",
|
||||
command=beru,
|
||||
bg="#2a7a2a", fg="white",
|
||||
font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6,
|
||||
).pack(side="left", padx=2, pady=4)
|
||||
|
||||
tk.Button(
|
||||
frame_top,
|
||||
text="✗ Přeskočit\n",
|
||||
command=preskocit,
|
||||
bg="#7a2a2a", fg="white",
|
||||
font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6,
|
||||
).pack(side="left", padx=2, pady=4)
|
||||
|
||||
# ── Navigace stran — úplně vpravo ──
|
||||
btn_next_page = tk.Button(
|
||||
frame_top,
|
||||
text="Další ►\n",
|
||||
command=lambda: show(current[0], current_page[0] + 1),
|
||||
bg="#555", fg="white",
|
||||
font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6,
|
||||
)
|
||||
btn_next_page.pack(side="right", padx=2, pady=4)
|
||||
|
||||
btn_prev_page = tk.Button(
|
||||
frame_top,
|
||||
text="◄ Před.\n",
|
||||
command=lambda: show(current[0], current_page[0] - 1),
|
||||
bg="#555", fg="white",
|
||||
font=("Segoe UI", 9, "bold"),
|
||||
relief="flat", padx=8, pady=6,
|
||||
)
|
||||
btn_prev_page.pack(side="right", padx=2, pady=4)
|
||||
|
||||
# ── Obrázek ──
|
||||
lbl_img = tk.Label(root, bg="black")
|
||||
lbl_img.pack(fill="both", expand=True)
|
||||
|
||||
root.bind("<Key-1>", lambda e: show(0))
|
||||
root.bind("<Key-2>", lambda e: show(1))
|
||||
root.bind("<Key-3>", lambda e: show(2))
|
||||
root.bind("<Key-4>", lambda e: show(3))
|
||||
root.bind("<Key-5>", lambda e: show(4))
|
||||
root.bind("<Return>", lambda e: beru())
|
||||
root.bind("<Escape>", lambda e: preskocit())
|
||||
|
||||
show(0)
|
||||
root.lift()
|
||||
root.focus_force()
|
||||
root.after(100, lambda: root.focus_force())
|
||||
root.after(200, lambda: root.attributes("-topmost", True))
|
||||
root.mainloop()
|
||||
|
||||
for d in docs:
|
||||
try:
|
||||
d.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return chosen["path"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(1)
|
||||
variants = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
result = show(variants)
|
||||
print(json.dumps({"chosen": result}, ensure_ascii=False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
Pomocný modul pro pozicování oken podle hostname počítače.
|
||||
Nastavení se načítá z layout_settings.json ve stejném adresáři.
|
||||
|
||||
Použití:
|
||||
from window_layout import get_layout
|
||||
layout = get_layout()
|
||||
geom = layout["preview_viewer"] # {"x": 1100, "y": 0, "w": 1400, "h": 2100}
|
||||
root.geometry(f"{geom['w']}x{geom['h']}+{geom['x']}+{geom['y']}")
|
||||
|
||||
Pro okna s "anchor": "bottom" (rename_dialog) použij get_bottom_y():
|
||||
y = layout["rename_dialog"]["x"] # jen x, y se počítá dynamicky
|
||||
"""
|
||||
import json
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
_SETTINGS_FILE = Path(__file__).parent / "layout_settings.json"
|
||||
|
||||
# Výchozí fallback layout (jeden monitor, původní chování)
|
||||
_DEFAULT = {
|
||||
"duplicity_viewer": None, # None = nepoužívat pevnou geometrii
|
||||
"preview_viewer": None,
|
||||
"rename_dialog": None,
|
||||
}
|
||||
|
||||
|
||||
def get_hostname() -> str:
|
||||
return socket.gethostname().upper()
|
||||
|
||||
|
||||
def get_layout() -> dict:
|
||||
"""
|
||||
Vrátí slovník s geometrií oken pro aktuální počítač.
|
||||
Pokud hostname není v settings, vrátí fallback (None = původní chování).
|
||||
"""
|
||||
hostname = get_hostname()
|
||||
if not _SETTINGS_FILE.exists():
|
||||
return dict(_DEFAULT)
|
||||
try:
|
||||
settings = json.loads(_SETTINGS_FILE.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return dict(_DEFAULT)
|
||||
return settings.get(hostname, dict(_DEFAULT))
|
||||
|
||||
|
||||
def apply_geometry(root, layout: dict, window: str, fallback_fn=None):
|
||||
"""
|
||||
Aplikuje geometrii okna z layoutu.
|
||||
- root: tkinter Tk nebo Toplevel
|
||||
- layout: výsledek get_layout()
|
||||
- window: klíč ("duplicity_viewer", "preview_viewer", "rename_dialog")
|
||||
- fallback_fn: callable() volaný pokud pro toto okno není layout definován
|
||||
"""
|
||||
geom = layout.get(window)
|
||||
if not geom:
|
||||
if fallback_fn:
|
||||
fallback_fn()
|
||||
return
|
||||
|
||||
w = geom.get("w")
|
||||
h = geom.get("h")
|
||||
x = geom.get("x", 0)
|
||||
y = geom.get("y", 0)
|
||||
|
||||
if geom.get("anchor") == "bottom":
|
||||
# Výška se ignoruje, okno se přilepí ke spodnímu okraji work area
|
||||
import ctypes, ctypes.wintypes
|
||||
rect = ctypes.wintypes.RECT()
|
||||
ctypes.windll.user32.SystemParametersInfoW(48, 0, ctypes.byref(rect), 0)
|
||||
root.update_idletasks()
|
||||
h_actual = root.winfo_height()
|
||||
y = rect.bottom - h_actual - 10
|
||||
if w:
|
||||
root.geometry(f"+{x}+{y}")
|
||||
else:
|
||||
root.geometry(f"+{x}+{y}")
|
||||
else:
|
||||
if w and h:
|
||||
root.geometry(f"{w}x{h}+{x}+{y}")
|
||||
elif w:
|
||||
root.update_idletasks()
|
||||
root.geometry(f"{w}x{root.winfo_height()}+{x}+{y}")
|
||||
else:
|
||||
root.update_idletasks()
|
||||
root.geometry(f"+{x}+{y}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
hostname = get_hostname()
|
||||
layout = get_layout()
|
||||
print(f"Hostname: {hostname}")
|
||||
if all(v is None for v in layout.values()):
|
||||
print("Žádné nastavení pro tento počítač — používá se výchozí chování.")
|
||||
print(f"Přidej klíč '{hostname}' do {_SETTINGS_FILE}")
|
||||
else:
|
||||
print(f"Layout pro '{hostname}':")
|
||||
for k, v in layout.items():
|
||||
print(f" {k}: {v}")
|
||||
Reference in New Issue
Block a user