"""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()