Files
sufw-anweisenheiten/generate.py
T
Rob 34e9aa49d6 feat(cli): add shared PDF-to-image rendering and document usage
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
2026-04-17 13:48:27 +02:00

235 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()