This commit is contained in:
2026-05-06 13:24:43 +02:00
parent 15f70988dc
commit 06b1f87107
11 changed files with 1031 additions and 0 deletions
@@ -0,0 +1,42 @@
from pathlib import Path
from pypdf import PdfReader, PdfWriter, Transformation, PageObject
INPUT_PDF = Path(r"2009-05-04 Puzzle SudokuKiller 376 [difficulty 4 of 10] [average solving time 30 min].pdf")
OUTPUT_PDF = Path(r"sudoku_50pct_A4.pdf")
# A4 v bodech, 72 dpi
A4_WIDTH = 595.2756
A4_HEIGHT = 841.8898
SCALE = 0.5
reader = PdfReader(str(INPUT_PDF))
source_page = reader.pages[0]
source_width = float(source_page.mediabox.width)
source_height = float(source_page.mediabox.height)
# Nová prázdná A4 stránka
new_page = PageObject.create_blank_page(
width=A4_WIDTH,
height=A4_HEIGHT
)
# Výpočet pozice pro vycentrování
target_width = source_width * SCALE
target_height = source_height * SCALE
x = (A4_WIDTH - target_width) / 2
y = (A4_HEIGHT - target_height) / 2
# Vložit původní PDF stránku jako vektorový objekt, zmenšený na 50 %
transform = Transformation().scale(SCALE).translate(x, y)
new_page.merge_transformed_page(source_page, transform, expand=False)
writer = PdfWriter()
writer.add_page(new_page)
with OUTPUT_PDF.open("wb") as f:
writer.write(f)
print(f"Hotovo: {OUTPUT_PDF}")
@@ -0,0 +1,137 @@
"""
Batch crop Killer Sudoku PDF souborů — odstraní nadpis nahoře a copyright dole.
Zachovává vektorový obsah (cairo-generované PDF).
Použití:
python 20_CropPuzzles.py <vstup_dir> <vystup_dir> [--workers N]
"""
import argparse
import csv
import sys
from concurrent.futures import ProcessPoolExecutor, as_completed
from pathlib import Path
import fitz # PyMuPDF
from tqdm import tqdm
def detect_cuts(paths):
"""Vrátí (top_cut, bot_cut) nebo (None, None) pokud detekce selže."""
ys0 = sorted(set(round(p["rect"].y0) for p in paths))
ys1 = sorted(set(round(p["rect"].y1) for p in paths))
top_cut = None
for i in range(1, len(ys0)):
if ys0[i] - ys0[i - 1] > 10:
top_cut = (ys0[i - 1] + ys0[i]) / 2
break
bot_cut = None
for i in range(len(ys1) - 1, 0, -1):
if ys1[i] - ys1[i - 1] > 5:
bot_cut = (ys1[i - 1] + ys1[i]) / 2
break
return top_cut, bot_cut
def crop_one(args):
"""Zpracuje jeden soubor. Vrátí (src_path, status, detail)."""
src_path, dst_path = args
try:
doc_src = fitz.open(str(src_path))
page = doc_src[0]
paths = page.get_drawings()
if not paths:
doc_src.close()
return str(src_path), "anomalie", "žádné kresby (get_drawings prázdný)"
top_cut, bot_cut = detect_cuts(paths)
if top_cut is None or bot_cut is None:
doc_src.close()
return str(src_path), "anomalie", f"gap detekce selhala (top={top_cut}, bot={bot_cut})"
page_w = page.mediabox.width
clip = fitz.Rect(0, top_cut, page_w, bot_cut)
doc_new = fitz.open()
p = doc_new.new_page(width=clip.width, height=clip.height)
p.show_pdf_page(fitz.Rect(0, 0, clip.width, clip.height), doc_src, 0, clip=clip)
dst_path.parent.mkdir(parents=True, exist_ok=True)
doc_new.save(str(dst_path))
doc_src.close()
doc_new.close()
return str(src_path), "ok", ""
except Exception as e:
return str(src_path), "chyba", str(e)
def main():
parser = argparse.ArgumentParser(description="Batch crop Killer Sudoku PDF")
parser.add_argument("vstup", help="Vstupní adresář s PDF soubory")
parser.add_argument("vystup", help="Výstupní adresář pro oříznuté PDF")
parser.add_argument("--workers", type=int, default=4, help="Počet procesů (default: 4)")
args = parser.parse_args()
src_dir = Path(args.vstup)
dst_dir = Path(args.vystup)
if not src_dir.is_dir():
print(f"Chyba: vstupní adresář neexistuje: {src_dir}", file=sys.stderr)
sys.exit(1)
dst_dir.mkdir(parents=True, exist_ok=True)
all_pdfs = sorted(src_dir.rglob("*.pdf"))
if not all_pdfs:
print("Žádné PDF soubory nenalezeny.")
sys.exit(0)
# Přeskočit již zpracované
tasks = []
skipped = 0
for src in all_pdfs:
rel = src.relative_to(src_dir)
dst = dst_dir / rel
if dst.exists():
skipped += 1
else:
tasks.append((src, dst))
print(f"Celkem PDF: {len(all_pdfs)}, přeskočeno (existují): {skipped}, ke zpracování: {len(tasks)}")
if not tasks:
print("Vše již zpracováno.")
return
errors_csv = dst_dir / "errors.csv"
errors = []
with ProcessPoolExecutor(max_workers=args.workers) as executor:
futures = {executor.submit(crop_one, t): t for t in tasks}
with tqdm(total=len(tasks), unit="soubor") as bar:
for future in as_completed(futures):
src_path, status, detail = future.result()
if status != "ok":
errors.append({"soubor": src_path, "typ": status, "detail": detail})
bar.update(1)
bar.set_postfix(chyby=len(errors))
if errors:
with open(errors_csv, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["soubor", "typ", "detail"])
writer.writeheader()
writer.writerows(errors)
print(f"\nChyby/anomálie: {len(errors)} — viz {errors_csv}")
else:
print("\nVšechny soubory zpracovány bez chyb.")
if __name__ == "__main__":
main()
@@ -0,0 +1,61 @@
"""
Crop Killer Sudoku PDF ray-casting metodou:
1. Horizontální paprsek na y_mid → najde x_left, x_right mřížky
2. Vertikální paprsek podél x_left → najde top_cut, bot_cut mřížky
Výsledek: oříznuté PDF jen s mřížkou + malý bílý rámeček (MARGIN).
"""
import fitz
from pathlib import Path
MARGIN = 4 # pt bílého rámečku kolem mřížky
SRC = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/2009-05-04 Puzzle SudokuKiller 376 [difficulty 4 of 10] [average solving time 30 min].pdf")
DST = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/cropped_raycast.pdf")
def crop_raycast(src_path: Path, dst_path: Path, margin: float = MARGIN):
doc = fitz.open(str(src_path))
page = doc[0]
paths = page.get_drawings()
pw = page.mediabox.width
ph = page.mediabox.height
y_mid = ph / 2
# Krok 1: horizontální paprsek na y_mid → x_left, x_right
hit_h = [p["rect"] for p in paths if p["rect"].y0 <= y_mid <= p["rect"].y1]
if not hit_h:
raise ValueError("Horizontální paprsek nenašel žádné kresby na y_mid")
# Elementy z horizontálního paprsku jsou výhradně mřížka (nadpis/copyright
# jsou daleko od y_mid) — jejich y rozsah přímo dává top/bot hranici mřížky.
x_left = min(r.x0 for r in hit_h)
x_right = max(r.x1 for r in hit_h)
top_cut = min(r.y0 for r in hit_h)
bot_cut = max(r.y1 for r in hit_h)
print(f"x_left={x_left:.1f} x_right={x_right:.1f}")
print(f"top_cut={top_cut:.1f} bot_cut={bot_cut:.1f}")
print(f"stránka: {pw:.1f} x {ph:.1f} pt")
clip = fitz.Rect(
x_left - margin,
top_cut - margin,
x_right + margin,
bot_cut + margin,
)
clip_w = clip.width
clip_h = clip.height
doc_new = fitz.open()
p = doc_new.new_page(width=clip_w, height=clip_h)
p.show_pdf_page(fitz.Rect(0, 0, clip_w, clip_h), doc, 0, clip=clip)
doc_new.save(str(dst_path))
doc.close()
doc_new.close()
print(f"Uloženo: {dst_path} ({clip_w:.1f} x {clip_h:.1f} pt)")
crop_raycast(SRC, DST)
@@ -0,0 +1,104 @@
"""
Stáhne 10 puzzle z MySQL (tabulka sudoku_killer), ořízne ray-cast metodou
a uloží do Testy/verify/ pro vizuální verifikaci.
"""
import sys
from pathlib import Path
import fitz
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "Knihovny"))
from mysql_db import connect_mysql
import pymysql.cursors
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
OUT_DIR = Path(__file__).parent / "verify"
OUT_DIR.mkdir(exist_ok=True)
MARGIN = 2 # pt — minimální rámeček
def crop_raycast(pdf_bytes: bytes) -> bytes:
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
page = doc[0]
paths = page.get_drawings()
ph = page.mediabox.height
y_mid = ph / 2
hit_h = [(p["rect"], p.get("width") or 0) for p in paths
if p["rect"].y0 <= y_mid <= p["rect"].y1]
if not hit_h:
raise ValueError("Horizontální paprsek nenašel žádné kresby")
rects = [r for r, _ in hit_h]
x_left = min(r.x0 for r in rects)
x_right = max(r.x1 for r in rects)
top_cut = min(r.y0 for r in rects)
bot_cut = max(r.y1 for r in rects)
# lineWidth svislých okrajových čar — souřadnice jsou středy, ne vizuální okraje
lw_left = next((lw for r, lw in hit_h if r.x0 == x_left), 0)
lw_right = next((lw for r, lw in hit_h if r.x1 == x_right), 0)
vis_x_left = x_left - lw_left / 2
vis_x_right = x_right + lw_right / 2
# top_cut / bot_cut jsou již vnější vizuální hrany (shodují se s okrajem horizontálních čar)
clip = fitz.Rect(
vis_x_left - MARGIN,
top_cut - MARGIN,
vis_x_right + MARGIN,
bot_cut + MARGIN,
)
doc_new = fitz.open()
p = doc_new.new_page(width=clip.width, height=clip.height)
p.show_pdf_page(fitz.Rect(0, 0, clip.width, clip.height), doc, 0, clip=clip)
out = doc_new.tobytes()
doc.close()
doc_new.close()
return out
def main():
import pymysql.cursors
conn = connect_mysql(database="puzzle", cursorclass=pymysql.cursors.DictCursor)
cursor = conn.cursor()
cursor.execute("""
SELECT puzzle_number, puzzle_date, difficulty, file_puzzle
FROM sudoku_killer
WHERE file_puzzle IS NOT NULL
ORDER BY puzzle_number
LIMIT 10
""")
rows = cursor.fetchall()
cursor.close()
conn.close()
print(f"Staženo {len(rows)} záznamů z DB.")
for row in rows:
num = row["puzzle_number"]
date = row["puzzle_date"]
diff = row["difficulty"]
pdf_bytes = bytes(row["file_puzzle"])
try:
cropped = crop_raycast(pdf_bytes)
out_path = OUT_DIR / f"{date} Puzzle SudokuKiller {num} [diff {diff}] cropped.pdf"
out_path.write_bytes(cropped)
print(f" OK #{num}{out_path.name}")
except Exception as e:
print(f" CHYBA #{num}: {e}", file=sys.stderr)
print(f"\nHotovo. Soubory v: {OUT_DIR}")
if __name__ == "__main__":
main()
@@ -0,0 +1,84 @@
"""
Ořízne vzorový puzzle (ray-cast) a vygeneruje jedno PDF s 7 stránkami A4,
každá stránka ukazuje puzzle zmenšený o 1070 % (krok 10 %).
"""
import fitz
from pathlib import Path
SRC = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/2009-05-04 Puzzle SudokuKiller 376 [difficulty 4 of 10] [average solving time 30 min].pdf")
DST = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/size_preview.pdf")
A4_W = 595.276
A4_H = 841.890
MARGIN = 2 # pt bílý rámeček kolem puzzlu po ořezu
def detect_clip(page) -> fitz.Rect:
paths = page.get_drawings()
ph = page.mediabox.height
y_mid = ph / 2
hit_h = [(p["rect"], p.get("width") or 0) for p in paths
if p["rect"].y0 <= y_mid <= p["rect"].y1]
if not hit_h:
raise ValueError("Detekce hranic selhala")
rects = [r for r, _ in hit_h]
x_left = min(r.x0 for r in rects)
x_right = max(r.x1 for r in rects)
top_cut = min(r.y0 for r in rects)
bot_cut = max(r.y1 for r in rects)
lw_left = next((lw for r, lw in hit_h if r.x0 == x_left), 0)
lw_right = next((lw for r, lw in hit_h if r.x1 == x_right), 0)
return fitz.Rect(
x_left - lw_left / 2 - MARGIN,
top_cut - MARGIN,
x_right + lw_right / 2 + MARGIN,
bot_cut + MARGIN,
)
def main():
doc_src = fitz.open(str(SRC))
page_src = doc_src[0]
clip = detect_clip(page_src)
puzzle_w = clip.width
puzzle_h = clip.height
print(f"Oříznutý puzzle: {puzzle_w:.1f} × {puzzle_h:.1f} pt")
doc_out = fitz.open()
scales = [0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70]
for scale in scales:
pw = puzzle_w * scale
ph = puzzle_h * scale
# Vycentrovat na A4
x0 = (A4_W - pw) / 2
y0 = (A4_H - ph) / 2
page = doc_out.new_page(width=A4_W, height=A4_H)
page.show_pdf_page(
fitz.Rect(x0, y0, x0 + pw, y0 + ph),
doc_src, 0,
clip=clip,
)
pct = int(scale * 100)
label = f"{pct} % ({pw:.0f} × {ph:.0f} pt = {pw/72*25.4:.0f} × {ph/72*25.4:.0f} mm)"
page.insert_text((30, 30), label, fontsize=11, color=(0.4, 0.4, 0.4))
print(f" Stránka {pct}%: puzzle {pw:.0f}×{ph:.0f} pt ({pw/72*25.4:.0f}×{ph/72*25.4:.0f} mm)")
doc_out.save(str(DST))
doc_src.close()
doc_out.close()
print(f"\nUloženo: {DST}")
if __name__ == "__main__":
main()
@@ -0,0 +1,73 @@
"""
Ukázka 2 puzzle vedle sebe na A4 — varianty 93 % (mezera 10 pt) a 89 % (mezera 20 pt).
Výsledek: 2stránkové PDF.
"""
import fitz
from pathlib import Path
SRC = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/2009-05-04 Puzzle SudokuKiller 376 [difficulty 4 of 10] [average solving time 30 min].pdf")
DST = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/two_puzzles.pdf")
A4_W = 595.276
A4_H = 841.890
CROP_MARGIN = 2
def detect_clip(page) -> fitz.Rect:
paths = page.get_drawings()
y_mid = page.mediabox.height / 2
hit_h = [(p["rect"], p.get("width") or 0) for p in paths
if p["rect"].y0 <= y_mid <= p["rect"].y1]
rects = [r for r, _ in hit_h]
x_left = min(r.x0 for r in rects)
x_right = max(r.x1 for r in rects)
top_cut = min(r.y0 for r in rects)
bot_cut = max(r.y1 for r in rects)
lw_l = next((lw for r, lw in hit_h if r.x0 == x_left), 0)
lw_r = next((lw for r, lw in hit_h if r.x1 == x_right), 0)
return fitz.Rect(
x_left - lw_l / 2 - CROP_MARGIN,
top_cut - CROP_MARGIN,
x_right + lw_r / 2 + CROP_MARGIN,
bot_cut + CROP_MARGIN,
)
def add_page(doc_out, doc_src, clip, gap_pt):
scale = (A4_W - 3 * gap_pt) / 2 / clip.width
pw = clip.width * scale
ph = clip.height * scale
y0 = (A4_H - ph) / 2 # vertikálně vycentrovat
page = doc_out.new_page(width=A4_W, height=A4_H)
for i in range(2):
x0 = gap_pt + i * (pw + gap_pt)
page.show_pdf_page(fitz.Rect(x0, y0, x0 + pw, y0 + ph), doc_src, 0, clip=clip)
pct = scale * 100
label = (f"mezera {gap_pt:.0f} pt | měřítko {pct:.0f} % | "
f"puzzle {pw:.0f} × {ph:.0f} pt = {pw/72*25.4:.0f} × {ph/72*25.4:.0f} mm")
page.insert_text((30, 25), label, fontsize=9, color=(0.4, 0.4, 0.4))
def main():
doc_src = fitz.open(str(SRC))
clip = detect_clip(doc_src[0])
print(f"Oříznutý puzzle: {clip.width:.1f} × {clip.height:.1f} pt")
doc_out = fitz.open()
for gap in (10, 20):
add_page(doc_out, doc_src, clip, gap)
scale = (A4_W - 3 * gap) / 2 / clip.width
print(f" gap={gap} pt -> meritko {scale*100:.0f} % puzzle {clip.width*scale:.0f}x{clip.height*scale:.0f} pt")
doc_out.save(str(DST))
doc_src.close()
doc_out.close()
print(f"\nUloženo: {DST}")
if __name__ == "__main__":
main()
@@ -0,0 +1,76 @@
"""
2 puzzle na A4 — 100 %, pod sebou, horizontálně vycentrované.
Místo vlevo/vpravo zůstává pro poznámky.
"""
import fitz
from pathlib import Path
SRC = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/2009-05-04 Puzzle SudokuKiller 376 [difficulty 4 of 10] [average solving time 30 min].pdf")
DST = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/two_vertical_110.pdf")
A4_W = 595.276
A4_H = 841.890
CROP_MARGIN = 2
SCALE = 1.10
def detect_clip(page) -> fitz.Rect:
paths = page.get_drawings()
y_mid = page.mediabox.height / 2
hit_h = [(p["rect"], p.get("width") or 0) for p in paths
if p["rect"].y0 <= y_mid <= p["rect"].y1]
rects = [r for r, _ in hit_h]
x_left = min(r.x0 for r in rects)
x_right = max(r.x1 for r in rects)
top_cut = min(r.y0 for r in rects)
bot_cut = max(r.y1 for r in rects)
lw_l = next((lw for r, lw in hit_h if r.x0 == x_left), 0)
lw_r = next((lw for r, lw in hit_h if r.x1 == x_right), 0)
return fitz.Rect(
x_left - lw_l / 2 - CROP_MARGIN,
top_cut - CROP_MARGIN,
x_right + lw_r / 2 + CROP_MARGIN,
bot_cut + CROP_MARGIN,
)
def main():
doc_src = fitz.open(str(SRC))
clip = detect_clip(doc_src[0])
pw = clip.width * SCALE
ph = clip.height * SCALE
# Horizontální pozice — vycentrovat na A4
x0 = (A4_W - pw) / 2
# Vertikální rozdělení: 3 mezery (nahoře, mezi, dole)
gap = (A4_H - 2 * ph) / 3
y_top = gap
y_bot = gap + ph + gap
side_space = x0 # místo vlevo/vpravo pro poznámky
print(f"Puzzle: {pw:.1f} x {ph:.1f} pt ({pw/72*25.4:.0f} x {ph/72*25.4:.0f} mm)")
print(f"Meritko: {SCALE*100:.0f} %")
print(f"Misto vlevo/vpravo: {side_space:.1f} pt ({side_space/72*25.4:.0f} mm)")
print(f"Mezera mezi puzzle: {gap:.1f} pt ({gap/72*25.4:.0f} mm)")
doc_out = fitz.open()
page = doc_out.new_page(width=A4_W, height=A4_H)
for y0_pos in (y_top, y_bot):
page.show_pdf_page(
fitz.Rect(x0, y0_pos, x0 + pw, y0_pos + ph),
doc_src, 0,
clip=clip,
)
doc_out.save(str(DST))
doc_src.close()
doc_out.close()
print(f"Ulozeno: {DST}")
if __name__ == "__main__":
main()
@@ -0,0 +1,99 @@
"""
Změří finální puzzle, spočítá layout "2PuzzleOnA4" a uloží do layouts.json.
"""
import json
import fitz
from pathlib import Path
SRC = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/Testy/2009-05-04 Puzzle SudokuKiller 376 [difficulty 4 of 10] [average solving time 30 min].pdf")
JSON_PATH = Path(r"U:/ordinaceprojekt/SběrDatRůzné/SudokuKiller/layouts.json")
A4_W_PT = 595.276
A4_H_PT = 841.890
CROP_MARGIN = 2
TARGET_SCALE = 1.10 # 110 % — to co se nám líbilo
def pt_to_mm(pt):
return round(pt / 72 * 25.4, 2)
def detect_clip(page) -> fitz.Rect:
paths = page.get_drawings()
y_mid = page.mediabox.height / 2
hit_h = [(p["rect"], p.get("width") or 0) for p in paths
if p["rect"].y0 <= y_mid <= p["rect"].y1]
rects = [r for r, _ in hit_h]
x_left = min(r.x0 for r in rects)
x_right = max(r.x1 for r in rects)
top_cut = min(r.y0 for r in rects)
bot_cut = max(r.y1 for r in rects)
lw_l = next((lw for r, lw in hit_h if r.x0 == x_left), 0)
lw_r = next((lw for r, lw in hit_h if r.x1 == x_right), 0)
return fitz.Rect(
x_left - lw_l / 2 - CROP_MARGIN,
top_cut - CROP_MARGIN,
x_right + lw_r / 2 + CROP_MARGIN,
bot_cut + CROP_MARGIN,
)
def main():
doc = fitz.open(str(SRC))
clip = detect_clip(doc[0])
doc.close()
raw_w_mm = pt_to_mm(clip.width)
raw_h_mm = pt_to_mm(clip.height)
target_w_mm = round(pt_to_mm(clip.width * TARGET_SCALE), 2)
target_h_mm = round(pt_to_mm(clip.height * TARGET_SCALE), 2)
target_w_pt = clip.width * TARGET_SCALE
target_h_pt = clip.height * TARGET_SCALE
gap_pt = (A4_H_PT - 2 * target_h_pt) / 3
side_pt = (A4_W_PT - target_w_pt) / 2
layout = {
"2PuzzleOnA4": {
"description": "2 puzzle pod sebou, horizontalne vycentrovane, misto po stranach na vypocty",
"page": {
"format": "A4",
"width_pt": A4_W_PT,
"height_pt": A4_H_PT
},
"count": 2,
"arrangement": "vertical",
"horizontal_align": "center",
"vertical_distribution": "equal_gaps",
"target_puzzle_width_mm": target_w_mm,
"target_puzzle_height_mm": target_h_mm,
"crop_margin_pt": CROP_MARGIN,
"info": {
"sample_raw_puzzle_mm": f"{raw_w_mm} x {raw_h_mm}",
"scale_used_for_sample": TARGET_SCALE,
"side_margin_mm": pt_to_mm(side_pt),
"gap_between_puzzles_mm": pt_to_mm(gap_pt)
}
}
}
# Načíst existující JSON a přidat/přepsat klíč
if JSON_PATH.exists():
existing = json.loads(JSON_PATH.read_text(encoding="utf-8"))
existing.update(layout)
layout = existing
JSON_PATH.write_text(json.dumps(layout, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"Ulozeno: {JSON_PATH}")
print(f" Surove puzzle: {raw_w_mm} x {raw_h_mm} mm")
print(f" Cilova velikost: {target_w_mm} x {target_h_mm} mm")
print(f" Misto po stranach: {pt_to_mm(side_pt):.1f} mm")
print(f" Mezera mezi puzzle: {pt_to_mm(gap_pt):.1f} mm")
if __name__ == "__main__":
main()
@@ -0,0 +1,133 @@
"""
Načte layout z layouts.json a aplikuje ho na 2 vstupní PDF soubory.
Použití:
python 27_ApplyLayout.py <pdf1> <pdf2> <vystup.pdf> [--layout 2PuzzleOnA4]
Skript si sám detekuje hranice každého puzzle (ray-cast), spočítá
scale z aktuální velikosti vs. cílové velikosti v JSON a rozmístí je.
"""
import sys
import json
import argparse
import fitz
from pathlib import Path
LAYOUTS_JSON = Path(__file__).parent.parent / "layouts.json"
DEFAULT_LAYOUT = "2PuzzleOnA4"
CROP_MARGIN_FALLBACK = 2
def detect_clip(page, crop_margin) -> fitz.Rect:
paths = page.get_drawings()
y_mid = page.mediabox.height / 2
hit_h = [(p["rect"], p.get("width") or 0) for p in paths
if p["rect"].y0 <= y_mid <= p["rect"].y1]
if not hit_h:
raise ValueError("Ray-cast detekce selhala — zadne kresby na y_mid")
rects = [r for r, _ in hit_h]
x_left = min(r.x0 for r in rects)
x_right = max(r.x1 for r in rects)
top_cut = min(r.y0 for r in rects)
bot_cut = max(r.y1 for r in rects)
lw_l = next((lw for r, lw in hit_h if r.x0 == x_left), 0)
lw_r = next((lw for r, lw in hit_h if r.x1 == x_right), 0)
return fitz.Rect(
x_left - lw_l / 2 - crop_margin,
top_cut - crop_margin,
x_right + lw_r / 2 + crop_margin,
bot_cut + crop_margin,
)
def mm_to_pt(mm):
return mm / 25.4 * 72
def apply_2_vertical(doc_out, sources, layout):
page_w = layout["page"]["width_pt"]
page_h = layout["page"]["height_pt"]
target_w_pt = mm_to_pt(layout["target_puzzle_width_mm"])
target_h_pt = mm_to_pt(layout["target_puzzle_height_mm"])
crop_margin = layout.get("crop_margin_pt", CROP_MARGIN_FALLBACK)
page = doc_out.new_page(width=page_w, height=page_h)
clips = []
for doc_src in sources:
clip = detect_clip(doc_src[0], crop_margin)
clips.append(clip)
actual_w_mm = clip.width / 72 * 25.4
actual_h_mm = clip.height / 72 * 25.4
scale_w = target_w_pt / clip.width
scale_h = target_h_pt / clip.height
print(f" Puzzle: {actual_w_mm:.1f} x {actual_h_mm:.1f} mm -> scale {scale_w:.3f} x {scale_h:.3f}")
# Pro každý puzzle spočítej scale individuálně
positions = []
for clip in clips:
pw = clip.width * (target_w_pt / clip.width)
ph = clip.height * (target_h_pt / clip.height)
positions.append((pw, ph))
# Vertikální rozmístění — equal gaps (předpokládáme stejnou výšku obou)
ph0 = positions[0][1]
ph1 = positions[1][1]
gap0 = (page_h - ph0 - ph1) / 3
gap1 = gap0
y0 = gap0
y1 = gap0 + ph0 + gap1
for i, (doc_src, clip, (pw, ph)) in enumerate(zip(sources, clips, positions)):
x0 = (page_w - pw) / 2
y_pos = y0 if i == 0 else y1
page.show_pdf_page(
fitz.Rect(x0, y_pos, x0 + pw, y_pos + ph),
doc_src, 0,
clip=clip,
)
side_mm = ((page_w - positions[0][0]) / 2) / 72 * 25.4
gap_mm = gap0 / 72 * 25.4
print(f" Misto po stranach: {side_mm:.1f} mm | Mezera: {gap_mm:.1f} mm")
def main():
parser = argparse.ArgumentParser(description="Aplikuje layout na 2 puzzle PDF")
parser.add_argument("pdf1", help="Prvni puzzle PDF")
parser.add_argument("pdf2", help="Druhy puzzle PDF")
parser.add_argument("vystup", help="Vystupni PDF")
parser.add_argument("--layout", default=DEFAULT_LAYOUT, help=f"Nazev layoutu (default: {DEFAULT_LAYOUT})")
args = parser.parse_args()
if not LAYOUTS_JSON.exists():
print(f"CHYBA: {LAYOUTS_JSON} nenalezen. Spust nejdrive 26_SaveLayout.py.", file=sys.stderr)
sys.exit(1)
layouts = json.loads(LAYOUTS_JSON.read_text(encoding="utf-8"))
if args.layout not in layouts:
print(f"CHYBA: layout '{args.layout}' nenalezen v {LAYOUTS_JSON}", file=sys.stderr)
print(f"Dostupne layouty: {list(layouts.keys())}", file=sys.stderr)
sys.exit(1)
layout = layouts[args.layout]
print(f"Layout: {args.layout}")
print(f"Cilova velikost: {layout['target_puzzle_width_mm']} x {layout['target_puzzle_height_mm']} mm")
doc1 = fitz.open(args.pdf1)
doc2 = fitz.open(args.pdf2)
doc_out = fitz.open()
apply_2_vertical(doc_out, [doc1, doc2], layout)
doc_out.save(args.vystup)
doc1.close()
doc2.close()
doc_out.close()
print(f"Ulozeno: {args.vystup}")
if __name__ == "__main__":
main()