From 6498c4e444ee536db1da463a4939fe5ba8fbe760 Mon Sep 17 00:00:00 2001 From: "vladimir.buzalka" Date: Mon, 18 May 2026 11:03:53 +0200 Subject: [PATCH] z230 --- RozkladScanu/NOTES.md | 69 ++++++++++ RozkladScanu/jak_skenovat.md | 9 ++ RozkladScanu/navrh_pojmenovani.py | 205 ++++++++++++++++++++++++++++++ RozkladScanu/rozloz_brozuru.py | 76 +++++++++++ 4 files changed, 359 insertions(+) create mode 100644 RozkladScanu/NOTES.md create mode 100644 RozkladScanu/jak_skenovat.md create mode 100644 RozkladScanu/navrh_pojmenovani.py create mode 100644 RozkladScanu/rozloz_brozuru.py diff --git a/RozkladScanu/NOTES.md b/RozkladScanu/NOTES.md new file mode 100644 index 0000000..5591338 --- /dev/null +++ b/RozkladScanu/NOTES.md @@ -0,0 +1,69 @@ +# RozkladScanu — poznámky + +## Skript: `rozloz_brozuru.py` + +Univerzální skript pro oba druhy skenování níže. Vše se odvodí automaticky z PDF. + +``` +python rozloz_brozuru.py vstup/ vystup/ +``` + +Pokud není zadán výstup, pojmenuje se `_serazena.pdf` ve stejném adresáři jako vstup. + +--- + +## Druh skenování: KONICA_DUPLEX_OD_STREDU + +**Použití:** brožura (sešit) se sedlovým šitím, rozešitá a skenovaná oboustranně od prostřední stránky směrem ven na Konice. + +**Podmínky:** +- Každý sken = dvojstránka A3 landscape (levá + pravá A4 vedle sebe) +- Skenování začíná od středového listu a postupuje ven +- Každý list: nejdřív přední strana, pak zadní + +**Příklad — brožura 24 stran (střed 12+13), 12 PDF stránek:** + +| PDF str. | Levá | Pravá | +|----------|:----:|:-----:| +| 1 | 12 | 13 | +| 2 | 14 | 11 | +| 3 | 10 | 15 | +| ... | ... | ... | +| 12 | 24 | 1 | + +Výstup: 24 stránek A4, seřazeny 1–24. + +--- + +## Druh skenování: RICOH_DUPLEX_OD_STREDU + +**Použití:** brožura (sešit) se sedlovým šitím, rozešitá a skenovaná oboustranně od prostřední stránky směrem ven na RICOHu. + +**Podmínky:** +- Každý sken = A4 landscape (levá + pravá A5 vedle sebe) +- Skenování začíná od středového listu a postupuje ven +- Každý list: nejdřív přední strana, pak zadní + +**Příklad — brožura 80 stran (střed 40+41), 40 PDF stránek:** + +| PDF str. | Levá | Pravá | +|----------|:----:|:-----:| +| 1 | 40 | 41 | +| 2 | 42 | 39 | +| 3 | 38 | 43 | +| ... | ... | ... | +| 40 | 80 | 1 | + +Výstup: 80 stránek A5, seřazeny 1–80. + +--- + +## Algoritmus (společný pro oba druhy) + +Střed se odvodí automaticky: `CENTER_LEFT = počet PDF stránek`, `CENTER_RIGHT = počet PDF stránek + 1`. + +Pro list č. `k` (0-based od středu): +- přední strana (PDF `2k+1`): levá = `CENTER_LEFT - 2k`, pravá = `CENTER_RIGHT + 2k` +- zadní strana (PDF `2k+2`): levá = `CENTER_RIGHT + 2k + 1`, pravá = `CENTER_LEFT - 2k - 1` + +Každá PDF stránka se ořízne na levou a pravou polovinu (vždy stejný formát — poloviční šířka). diff --git a/RozkladScanu/jak_skenovat.md b/RozkladScanu/jak_skenovat.md new file mode 100644 index 0000000..9ecf1f1 --- /dev/null +++ b/RozkladScanu/jak_skenovat.md @@ -0,0 +1,9 @@ +# Jak skenovat brožuru + +## KONICA + +Rozlož brožuru středem nahoru a textem k sobě. + +## RICOH + +Rozlož brožuru a dej do skeneru tak, aby zadní strana byla střed brožury a dolní okraj textu na střední stránce aby byl vlevo. diff --git a/RozkladScanu/navrh_pojmenovani.py b/RozkladScanu/navrh_pojmenovani.py new file mode 100644 index 0000000..09752c9 --- /dev/null +++ b/RozkladScanu/navrh_pojmenovani.py @@ -0,0 +1,205 @@ +""" +navrh_pojmenovani.py — Claude navrhne jmeno souboru pro naskenovanou brozuru. + +Vezme prvnich N stranek PDF, posle je Claudovi jako obrazky a navrhne nazev. +Dialog umozni opravit navrh. Historie dvojic (navrh Claudea, volba uzivatele) +se ulozi a priste prikladem uci Claudea pojmenovavat podle tvych preferencí. + +Pouziti: + python navrh_pojmenovani.py vystup/brozura.pdf + python navrh_pojmenovani.py vystup/brozura.pdf vystup/muj_nazev.pdf +""" + +import sys +import json +import base64 +import io +import copy +import tkinter as tk +from pathlib import Path +from datetime import date +import fitz # pymupdf +import anthropic + +HISTORY_FILE = Path(__file__).parent / "pojmenovani_historie.json" +N_PAGES = 5 +DPI = 120 # dostatecne pro cteni obsahu, nizke naklady na tokeny + + +# ---------- pomocne funkce ---------- + +def load_history() -> list: + if HISTORY_FILE.exists(): + return json.loads(HISTORY_FILE.read_text(encoding="utf-8")) + return [] + + +def save_history(history: list): + HISTORY_FILE.write_text( + json.dumps(history, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + +def pdf_pages_to_b64(pdf_path: str, n: int = N_PAGES) -> list[str]: + """Vrati seznam base64 JPEG retezcu pro prvnich n stranek PDF.""" + doc = fitz.open(pdf_path) + pages = min(n, len(doc)) + result = [] + mat = fitz.Matrix(DPI / 72, DPI / 72) + for i in range(pages): + pix = doc[i].get_pixmap(matrix=mat, colorspace=fitz.csRGB) + jpeg = pix.tobytes("jpeg", jpg_quality=75) + result.append(base64.standard_b64encode(jpeg).decode()) + doc.close() + return result + + +def ask_claude(images_b64: list[str], history: list) -> str: + """Posle stranky Claudovi a vrati navrzeny nazev souboru.""" + client = anthropic.Anthropic() + + history_text = "" + if history: + history_text = "\n\nPriklady z minulosti (muj navrh → uzivatel zvolil):\n" + for item in history[-15:]: + history_text += f" {item['claude']} -> {item['user']}\n" + + content: list = [] + for i, b64 in enumerate(images_b64): + content.append({ + "type": "image", + "source": {"type": "base64", "media_type": "image/jpeg", "data": b64}, + }) + + today = date.today().strftime("%Y-%m-%d") + content.append({ + "type": "text", + "text": ( + f"Toto je prvnich {len(images_b64)} stranek naskenované brozury. " + "Navrhni vhodny kratky nazev souboru (bez pripony). " + f"Zacni vzdy datem dnesniho dne ve formatu YYYY-MM-DD ({today}), " + "pak podtrzitko a popis obsahu. " + "Pouzij pouze ASCII znaky, cisla, podtrzitka nebo pomlcky — zadna diakritika ani mezery. " + "Popis ma vystihovat obsah dokumentu, byt strucny (2–4 slova)." + f"{history_text}\n\n" + "Odpovez pouze samotnym nazvem souboru, nic jineho. " + f"Priklad spravneho formatu: {today}_nazev_dokumentu" + ), + }) + + response = client.messages.create( + model="claude-opus-4-7", + max_tokens=64, + thinking={"type": "adaptive"}, + messages=[{"role": "user", "content": content}], + ) + + for block in response.content: + if block.type == "text": + return block.text.strip().strip('"').strip("'") + return "brozura" + + +def dialog(claude_suggestion: str) -> dict: + """Zobrazi dialog s navrzenim nazvem, uzivatel muze opravit.""" + root = tk.Tk() + root.title("Navrh pojmenovani") + root.resizable(False, False) + root.attributes("-topmost", True) + + result = {"name": None, "cancelled": False} + + tk.Label( + root, text="Claude navrhuje:", font=("Segoe UI", 9, "italic"), fg="#555" + ).pack(padx=24, pady=(16, 2), anchor="w") + + tk.Label( + root, text=claude_suggestion, font=("Segoe UI", 12, "bold"), fg="#1a6aaa" + ).pack(padx=24, anchor="w") + + tk.Label( + root, text="Nazev souboru (uprav pokud chces):", font=("Segoe UI", 9) + ).pack(padx=24, pady=(12, 2), anchor="w") + + entry = tk.Entry(root, width=52, font=("Segoe UI", 10)) + entry.insert(0, claude_suggestion) + entry.pack(padx=24) + entry.select_range(0, tk.END) + entry.focus_set() + + def ok(event=None): + val = entry.get().strip() + if val: + result["name"] = val + root.destroy() + + def cancel(event=None): + result["cancelled"] = True + root.destroy() + + btn = tk.Frame(root) + btn.pack(pady=16) + tk.Button(btn, text="OK", width=12, command=ok).pack(side=tk.LEFT, padx=6) + tk.Button(btn, text="Zrusit", width=12, command=cancel).pack(side=tk.LEFT, padx=6) + + root.bind("", ok) + root.bind("", cancel) + root.mainloop() + return result + + +# ---------- hlavni logika ---------- + +def main(): + if len(sys.argv) < 2: + print(__doc__) + sys.exit(1) + + pdf_path = Path(sys.argv[1]) + if not pdf_path.exists(): + print(f"Soubor nenalezen: {pdf_path}") + sys.exit(1) + + # vystupni cesta — volitelny druhy argument + out_path: Path | None = None + if len(sys.argv) > 2: + out_path = Path(sys.argv[2]) + + print(f"Nacitam stranky z: {pdf_path}") + images = pdf_pages_to_b64(str(pdf_path), N_PAGES) + print(f" {len(images)} stranek prevedeno na obrazky") + + history = load_history() + print(f" {len(history)} polozek v historii pojmenovani") + + print("Ptam se Claudea...") + claude_name = ask_claude(images, history) + print(f" Claude navrhuje: {claude_name}") + + res = dialog(claude_name) + + if res["cancelled"] or not res["name"]: + print("Zruseno — soubor nebyl prejmenovan.") + return + + user_name = res["name"] + + # uloz do historie + history.append({"claude": claude_name, "user": user_name}) + save_history(history) + print(f" Historie ulozena ({len(history)} polozek)") + + # urceni vystupniho souboru + if out_path is None: + out_path = pdf_path.parent / (user_name + pdf_path.suffix) + + if out_path != pdf_path: + pdf_path.rename(out_path) + print(f"Prejmenovan: {out_path}") + else: + print(f"Nazev beze zmeny: {out_path}") + + +if __name__ == "__main__": + main() diff --git a/RozkladScanu/rozloz_brozuru.py b/RozkladScanu/rozloz_brozuru.py new file mode 100644 index 0000000..c4a4aca --- /dev/null +++ b/RozkladScanu/rozloz_brozuru.py @@ -0,0 +1,76 @@ +""" +Rozlozi naskenovanou brozuru do PDF se strankami razenymi od 1. + +Predpoklady: + - Kazda PDF stranka obsahuje dve stranky brozury vedle sebe (leva + prava). + - Sken probíhal duplexne od stredoveho listu smerem ven. + - Stredove stranky se odvodi automaticky z poctu PDF stranek. + +Pouziti: + python rozloz_brozuru.py [vystup.pdf] +""" + +import sys +import copy +from pypdf import PdfReader, PdfWriter +from pypdf.generic import RectangleObject + + +def rozloz(input_path, output_path): + reader = PdfReader(input_path) + n_pdf = len(reader.pages) + + center_left = n_pdf # napr. 12 nebo 40 + center_right = n_pdf + 1 # napr. 13 nebo 41 + total_pages = n_pdf * 2 # celkovy pocet stran brozury + + print(f"PDF stranek : {n_pdf}") + print(f"Stran brozury: {total_pages} (stred: {center_left}+{center_right})") + + # Mapovani: brozurova strana -> (pdf_idx 0-based, 'L'/'P') + page_map = {} + n_sheets = n_pdf // 2 + + for k in range(n_sheets): + front_pdf = 2 * k + back_pdf = 2 * k + 1 + + page_map[center_left - 2 * k] = (front_pdf, 'L') + page_map[center_right + 2 * k] = (front_pdf, 'P') + page_map[center_right + 2 * k + 1] = (back_pdf, 'L') + page_map[center_left - 2 * k - 1] = (back_pdf, 'P') + + writer = PdfWriter() + + for bp in range(1, total_pages + 1): + pdf_idx, lr = page_map[bp] + src = reader.pages[pdf_idx] + + width = float(src.mediabox.width) + height = float(src.mediabox.height) + half_w = width / 2 + + new_page = copy.copy(src) + if lr == 'L': + box = RectangleObject([0, 0, half_w, height]) + else: + box = RectangleObject([half_w, 0, width, height]) + new_page.mediabox = box + new_page.cropbox = box + + writer.add_page(new_page) + + with open(output_path, "wb") as f: + writer.write(f) + + print(f"Hotovo: {output_path} ({total_pages} stranek)") + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Pouziti: python rozloz_brozuru.py [vystup.pdf]") + sys.exit(1) + + inp = sys.argv[1] + out = sys.argv[2] if len(sys.argv) > 2 else inp.replace(".pdf", "_serazena.pdf") + rozloz(inp, out)