add shared render_pdf_pages helper for consistent rasterization reuse the same renderer in regression test and CLI flow add new render-image CLI command to export PDF pages as PNG support single-page render (--page) and full export (--all-pages) add configurable output path and DPI options document the new rendering command and options in README
235 lines
6.7 KiB
Python
235 lines
6.7 KiB
Python
"""CLI entry point for the Attendance PDF Generator.
|
||
|
||
Usage
|
||
-----
|
||
python generate.py generate --file Anwesenheit_2026.xlsx --sheet Feb
|
||
python generate.py validate --file Anwesenheit_2026.xlsx --sheet Feb
|
||
"""
|
||
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import click
|
||
from PIL import Image
|
||
|
||
# Add project root to sys.path so 'src' is importable
|
||
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
||
from src.config_loader import load_config
|
||
from src.excel_reader import extract_attendance, extract_persons, extract_year_from_filename
|
||
from src.pdf_filler import fill_pdf
|
||
from src.pdf_render import render_pdf_pages
|
||
from src.validators import show_preview_and_confirm, validate
|
||
|
||
|
||
@click.group()
|
||
def cli() -> None:
|
||
"""Anwesenheit PDF Generator — Excel → PDF"""
|
||
|
||
|
||
@cli.command("validate")
|
||
@click.option(
|
||
"--file",
|
||
"excel_file",
|
||
required=True,
|
||
type=click.Path(exists=False, file_okay=True, dir_okay=False),
|
||
help="Pfad zur Excel-Datei (z. B. Anwesenheit_2026.xlsx)",
|
||
)
|
||
@click.option(
|
||
"--month",
|
||
required=True,
|
||
help="Name des Monats-Sheets (z. B. Feb, Mrz, Jan)",
|
||
)
|
||
def validate_cmd(excel_file: str, month: str) -> None:
|
||
"""Nur Validierung und Vorschau – keine PDFs werden erzeugt."""
|
||
config = load_config()
|
||
errors = validate(excel_file, month)
|
||
|
||
try:
|
||
year = extract_year_from_filename(excel_file, config)
|
||
except Exception:
|
||
year = 0
|
||
|
||
try:
|
||
persons = extract_persons(excel_file, month, config)
|
||
person_count = len(persons)
|
||
except Exception:
|
||
person_count = 0
|
||
|
||
show_preview_and_confirm(excel_file, month, year, person_count, config, errors)
|
||
|
||
if errors:
|
||
click.echo("\nValidierung fehlgeschlagen. Keine PDFs erzeugt.")
|
||
sys.exit(1)
|
||
else:
|
||
click.echo("\nValidierung erfolgreich.")
|
||
|
||
|
||
@cli.command("generate")
|
||
@click.option(
|
||
"--file",
|
||
"excel_file",
|
||
required=True,
|
||
type=click.Path(exists=False, file_okay=True, dir_okay=False),
|
||
help="Pfad zur Excel-Datei (z. B. Anwesenheit_2026.xlsx)",
|
||
)
|
||
@click.option(
|
||
"--month",
|
||
required=True,
|
||
help="Name des Monats-Sheets (z. B. Feb, Mrz, Jan)",
|
||
)
|
||
@click.option(
|
||
"--out",
|
||
"output_dir",
|
||
required=False,
|
||
default=None,
|
||
type=click.Path(file_okay=False, dir_okay=True),
|
||
help="Ausgabeverzeichnis (Standard: output/ aus config.json)",
|
||
)
|
||
def generate_cmd(excel_file: str, month: str, output_dir: str | None) -> None:
|
||
"""Liest Excel-Daten und erzeugt eine PDF-Datei pro Teilnehmer."""
|
||
config = load_config()
|
||
|
||
# --- Validation ----------------------------------------------------------
|
||
errors = validate(excel_file, month)
|
||
|
||
try:
|
||
year = extract_year_from_filename(excel_file, config)
|
||
except Exception as exc:
|
||
click.echo(f"Fehler bei Jahresextraktion: {exc}", err=True)
|
||
sys.exit(1)
|
||
|
||
try:
|
||
persons = extract_persons(excel_file, month, config)
|
||
except Exception as exc:
|
||
click.echo(f"Fehler beim Lesen der Teilnehmer: {exc}", err=True)
|
||
sys.exit(1)
|
||
|
||
# --- Preview + confirmation ----------------------------------------------
|
||
confirmed = show_preview_and_confirm(
|
||
excel_file, month, year, len(persons), config, errors
|
||
)
|
||
if not confirmed:
|
||
click.echo("Abgebrochen. Keine PDFs erzeugt.")
|
||
sys.exit(0)
|
||
|
||
if errors:
|
||
click.echo("Generierung abgebrochen – es gibt Validierungsfehler.")
|
||
sys.exit(1)
|
||
|
||
# --- Resolve paths -------------------------------------------------------
|
||
project_root = Path(__file__).parent
|
||
template_path = (project_root / config["template_pdf"]).resolve()
|
||
out_dir = Path(output_dir).resolve() if output_dir else (project_root / config["output_dir"]).resolve()
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
# --- Generate PDFs -------------------------------------------------------
|
||
success_count = 0
|
||
fail_count = 0
|
||
|
||
for person in persons:
|
||
try:
|
||
attendance = extract_attendance(excel_file, month, person, year, config)
|
||
output_filename = f"{person.filename_safe_name}.pdf"
|
||
output_path = out_dir / output_filename
|
||
fill_pdf(attendance, template_path, output_path, config)
|
||
click.echo(f" [OK] {output_filename}")
|
||
success_count += 1
|
||
except Exception as exc:
|
||
click.echo(f" [FEHLER] {person.filename_safe_name}: {exc}", err=True)
|
||
fail_count += 1
|
||
|
||
click.echo()
|
||
click.echo(f"Fertig: {success_count} PDF(s) erzeugt, {fail_count} Fehler.")
|
||
click.echo(f"Ausgabeverzeichnis: {out_dir}")
|
||
|
||
if fail_count > 0:
|
||
sys.exit(1)
|
||
|
||
|
||
@cli.command("render-image")
|
||
@click.option(
|
||
"--pdf",
|
||
"pdf_file",
|
||
required=True,
|
||
type=click.Path(exists=True, file_okay=True, dir_okay=False),
|
||
help="Pfad zur PDF-Datei, die als Bild gerendert werden soll.",
|
||
)
|
||
@click.option(
|
||
"--out",
|
||
"output_path",
|
||
required=False,
|
||
default=None,
|
||
type=click.Path(file_okay=True, dir_okay=True),
|
||
help="Ausgabepfad (Datei für Einzelseite oder Ordner für --all-pages).",
|
||
)
|
||
@click.option(
|
||
"--dpi",
|
||
required=False,
|
||
default=150,
|
||
show_default=True,
|
||
type=int,
|
||
help="Render-Auflösung in DPI.",
|
||
)
|
||
@click.option(
|
||
"--page",
|
||
required=False,
|
||
default=1,
|
||
show_default=True,
|
||
type=int,
|
||
help="1-basierter Seitenindex für Einzelseiten-Render.",
|
||
)
|
||
@click.option(
|
||
"--all-pages",
|
||
is_flag=True,
|
||
default=False,
|
||
help="Alle Seiten rendern (ignoriert --page).",
|
||
)
|
||
def render_image_cmd(
|
||
pdf_file: str,
|
||
output_path: str | None,
|
||
dpi: int,
|
||
page: int,
|
||
all_pages: bool,
|
||
) -> None:
|
||
"""Rendert eine PDF direkt als PNG-Bild(er) über die Regression-Renderfunktion."""
|
||
pdf_path = Path(pdf_file).resolve()
|
||
images = render_pdf_pages(pdf_path, dpi=dpi)
|
||
|
||
if not images:
|
||
click.echo("Keine Seiten in der PDF gefunden.", err=True)
|
||
sys.exit(1)
|
||
|
||
if all_pages:
|
||
if output_path:
|
||
out_dir = Path(output_path).resolve()
|
||
else:
|
||
out_dir = pdf_path.parent / f"{pdf_path.stem}_images"
|
||
out_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
for idx, img in enumerate(images, start=1):
|
||
out_file = out_dir / f"{pdf_path.stem}_seite_{idx:02d}.png"
|
||
Image.fromarray(img).save(out_file)
|
||
click.echo(f"[OK] {out_file}")
|
||
return
|
||
|
||
if page < 1 or page > len(images):
|
||
click.echo(
|
||
f"Ungültige Seite: {page}. PDF hat {len(images)} Seite(n).",
|
||
err=True,
|
||
)
|
||
sys.exit(1)
|
||
|
||
if output_path:
|
||
out_file = Path(output_path).resolve()
|
||
else:
|
||
out_file = pdf_path.parent / f"{pdf_path.stem}_seite_{page:02d}.png"
|
||
out_file.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
Image.fromarray(images[page - 1]).save(out_file)
|
||
click.echo(f"[OK] {out_file}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
cli()
|