z230
This commit is contained in:
@@ -0,0 +1,199 @@
|
|||||||
|
"""
|
||||||
|
Batch ořez puzzle z MySQL.
|
||||||
|
|
||||||
|
Pro každý řádek v sudoku_killer kde file_puzzle_cropped IS NULL:
|
||||||
|
- načte file_puzzle + crop_method
|
||||||
|
- ořízne podle metody
|
||||||
|
- uloží zpět do file_puzzle_cropped
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Nastavení — upravuj zde před spuštěním v PyCharm
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
WORKERS = 4 # počet paralelních procesů
|
||||||
|
LIMIT = None # None = vše; číslo (např. 20) = jen prvních N puzzle (pro testování)
|
||||||
|
BATCH = 200 # kolik oříznutých PDF uložit najednou do DB
|
||||||
|
DRY_RUN = False # True = jen ořez, nic se neuloží do DB
|
||||||
|
LOG_EVERY = 500 # vypiš stav do konzole každých N zpracovaných puzzle
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import csv
|
||||||
|
from pathlib import Path
|
||||||
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||||
|
|
||||||
|
import fitz
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "Knihovny"))
|
||||||
|
from mysql_db import connect_mysql
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
sys.stderr.reconfigure(encoding="utf-8")
|
||||||
|
|
||||||
|
ERRORS_CSV = Path(__file__).parent / "crop_errors.csv"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Crop metody — přidat sem nové funkce pro nové metody
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def crop_raycast_auto(pdf_bytes: bytes, params: dict) -> bytes:
|
||||||
|
crop_margin = params.get("crop_margin_pt", 2)
|
||||||
|
|
||||||
|
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
|
||||||
|
page = doc[0]
|
||||||
|
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: 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)
|
||||||
|
|
||||||
|
clip = fitz.Rect(
|
||||||
|
x_left - lw_l / 2 - crop_margin,
|
||||||
|
top_cut - crop_margin,
|
||||||
|
x_right + lw_r / 2 + crop_margin,
|
||||||
|
bot_cut + crop_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
|
||||||
|
|
||||||
|
|
||||||
|
CROP_METHODS = {
|
||||||
|
"raycast_auto": crop_raycast_auto,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Worker — spouští se v samostatném procesu
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def process_one(args):
|
||||||
|
puzzle_id, puzzle_number, pdf_bytes, method_name, params_json = args
|
||||||
|
try:
|
||||||
|
params = json.loads(params_json) if isinstance(params_json, str) else params_json
|
||||||
|
fn = CROP_METHODS.get(method_name)
|
||||||
|
if fn is None:
|
||||||
|
return puzzle_id, puzzle_number, None, f"neznama metoda: {method_name}"
|
||||||
|
cropped = fn(bytes(pdf_bytes), params)
|
||||||
|
return puzzle_id, puzzle_number, cropped, None
|
||||||
|
except Exception as e:
|
||||||
|
return puzzle_id, puzzle_number, None, str(e)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Hlavní logika
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def fetch_todo(limit):
|
||||||
|
import pymysql.cursors
|
||||||
|
conn = connect_mysql(database="puzzle", cursorclass=pymysql.cursors.DictCursor)
|
||||||
|
cur = conn.cursor()
|
||||||
|
sql = """
|
||||||
|
SELECT sk.id, sk.puzzle_number, sk.file_puzzle,
|
||||||
|
cm.name AS method_name, cm.params_json
|
||||||
|
FROM sudoku_killer sk
|
||||||
|
JOIN puzzle_crop_method cm ON sk.crop_method_id = cm.id
|
||||||
|
WHERE sk.file_puzzle_cropped IS NULL
|
||||||
|
ORDER BY sk.puzzle_number
|
||||||
|
"""
|
||||||
|
if limit:
|
||||||
|
sql += f" LIMIT {int(limit)}"
|
||||||
|
cur.execute(sql)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def save_cropped(updates: list[tuple]):
|
||||||
|
"""updates = [(cropped_bytes, puzzle_id), ...]"""
|
||||||
|
import pymysql.cursors
|
||||||
|
conn = connect_mysql(database="puzzle", cursorclass=pymysql.cursors.DictCursor)
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.executemany(
|
||||||
|
"UPDATE sudoku_killer SET file_puzzle_cropped = %s WHERE id = %s",
|
||||||
|
updates,
|
||||||
|
)
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("Nacitam seznam puzzle k orizeni...")
|
||||||
|
rows = fetch_todo(LIMIT)
|
||||||
|
total = len(rows)
|
||||||
|
if total == 0:
|
||||||
|
print("Vsechny puzzle jsou jiz orizeny.")
|
||||||
|
return
|
||||||
|
print(f"Ke zpracovani: {total} puzzle | workers: {WORKERS} | batch: {BATCH} | dry-run: {DRY_RUN}")
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
pending_saves = [] # [(cropped_bytes, puzzle_id)]
|
||||||
|
done = 0
|
||||||
|
|
||||||
|
tasks = [
|
||||||
|
(r["id"], r["puzzle_number"], r["file_puzzle"], r["method_name"], r["params_json"])
|
||||||
|
for r in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
with ProcessPoolExecutor(max_workers=WORKERS) as executor:
|
||||||
|
futures = {executor.submit(process_one, t): t for t in tasks}
|
||||||
|
with tqdm(total=total, unit="puzzle") as bar:
|
||||||
|
for future in as_completed(futures):
|
||||||
|
puzzle_id, puzzle_number, cropped, err = future.result()
|
||||||
|
|
||||||
|
if err:
|
||||||
|
errors.append({"puzzle_id": puzzle_id, "puzzle_number": puzzle_number, "chyba": err})
|
||||||
|
tqdm.write(f" [CHYBA] puzzle #{puzzle_number}: {err}")
|
||||||
|
elif not DRY_RUN:
|
||||||
|
pending_saves.append((cropped, puzzle_id))
|
||||||
|
if len(pending_saves) >= BATCH:
|
||||||
|
save_cropped(pending_saves)
|
||||||
|
pending_saves.clear()
|
||||||
|
|
||||||
|
done += 1
|
||||||
|
bar.update(1)
|
||||||
|
bar.set_postfix(chyby=len(errors), ulozeno=done - len(errors) - len(pending_saves))
|
||||||
|
|
||||||
|
if done % LOG_EVERY == 0:
|
||||||
|
zbyvá = total - done
|
||||||
|
pct = done / total * 100
|
||||||
|
tqdm.write(f" >> {done}/{total} ({pct:.1f}%) | puzzle #{puzzle_number} | zbyvá: {zbyvá} | chyby: {len(errors)}")
|
||||||
|
|
||||||
|
# Uložit zbývající
|
||||||
|
if pending_saves and not DRY_RUN:
|
||||||
|
save_cropped(pending_saves)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
with open(ERRORS_CSV, "w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=["puzzle_id", "puzzle_number", "chyba"])
|
||||||
|
w.writeheader()
|
||||||
|
w.writerows(errors)
|
||||||
|
print(f"\nChyby: {len(errors)} — viz {ERRORS_CSV}")
|
||||||
|
else:
|
||||||
|
print("\nVse bez chyb.")
|
||||||
|
|
||||||
|
ok = done - len(errors)
|
||||||
|
print(f"Hotovo: {ok} orizeno, {len(errors)} chyb, {total - done} preskoceno.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -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 10–70 % (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()
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"2PuzzleOnA4": {
|
||||||
|
"description": "2 puzzle pod sebou, horizontalne vycentrovane, misto po stranach na vypocty",
|
||||||
|
"page": {
|
||||||
|
"format": "A4",
|
||||||
|
"width_pt": 595.276,
|
||||||
|
"height_pt": 841.89
|
||||||
|
},
|
||||||
|
"count": 2,
|
||||||
|
"arrangement": "vertical",
|
||||||
|
"horizontal_align": "center",
|
||||||
|
"vertical_distribution": "equal_gaps",
|
||||||
|
"target_puzzle_width_mm": 117.83,
|
||||||
|
"target_puzzle_height_mm": 117.83,
|
||||||
|
"crop_margin_pt": 2,
|
||||||
|
"info": {
|
||||||
|
"sample_raw_puzzle_mm": "107.12 x 107.12",
|
||||||
|
"scale_used_for_sample": 1.1,
|
||||||
|
"side_margin_mm": 46.09,
|
||||||
|
"gap_between_puzzles_mm": 20.45
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user