60 ScansProcessing: PDF náhled, fuzzy RČ, Medicus podle počítače

- Nová knihovna Knihovny/najdi_medicus.py — připojení k Medicus podle hostname
  (LEKAR=localhost/M:, SESTRA+LENOVO=192.168.1.10/M:, ostatní=localhost/C:)
- Fuzzy matching RČ rozšířen o vkládání nuly (oprava sekvencí 00→0)
- PDF náhled jako samostatný subprocess (preview_viewer.py, pymupdf)
  otevře se před OCR, zůstane do přejmenování, pracuje s temp kopií
- Prompt: poznámka čerpá přednostně ze sekce Závěr: zprávy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
michaela.buzalkova
2026-04-22 09:08:34 +02:00
parent 3aca4f5772
commit a1cde8b471
3 changed files with 328 additions and 74 deletions
+60
View File
@@ -226,5 +226,65 @@
{ {
"original": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, LRPéče indikace II/3 hypertenzní choroba II-III.st].pdf", "original": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, LRPéče indikace II/3 hypertenzní choroba II-III.st].pdf",
"corrected": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, návrh lázně, indikace II3 hypertenzní choroba II-III.st].pdf" "corrected": "470629074 2026-04-21 Šebesta, Jaroslav [oznámení ZP správní řízení] [zahájení správního řízení, návrh lázně, indikace II3 hypertenzní choroba II-III.st].pdf"
},
{
"original": "5503040026 2026-02-17 Koubek, Jiří [LZ kardiologie] [ECHO: EF 65%, konc.hypertrofie, diastol.dysfunkce I.st, Bevimlar 20mg].pdf",
"corrected": "5503040026 2026-02-17 Koubek, Jiří [LZ kardiologie] [ECHO EF 65%, konc.hypertrofie, diastol.dysfunkce I.st, Bevimlar 20mg].pdf"
},
{
"original": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [osteomarkery, Ca, P, ALP, vit.D 67,1 snížen, PTH, Beta-CrossLaps].pdf",
"corrected": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [osteomarkery, Ca, P, ALP, vit.D 67.1 snížen, PTH, Beta-CrossLaps].pdf"
},
{
"original": "435520110 2026-04-20 Nechodomová, Marie [sonografie břicha] [hypersekr.žaludku, lipomatoza pankreatu, steatoza jat., cholecystolithiaza, splenomegalie].pdf",
"corrected": "435520110 2026-04-20 Nechodomová, Marie [sonografie břicha] [zesílení stěny žaludku - dovyšetřit, hypersekr.žaludku, lipomatoza pankreatu, steatoza jat., cholecystolithiaza, splenomegalie].pdf"
},
{
"original": "6903020080 2026-04-20 Novotný, Martin [Laboratoř] [cholesterol 5.54, LDL 3.25, TG 2.06, glukoza 6.1, HbA1c 38].pdf",
"corrected": "6903020080 2026-04-20 Novotný, Martin [Laboratoř] [smíšená hyperlipidémie, prediabetes, cholesterol 5.54, LDL 3.25, TG 2.06, glukoza 6.1, HbA1c 38].pdf"
},
{
"original": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [ELFO bílkovin, bílkovina 69.0, albumin 0.581, gama-globuliny 0.125].pdf",
"corrected": "480529219 2026-04-17 Nytra, Vlastimil [Laboratoř] [ELFO bílkovin OK, bílkovina 69.0, albumin 0.581, gama-globuliny 0.125].pdf"
},
{
"original": "5556046672 2026-04-07 Simionová, Lýdia [Laboratoř] [močový konkrement, whewellit 100%, 6x3mm, hnědý, bradavičnatý].pdf",
"corrected": "5556046672 2026-04-07 Simionová, Lýdia [Laboratoř] [močový konkrement analýza, whewellit 100%, 6x3mm, hnědý, bradavičnatý].pdf"
},
{
"original": "510802325 2026-04-20 Simion, Vladimír [LZ chirurgie] [chronický vřed kůže, TMT amputace IV.+V.prstu PDK, defekt LDK 5x3.5cm].pdf",
"corrected": "510802325 2026-04-20 Simion, Vladimír [LZ chirurgie] [chronický vřed kůže, TMT amputace IV.+V.prstu PDK, defekt LDK 5x3.5cm, DP 3xt].pdf"
},
{
"original": "436114002 2026-03-17 Petrovská, Eliška [LZ kardiologie] [fibrilace síní paroxysmální, sinus, st.p.kardioverzi, rivaroxaban].pdf",
"corrected": "436114002 2026-03-17 Petrovská, Eliška [LZ kardiologie] [fibrilace síní paroxysmální, sinus, st.p.kardioverzi, rivaroxaban, ad Holter EKG, bisoprolol vysadí].pdf"
},
{
"original": "436114002 2026-03-14 Petrovská, Eliška [LZ interna] [fibrilace síní paroxysmální, kardioverze, sinusový rytmus, rivaroxaban].pdf",
"corrected": "436114002 2026-03-14 Petrovská, Eliška [LZ interna urgent] [fibrilace síní paroxysmální, kardioverze, sinusový rytmus, rivaroxaban].pdf"
},
{
"original": "6008091738 2026-04-20 Nikitin, Petro [Laboratoř] [urea 9.47 zvýš, CKD-EPI G2, glukoza 6.6, osmolalita 296, MCV 81.5].pdf",
"corrected": "6008091738 2026-04-20 Nikitin, Petro [Laboratoř] [Z000 prediabetes, mikrocyty, urea 9.47 zvýš, CKD-EPI G2, glukoza 6.6, osmolalita 296, MCV 81.5].pdf"
},
{
"original": "440802018 2026-04-20 Havelka, Miroslav [Laboratoř] [CKD-EPI G2, NT-proBNP 6128 zvýš, CRP 6.6, MCV 81.8, MCHC 314].pdf",
"corrected": "440802018 2026-04-20 Havelka, Miroslav [Laboratoř] [srdeční selhání, mikrocyty, CKD-EPI G2, NT-proBNP 6128 zvýš, CRP 6.6, MCV 81.8, MCHC 314].pdf"
},
{
"original": "7857260422 2023-02-28 Jindrová, Kateřina [LZ ORL] [st.p. incizi inflam aterom P tváře - zhojeno, extirpace atheromu P tváře].pdf",
"corrected": "7857260422 2023-02-28 Jindrová, Kateřina [LZ ORL] [st.p. incizi inflam aterom P tváře - zhojeno, extirpace atheromu P tváře domluveno].pdf"
},
{
"original": "7857260422 2021-05-06 Jindrová, Kateřina [LZ angiologie] [CVI II. st. dle CEAP C4, ortostáza, flebitida/flebotrombóza bilat. neprokázána].pdf",
"corrected": "7857260422 2021-05-06 Jindrová, Kateřina [LZ angiologie] [CVI II. st. dle CEAP C4, ortostáza, flebitidaflebotrombóza bilat. neprokázána].pdf"
},
{
"original": "7857260422 2021-05-20 Jindrová, Kateřina [LZ neurologie] [VAS C-pá, porucha statodynamiky C úseku, tinitus auric. bilat., ad rehab].pdf",
"corrected": "7857260422 2021-05-20 Jindrová, Kateřina [LZ neurologie] [VAS Cp, porucha statodynamiky C úseku, tinitus auric. bilat., ad RHB].pdf"
},
{
"original": "7857260422 2024-02-12 Jindrová, Kateřina [EKG] [sinusový rytmus 70/min, semivertik poloha, osa 55st, fyziol záznam].pdf",
"corrected": "7857260422 2024-02-12 Jindrová, Kateřina [EKG] [sinusový rytmus 70min, semivertik poloha, osa 55st, fyziol záznam].pdf"
} }
] ]
+170 -74
View File
@@ -13,6 +13,7 @@ import json
import os import os
import re import re
import shutil import shutil
import subprocess
import sys import sys
import time import time
from pathlib import Path from pathlib import Path
@@ -121,18 +122,23 @@ def _lookup_by_rc(cur, rc_digits: str) -> dict | None:
def _rc_candidates(rc: str) -> list[str]: def _rc_candidates(rc: str) -> list[str]:
""" """
Generuje kandidáty RČ pro fuzzy matching (edit distance 1): Generuje kandidáty RČ pro fuzzy matching:
- vynechání každé cifry (oprava extra znaku z OCR) - vynechání každé cifry (OCR přečetlo znak navíc)
- vložení nuly na každou pozici (OCR přehlédlo nulu v sekvenci 00)
- záměna podobně vypadajících číslic na každé pozici - záměna podobně vypadajících číslic na každé pozici
Vrátí unikátní seznam kandidátů bez původního RČ. Vrátí unikátní seznam kandidátů bez původního RČ.
""" """
similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"} similar = {"0": "8", "8": "0", "1": "7", "7": "1", "5": "6", "6": "5", "3": "8"}
candidates = set() candidates = set()
# Vynechání jedné cifry (nejčastější OCR chyba — přebývající nula) # Vynechání jedné cifry (OCR přečetlo znak navíc)
for i in range(len(rc)): for i in range(len(rc)):
candidates.add(rc[:i] + rc[i+1:]) candidates.add(rc[:i] + rc[i+1:])
# Vložení nuly na každou pozici (nejčastější chyba: sekvence 00 přečtena jako 0)
for i in range(len(rc) + 1):
candidates.add(rc[:i] + "0" + rc[i:])
# Záměna podobné cifry na každé pozici # Záměna podobné cifry na každé pozici
for i, ch in enumerate(rc): for i, ch in enumerate(rc):
if ch in similar: if ch in similar:
@@ -235,7 +241,10 @@ def extract_patient_info(pdf_path: str) -> dict:
"\"PZ {oddělení}\" = propouštěcí zpráva z hospitalizace (např. \"PZ interna\", \"PZ neurologie\"). " "\"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\", " "Jiné typy: \"Laboratoř\", \"CT břicha\", \"MRI páteře\", \"kolonoskopie\", "
"\"operační protokol oční\", \"poukaz FT\", \"diagnostická mamografie\" atd.\n" "\"operační protokol oční\", \"poukaz FT\", \"diagnostická mamografie\" atd.\n"
"- \"poznamka\": krátká klinická poznámka česky, max 80 znaků\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"
"- \"nazev_souboru\": název souboru ve formátu " "- \"nazev_souboru\": název souboru ve formátu "
"\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" " "\"{rodne_cislo} {datum_zpravy} {Příjmení}, {Jméno} [{typ_dokumentu}] [{poznamka}].pdf\" "
"(jméno bez titulu, RČ bez lomítka)\n" "(jméno bez titulu, RČ bez lomítka)\n"
@@ -281,6 +290,95 @@ def sanitize_filename(name: str) -> str:
return re.sub(r'[<>:"/\\|?*]', '', name) return re.sub(r'[<>:"/\\|?*]', '', name)
def _open_preview(root, pdf_path: Path):
"""Otevře náhledové okno PDF/obrázku jako Toplevel. Pracuje s temp kopií — žádné zamykání originálu."""
import tkinter as tk
import tempfile
import shutil as _shutil
try:
from PIL import Image, ImageTk
import fitz
except ImportError:
return
# Temp kopie — prohlížeč nikdy nesahá na originál
tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix))
_shutil.copy2(pdf_path, tmp)
suffix = pdf_path.suffix.lower()
if suffix in (".jpg", ".jpeg", ".png"):
pil_pages = [Image.open(tmp)]
doc = None
else:
try:
doc = fitz.open(str(tmp))
except Exception:
tmp.unlink(missing_ok=True)
return
pil_pages = []
def render(n) -> Image.Image:
if doc is not None:
page = doc[n]
zoom = min(700 / page.rect.width, (sh - 150) / 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_pages[0].copy()
img.thumbnail((700, sh - 150), Image.LANCZOS)
return img
def on_close():
try:
if doc:
doc.close()
except Exception:
pass
tmp.unlink(missing_ok=True)
win.destroy()
page_count = len(doc) if doc else 1
sh = root.winfo_screenheight()
current = [0]
photo_ref = [None]
win = tk.Toplevel(root)
win.title(pdf_path.name)
win.attributes("-topmost", True)
win.resizable(False, False)
win.protocol("WM_DELETE_WINDOW", on_close)
lbl_img = tk.Label(win)
lbl_img.pack()
frame_nav = tk.Frame(win)
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)
win.update_idletasks()
win.geometry(f"+0+0")
def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None: def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None:
""" """
Tkinter dialog pro schválení / opravu názvu souboru. Tkinter dialog pro schválení / opravu názvu souboru.
@@ -291,15 +389,18 @@ def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None:
result = {"value": None} result = {"value": None}
root = tk.Tk() root = tk.Tk()
root.withdraw()
root.tk.call("encoding", "system", "utf-8") root.tk.call("encoding", "system", "utf-8")
root.title("Schválení názvu souboru")
root.resizable(True, False) dlg = tk.Toplevel(root)
root.attributes("-topmost", True) dlg.title("Schválení názvu souboru")
dlg.resizable(True, False)
dlg.attributes("-topmost", True)
pad = {"padx": 12, "pady": 6} pad = {"padx": 12, "pady": 6}
# Informační sekce # Informační sekce
frame_info = tk.Frame(root, bg="#f0f0f0", bd=1, relief="sunken") frame_info = tk.Frame(dlg, bg="#f0f0f0", bd=1, relief="sunken")
frame_info.pack(fill="x", **pad) frame_info.pack(fill="x", **pad)
for line in info_lines: for line in info_lines:
color = "#b00000" if line.startswith("") else "#004080" if line.startswith("") else "#333" color = "#b00000" if line.startswith("") else "#004080" if line.startswith("") else "#333"
@@ -307,18 +408,18 @@ def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None:
fg=color, font=("Segoe UI", 10)).pack(fill="x", padx=8, pady=1) fg=color, font=("Segoe UI", 10)).pack(fill="x", padx=8, pady=1)
# Pole pro název (bez .pdf) # Pole pro název (bez .pdf)
tk.Label(root, text="Název souboru (bez .pdf):", anchor="w", tk.Label(dlg, text="Název souboru (bez .pdf):", anchor="w",
font=("Segoe UI", 9, "bold")).pack(fill="x", padx=12, pady=(10, 2)) font=("Segoe UI", 9, "bold")).pack(fill="x", padx=12, pady=(10, 2))
nazev_bez = nazev[:-4] if nazev and nazev.endswith(".pdf") else (nazev or "") nazev_bez = nazev[:-4] if nazev and nazev.endswith(".pdf") else (nazev or "")
var = tk.StringVar(value=nazev_bez) var = tk.StringVar(value=nazev_bez)
entry = tk.Entry(root, textvariable=var, font=("Segoe UI", 10), width=90) entry = tk.Entry(dlg, textvariable=var, font=("Segoe UI", 10), width=90)
entry.pack(fill="x", padx=12, pady=(0, 10)) entry.pack(fill="x", padx=12, pady=(0, 10))
entry.icursor(tk.END) entry.icursor(tk.END)
entry.focus_set() entry.focus_set()
# Tlačítka # Tlačítka
frame_btn = tk.Frame(root) frame_btn = tk.Frame(dlg)
frame_btn.pack(pady=(0, 12)) frame_btn.pack(pady=(0, 12))
def schvalit(event=None): def schvalit(event=None):
@@ -336,14 +437,18 @@ def _rename_dialog(nazev: str, info_lines: list[str]) -> str | None:
bg="#7a2a2a", fg="white", font=("Segoe UI", 10), bg="#7a2a2a", fg="white", font=("Segoe UI", 10),
padx=16, pady=6).pack(side="left", padx=8) padx=16, pady=6).pack(side="left", padx=8)
root.bind("<Return>", schvalit) dlg.bind("<Return>", schvalit)
root.bind("<Escape>", preskocit) dlg.bind("<Escape>", preskocit)
# Vystředit okno na obrazovce # Umísti dialog vpravo od náhledu (nebo vystředit pokud náhled není)
root.update_idletasks() dlg.update_idletasks()
w, h = root.winfo_width(), root.winfo_height() sw = dlg.winfo_screenwidth()
sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() sh = dlg.winfo_screenheight()
root.geometry(f"+{(sw - w) // 2}+{(sh - h) // 2}") w = dlg.winfo_width()
h = dlg.winfo_height()
x = min(720, sw - w - 20)
y = (sh - h) // 2
dlg.geometry(f"+{x}+{y}")
root.mainloop() root.mainloop()
return result["value"] return result["value"]
@@ -408,43 +513,8 @@ def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool:
print(f" Navržený název: {nazev}") print(f" Navržený název: {nazev}")
print(" Otevírám dialog...") print(" Otevírám dialog...")
# Otevři PDF v prohlížeči (konkrétní proces, abychom zavřeli jen toto okno)
viewer_proc = None
try:
import subprocess
import winreg
import shlex
def _get_pdf_exe() -> list[str] | None:
try:
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r".pdf") as k:
prog_id = winreg.QueryValue(k, "")
with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, rf"{prog_id}\shell\open\command") as k:
cmd = winreg.QueryValue(k, "")
parts = shlex.split(cmd.replace("\\", "/"))
exe = parts[0].replace("/", "\\")
return [exe, str(pdf_path)]
except Exception:
return None
cmd = _get_pdf_exe()
if cmd:
viewer_proc = subprocess.Popen(cmd)
else:
subprocess.Popen(["start", "", str(pdf_path)], shell=True)
except Exception as e:
print(f" [Prohlížeč] Nepodařilo se otevřít: {e}")
odpoved = _rename_dialog(nazev or "", info_lines) odpoved = _rename_dialog(nazev or "", info_lines)
# Zavři jen tento konkrétní proces prohlížeče a počkej na uvolnění zámku
if viewer_proc is not None:
try:
viewer_proc.terminate()
viewer_proc.wait(timeout=10)
except Exception:
pass
if odpoved is None: if odpoved is None:
print(" Přeskočeno.") print(" Přeskočeno.")
return False return False
@@ -465,19 +535,11 @@ def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool:
print(f" VAROVÁNÍ: '{final_name}' již existuje v Processed, přeskakuji.") print(f" VAROVÁNÍ: '{final_name}' již existuje v Processed, přeskakuji.")
return False return False
for attempt in range(5): if pdf_path.suffix.lower() in (".jpg", ".jpeg", ".png"):
try: from jpg_to_pdf import image_to_pdf
if pdf_path.suffix.lower() in (".jpg", ".jpeg", ".png"): image_to_pdf(pdf_path, dest, rotate_ccw=info.get("rotace") or 0)
from jpg_to_pdf import image_to_pdf else:
image_to_pdf(pdf_path, dest, rotate_ccw=info.get("rotace") or 0) shutil.copy2(pdf_path, dest)
else:
shutil.copy2(pdf_path, dest)
break
except PermissionError:
if attempt < 4:
time.sleep(0.5)
else:
raise
pdf_path.unlink() pdf_path.unlink()
print(f" ✓ Uloženo: Processed/{final_name}") print(f" ✓ Uloženo: Processed/{final_name}")
@@ -486,15 +548,49 @@ def interactive_rename(pdf_path: Path, info: dict, verif: dict) -> bool:
# ─── Hlavní logika ──────────────────────────────────────────────────────────── # ─── Hlavní logika ────────────────────────────────────────────────────────────
def _start_preview_process(pdf_path: Path):
"""
Otevře náhled PDF jako samostatný subprocess (žádné tkinter threading problémy).
Pracuje s temp kopií — originál zůstane volný.
Vrátí funkci close() pro ukončení procesu.
"""
import tempfile
import shutil as _shutil
tmp = Path(tempfile.mktemp(suffix=pdf_path.suffix))
_shutil.copy2(pdf_path, tmp)
viewer = Path(__file__).parent / "preview_viewer.py"
proc = subprocess.Popen(
[sys.executable, str(viewer), str(tmp), "--delete-on-close"],
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, "CREATE_NO_WINDOW") else 0,
)
def close():
try:
proc.terminate()
proc.wait(timeout=3)
except Exception:
pass
try:
tmp.unlink(missing_ok=True)
except Exception:
pass
return close
def process_file(pdf_path: Path): def process_file(pdf_path: Path):
info = extract_patient_info(str(pdf_path)) close_preview = _start_preview_process(pdf_path)
try:
rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "") info = extract_patient_info(str(pdf_path))
print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...") rc_from_scan = re.sub(r"\D", "", info.get("rodne_cislo") or "")
verif = verify_patient(rc_from_scan) print(f" Ověřuji v Medicus (RČ: {rc_from_scan})...")
print_verification(verif, rc_from_scan) verif = verify_patient(rc_from_scan)
print_verification(verif, rc_from_scan)
interactive_rename(pdf_path, info, verif) interactive_rename(pdf_path, info, verif)
finally:
close_preview()
def process_folder(folder: Path): def process_folder(folder: Path):
pdf_files = sorted(f for f in folder.iterdir() pdf_files = sorted(f for f in folder.iterdir()
+98
View File
@@ -0,0 +1,98 @@
"""
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:
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()
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(700 / page.rect.width, (sh - 150) / 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((700, sh - 150), 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.attributes("-topmost", True)
root.resizable(False, False)
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()
root.geometry("+0+0")
root.mainloop()
if __name__ == "__main__":
main()