Compare commits
3 Commits
f59c52551b
...
74269c4fbb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74269c4fbb | ||
|
|
84d8cfc2dc | ||
|
|
9c4c7c6ccb |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -14,6 +14,9 @@
|
|||||||
# Real participant data (copy to "Export Brevetkarte.csv" and fill in)
|
# Real participant data (copy to "Export Brevetkarte.csv" and fill in)
|
||||||
Export Brevetkarte.csv
|
Export Brevetkarte.csv
|
||||||
|
|
||||||
|
# Real event data (copy event.yml.example to event.yml and fill in)
|
||||||
|
event.yml
|
||||||
|
|
||||||
# Generated files
|
# Generated files
|
||||||
brevetkarte-personalized.tex
|
brevetkarte-personalized.tex
|
||||||
|
|
||||||
|
|||||||
46
Makefile
46
Makefile
@@ -1,7 +1,6 @@
|
|||||||
# Makefile for building Brevet card PDF in Docker
|
# Makefile for building Brevet card PDF in Docker
|
||||||
|
|
||||||
IMAGE_NAME := brevetcard-builder
|
IMAGE_NAME := brevetcard-builder
|
||||||
CONTAINER_NAME := brevetcard-build
|
|
||||||
TEX_FILE_FRONT := brevetkarte.tex
|
TEX_FILE_FRONT := brevetkarte.tex
|
||||||
TEX_FILE_BACK := brevetkarte-rueckseite.tex
|
TEX_FILE_BACK := brevetkarte-rueckseite.tex
|
||||||
TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex
|
TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex
|
||||||
@@ -10,23 +9,20 @@ PDF_FILE_BACK := brevetkarte-rueckseite.pdf
|
|||||||
PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf
|
PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf
|
||||||
CSV_FILE := Export Brevetkarte.csv
|
CSV_FILE := Export Brevetkarte.csv
|
||||||
|
|
||||||
.PHONY: all build clean build-image build-pdf build-front build-back generate-personalized build-personalized run shell help
|
.PHONY: all build clean build-image build-front build-back generate build-personalized run shell help
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
# Build both PDFs (builds image if needed, then compiles)
|
# Build static demo front side (builds image if needed)
|
||||||
build: build-image build-front build-back
|
build: build-image build-front
|
||||||
|
|
||||||
# Build Docker image
|
# Build Docker image
|
||||||
build-image:
|
build-image:
|
||||||
@echo "Building Docker image..."
|
@echo "Building Docker image..."
|
||||||
docker build -t $(IMAGE_NAME) .
|
docker build -t $(IMAGE_NAME) .
|
||||||
|
|
||||||
# Compile both PDFs in Docker container
|
# Compile static demo front side PDF
|
||||||
build-pdf: build-front build-back
|
|
||||||
|
|
||||||
# Compile front side PDF
|
|
||||||
build-front:
|
build-front:
|
||||||
@echo "Compiling front side LaTeX to PDF..."
|
@echo "Compiling front side LaTeX to PDF..."
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
@@ -35,8 +31,8 @@ build-front:
|
|||||||
pdflatex -interaction=nonstopmode $(TEX_FILE_FRONT)
|
pdflatex -interaction=nonstopmode $(TEX_FILE_FRONT)
|
||||||
@echo "PDF generated: $(PDF_FILE_FRONT)"
|
@echo "PDF generated: $(PDF_FILE_FRONT)"
|
||||||
|
|
||||||
# Compile back side PDF
|
# Compile back side PDF (always generated from event.yml via generate)
|
||||||
build-back:
|
build-back: build-image
|
||||||
@echo "Compiling back side LaTeX to PDF..."
|
@echo "Compiling back side LaTeX to PDF..."
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v $(PWD):/workspace \
|
-v $(PWD):/workspace \
|
||||||
@@ -44,24 +40,29 @@ build-back:
|
|||||||
pdflatex -interaction=nonstopmode $(TEX_FILE_BACK)
|
pdflatex -interaction=nonstopmode $(TEX_FILE_BACK)
|
||||||
@echo "PDF generated: $(PDF_FILE_BACK)"
|
@echo "PDF generated: $(PDF_FILE_BACK)"
|
||||||
|
|
||||||
# Generate personalized cards from CSV
|
# Generate all tex files from CSV + event.yml
|
||||||
generate-personalized:
|
generate:
|
||||||
@echo "Generating personalized cards from $(CSV_FILE)..."
|
@echo "Generating cards from $(CSV_FILE) + event.yml..."
|
||||||
@if [ ! -f "$(CSV_FILE)" ]; then \
|
@if [ ! -f "$(CSV_FILE)" ]; then \
|
||||||
echo "Error: $(CSV_FILE) not found!"; \
|
echo "Error: $(CSV_FILE) not found!"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
python3 generate_cards.py
|
python3 generate_cards.py
|
||||||
@echo "Generated $(TEX_FILE_PERSONALIZED)"
|
|
||||||
|
|
||||||
# Build personalized cards PDF
|
# Build personalized front + event back side PDFs
|
||||||
build-personalized: generate-personalized build-image
|
build-personalized: generate build-image
|
||||||
@echo "Compiling personalized cards to PDF..."
|
@echo "Compiling personalized front side to PDF..."
|
||||||
docker run --rm \
|
docker run --rm \
|
||||||
-v $(PWD):/workspace \
|
-v $(PWD):/workspace \
|
||||||
$(IMAGE_NAME) \
|
$(IMAGE_NAME) \
|
||||||
pdflatex -interaction=nonstopmode $(TEX_FILE_PERSONALIZED)
|
pdflatex -interaction=nonstopmode $(TEX_FILE_PERSONALIZED)
|
||||||
@echo "PDF generated: $(PDF_FILE_PERSONALIZED)"
|
@echo "PDF generated: $(PDF_FILE_PERSONALIZED)"
|
||||||
|
@echo "Compiling back side to PDF..."
|
||||||
|
docker run --rm \
|
||||||
|
-v $(PWD):/workspace \
|
||||||
|
$(IMAGE_NAME) \
|
||||||
|
pdflatex -interaction=nonstopmode $(TEX_FILE_BACK)
|
||||||
|
@echo "PDF generated: $(PDF_FILE_BACK)"
|
||||||
|
|
||||||
# Run container interactively
|
# Run container interactively
|
||||||
run:
|
run:
|
||||||
@@ -95,13 +96,12 @@ help:
|
|||||||
@echo "Brevet Card PDF Builder"
|
@echo "Brevet Card PDF Builder"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " make build - Build Docker image and compile both PDFs (default)"
|
@echo " make build - Build Docker image and compile static front side (default)"
|
||||||
@echo " make build-image - Build Docker image only"
|
@echo " make build-image - Build Docker image only"
|
||||||
@echo " make build-pdf - Compile both front and back PDFs"
|
@echo " make build-front - Compile static demo front side PDF"
|
||||||
@echo " make build-front - Compile front side PDF only"
|
@echo " make generate - Generate tex files from CSV + event.yml"
|
||||||
@echo " make build-back - Compile back side PDF only"
|
@echo " make build-personalized - Generate and compile front + back side PDFs"
|
||||||
@echo " make generate-personalized - Generate personalized cards from CSV"
|
@echo " make build-back - Compile back side PDF (after generate)"
|
||||||
@echo " make build-personalized - Generate and compile personalized cards"
|
|
||||||
@echo " make shell - Open interactive shell in container"
|
@echo " make shell - Open interactive shell in container"
|
||||||
@echo " make clean - Remove generated files (aux, log, pdf)"
|
@echo " make clean - Remove generated files (aux, log, pdf)"
|
||||||
@echo " make clean-all - Remove all files and Docker image"
|
@echo " make clean-all - Remove all files and Docker image"
|
||||||
|
|||||||
185
README.md
185
README.md
@@ -2,138 +2,111 @@
|
|||||||
|
|
||||||
LaTeX-basierter Generator für Audax Randonneurs Allemagne Brevetkarten mit Vorder- und Rückseite für den Duplexdruck.
|
LaTeX-basierter Generator für Audax Randonneurs Allemagne Brevetkarten mit Vorder- und Rückseite für den Duplexdruck.
|
||||||
|
|
||||||
## Schnellstart
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Alles bauen und beide PDFs erzeugen
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Die PDFs werden erstellt als:
|
|
||||||
# - brevetkarte.pdf (Vorderseite mit Teilnehmerinfos)
|
|
||||||
# - brevetkarte-rueckseite.pdf (Rückseite mit Kontrollen)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Docker
|
- Docker
|
||||||
- Make
|
- Make
|
||||||
|
- Python 3 + PyYAML (`pip install pyyaml`)
|
||||||
|
|
||||||
|
## Konfigurationsdateien
|
||||||
|
|
||||||
|
Vor dem ersten Build zwei Dateien aus den Beispielen anlegen und befüllen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp "Export Brevetkarte.csv.example" "Export Brevetkarte.csv"
|
||||||
|
cp event.yml.example event.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide Dateien sind in `.gitignore` eingetragen und werden nicht ins Repository übernommen.
|
||||||
|
|
||||||
|
### event.yml
|
||||||
|
|
||||||
|
Enthält alle veranstaltungsspezifischen Daten:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
event:
|
||||||
|
title: "Name der Veranstaltung"
|
||||||
|
km: "200"
|
||||||
|
date: "1. Januar 2025"
|
||||||
|
start_location: "Stadt, Ort"
|
||||||
|
club: "Clubname"
|
||||||
|
club_nr: "000000"
|
||||||
|
startzeit: "8:00"
|
||||||
|
|
||||||
|
backside:
|
||||||
|
"1_1": |
|
||||||
|
\vspace{2mm}
|
||||||
|
\textbf{Nr. 1:} Km 0 -- Startort\\
|
||||||
|
...
|
||||||
|
"1_2": "" # leer = Stempelfeld
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Die `backside`-Sektion belegt die 12 Zellen der Rückseite (3 Zeilen × 4 Spalten, Schlüssel `"Zeile_Spalte"`). Der Inhalt ist freies LaTeX. Leere Zellen (`""`) dienen als Stempelfelder.
|
||||||
|
|
||||||
|
### Export Brevetkarte.csv
|
||||||
|
|
||||||
|
Teilnehmerdaten im Format:
|
||||||
|
|
||||||
|
```
|
||||||
|
Startnr, Nachname, Vorname, Straße, PLZ, Ort, Land, Medaille
|
||||||
|
```
|
||||||
|
|
||||||
|
Siehe `Export Brevetkarte.csv.example` für das vollständige Format.
|
||||||
|
|
||||||
## Verwendung
|
## Verwendung
|
||||||
|
|
||||||
### Beide PDFs bauen
|
### Personalisierte Karten erzeugen und bauen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build
|
make build-personalized
|
||||||
```
|
```
|
||||||
|
|
||||||
Dies führt folgende Schritte aus:
|
Führt folgende Schritte aus:
|
||||||
1. Docker-Image mit TeX Live bauen
|
1. `generate_cards.py` liest `Export Brevetkarte.csv` + `event.yml`
|
||||||
2. `brevetkarte.tex` zu `brevetkarte.pdf` kompilieren (Vorderseite)
|
2. Erzeugt `brevetkarte-personalized.tex` (Vorderseite, eine Karte pro Teilnehmer)
|
||||||
3. `brevetkarte-rueckseite.tex` zu `brevetkarte-rueckseite.pdf` kompilieren (Rückseite)
|
3. Erzeugt `brevetkarte-rueckseite.tex` (Rückseite mit Kontrollpunkten aus event.yml)
|
||||||
|
4. Kompiliert beide .tex-Dateien zu PDFs
|
||||||
|
|
||||||
### Einzelne Seiten bauen
|
### Einzelne Schritte
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Nur Vorderseite bauen
|
# Nur tex-Dateien generieren (ohne Docker)
|
||||||
make build-front
|
make generate
|
||||||
|
|
||||||
# Nur Rückseite bauen
|
# Nur Rückseite kompilieren (nach generate)
|
||||||
make build-back
|
make build-back
|
||||||
|
|
||||||
# Beide Seiten bauen
|
# Statische Demo-Vorderseite bauen (ohne CSV, für Tests)
|
||||||
make build-pdf
|
make build-front
|
||||||
```
|
```
|
||||||
|
|
||||||
### Personalisierte Karten aus CSV
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Beispiel-CSV kopieren und Teilnehmerdaten eintragen
|
|
||||||
cp "Export Brevetkarte.csv.example" "Export Brevetkarte.csv"
|
|
||||||
# "Export Brevetkarte.csv" mit den Teilnehmern befüllen
|
|
||||||
|
|
||||||
# 2. Personalisierte Karten erzeugen und bauen
|
|
||||||
make build-personalized
|
|
||||||
# Ausgabe: brevetkarte-personalized.pdf
|
|
||||||
```
|
|
||||||
|
|
||||||
Die CSV-Datei `Export Brevetkarte.csv` wird nicht von git erfasst (sie kann personenbezogene Daten enthalten). Die Datei `Export Brevetkarte.csv.example` zeigt das erwartete Format.
|
|
||||||
|
|
||||||
### Weitere Befehle
|
### Weitere Befehle
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Nur das Docker-Image bauen
|
make build-image # Docker-Image bauen
|
||||||
make build-image
|
make shell # Interaktive Shell im Container
|
||||||
|
make clean # Erzeugte Dateien löschen (.aux, .log, .pdf)
|
||||||
# Interaktive Shell im Container öffnen
|
make clean-all # Alles löschen inkl. Docker-Image
|
||||||
make shell
|
make help # Alle Befehle anzeigen
|
||||||
|
|
||||||
# Erzeugte Dateien löschen (.aux, .log, .pdf)
|
|
||||||
make clean
|
|
||||||
|
|
||||||
# Alles löschen inkl. Docker-Image
|
|
||||||
make clean-all
|
|
||||||
|
|
||||||
# Von Grund auf neu bauen
|
|
||||||
make rebuild
|
|
||||||
|
|
||||||
# Hilfe anzeigen
|
|
||||||
make help
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dateien
|
## Dateien
|
||||||
|
|
||||||
- `brevetkarte.tex` - LaTeX-Quelle für die Vorderseite (zwei identische Blanko-Karten)
|
| Datei | Beschreibung |
|
||||||
- `brevetkarte-template.tex` - Vorlage für personalisierte Karten (Platzhalter werden aus CSV ersetzt)
|
|---|---|
|
||||||
- `brevetkarte-rueckseite.tex` - LaTeX-Quelle für die Rückseite (Kontrollen)
|
| `event.yml.example` | Vorlage für Veranstaltungsdaten (→ als `event.yml` kopieren) |
|
||||||
- `Export Brevetkarte.csv.example` - Beispiel-CSV mit dem Teilnehmerdatenformat
|
| `Export Brevetkarte.csv.example` | Vorlage für Teilnehmerdaten (→ als `Export Brevetkarte.csv` kopieren) |
|
||||||
- `generate_cards.py` - Erzeugt `brevetkarte-personalized.tex` aus CSV und Vorlage
|
| `brevetkarte-template.tex` | Vorlage Vorderseite (Platzhalter aus CSV + event.yml) |
|
||||||
- `cyclist-logo.png` - Audax Randonneurs Logo
|
| `brevetkarte-rueckseite-template.tex` | Vorlage Rückseite (Zellplatzhalter aus event.yml) |
|
||||||
- `Dockerfile` - Docker-Image-Definition (debian:bookworm-slim + TeX-Live-Pakete)
|
| `brevetkarte.tex` | Statische Demo-Vorderseite (ohne Personalisierung) |
|
||||||
- `Makefile` - Build-Automatisierung
|
| `generate_cards.py` | Generiert personalisierte .tex-Dateien |
|
||||||
|
| `cyclist-logo.png` | Audax Randonneurs Logo |
|
||||||
## Ausgabe
|
| `Dockerfile` | Docker-Image-Definition (debian:bookworm-slim + TeX Live) |
|
||||||
|
| `Makefile` | Build-Automatisierung |
|
||||||
### Vorderseite (brevetkarte.pdf)
|
|
||||||
Enthält zwei identische Brevetkarten, die in der Mitte geschnitten werden können. Jede Karte enthält:
|
|
||||||
- Teilnehmerinformationen (Name, Adresse usw.)
|
|
||||||
- Veranstaltungsdetails (200 km „Auf eine Pommes nach Belgien")
|
|
||||||
- Randonneur-Mondiaux-Regeln
|
|
||||||
- Homologationsbereich
|
|
||||||
- Startzeit: 8:30
|
|
||||||
|
|
||||||
### Rückseite (brevetkarte-rueckseite.pdf)
|
|
||||||
Enthält die Kontrollentabelle (4 Spalten × 6 Zeilen):
|
|
||||||
- **Zeilen 1–3**: Kontrollen für die obere Karte
|
|
||||||
- Nr. 1: Start (Km 0 – Unisport, Bonn)
|
|
||||||
- Nr. 2: Km 57 – Nationalpark-Tor, Heimbach
|
|
||||||
- Nr. 3: Km 100 – Friterie „Au Petit Creux", Waimes
|
|
||||||
- Nr. 4: Km 165 – Mahlberg
|
|
||||||
- Nr. 5: Km 205 – Ziel (Unisport, Bonn)
|
|
||||||
- **Zeilen 4–6**: Kontrollen für die untere Karte (wie oben, aber Nr. 5: Km 214)
|
|
||||||
- Leere Spalten für Stempel/Unterschriften
|
|
||||||
- Kontrollfrage zur Verifikation
|
|
||||||
|
|
||||||
## Duplexdruck
|
## Duplexdruck
|
||||||
|
|
||||||
Die PDFs sind für den Duplexdruck (beidseitiger Druck) ausgelegt:
|
1. `brevetkarte-personalized.pdf` auf einer Seite drucken
|
||||||
1. `brevetkarte.pdf` auf einer Seite drucken
|
|
||||||
2. `brevetkarte-rueckseite.pdf` auf der Rückseite drucken
|
2. `brevetkarte-rueckseite.pdf` auf der Rückseite drucken
|
||||||
3. Spalten und Zeilen sind so ausgerichtet, dass:
|
3. Blatt horizontal in der Mitte schneiden → zwei separate Brevetkarten
|
||||||
- Vorderseitenspalten mit Rückseitenspalten übereinstimmen
|
|
||||||
- Obere Karte (Zeilen 1–3) mit der oberen Vorderseitenkarte fluchtet
|
|
||||||
- Untere Karte (Zeilen 4–6) mit der unteren Vorderseitenkarte fluchtet
|
|
||||||
4. Blatt horizontal in der Mitte schneiden, um zwei separate Brevetkarten zu erhalten
|
|
||||||
|
|
||||||
## Anpassung
|
|
||||||
|
|
||||||
`brevetkarte.tex` bearbeiten für die Vorderseite:
|
|
||||||
- Veranstaltungsname, Datum und Ort
|
|
||||||
- Distanz (200 km)
|
|
||||||
- Startzeit
|
|
||||||
- Vereinsinformationen
|
|
||||||
- Brevetformat
|
|
||||||
|
|
||||||
`brevetkarte-rueckseite.tex` bearbeiten für die Rückseite:
|
|
||||||
- Kontrollpunkte
|
|
||||||
- Kontrollzeiten (von/bis)
|
|
||||||
- Distanzen
|
|
||||||
- Kontrollfragen
|
|
||||||
121
brevetkarte-rueckseite-template.tex
Normal file
121
brevetkarte-rueckseite-template.tex
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
\documentclass[a4paper,10pt,landscape]{article}
|
||||||
|
\usepackage[utf8]{inputenc}
|
||||||
|
\usepackage[T1]{fontenc}
|
||||||
|
\usepackage[landscape,top=0.8cm,bottom=0.8cm,left=0.8cm,right=0.8cm]{geometry}
|
||||||
|
\usepackage{array}
|
||||||
|
\usepackage{helvet}
|
||||||
|
|
||||||
|
% Set sans-serif font as default
|
||||||
|
\renewcommand{\familydefault}{\sfdefault}
|
||||||
|
|
||||||
|
\setlength{\parindent}{0pt}
|
||||||
|
\setlength{\tabcolsep}{3pt}
|
||||||
|
\pagestyle{empty}
|
||||||
|
|
||||||
|
\newcommand{\rowheight}{2.833cm}
|
||||||
|
|
||||||
|
\begin{document}
|
||||||
|
|
||||||
|
% Upper card table (rows 1-3)
|
||||||
|
\noindent
|
||||||
|
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|}
|
||||||
|
\hline
|
||||||
|
% Row 1
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_1}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_2}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_3}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_4}}}
|
||||||
|
\\
|
||||||
|
\hline
|
||||||
|
|
||||||
|
% Row 2
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_1}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_2}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_3}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_4}}}
|
||||||
|
\\
|
||||||
|
\hline
|
||||||
|
|
||||||
|
% Row 3
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_1}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_2}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_3}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_4}}}
|
||||||
|
\\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
|
||||||
|
\vspace{1.8cm}
|
||||||
|
|
||||||
|
% Lower card table (rows 1-3, identical)
|
||||||
|
\noindent
|
||||||
|
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|}
|
||||||
|
\hline
|
||||||
|
% Row 1
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_1}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_2}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_3}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_1_4}}}
|
||||||
|
\\
|
||||||
|
\hline
|
||||||
|
|
||||||
|
% Row 2
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_1}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_2}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_3}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_2_4}}}
|
||||||
|
\\
|
||||||
|
\hline
|
||||||
|
|
||||||
|
% Row 3
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_1}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_2}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_3}}}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
|
{{CELL_3_4}}}
|
||||||
|
\\
|
||||||
|
\hline
|
||||||
|
\end{tabular}
|
||||||
|
|
||||||
|
\end{document}
|
||||||
@@ -12,15 +12,6 @@
|
|||||||
\setlength{\tabcolsep}{3pt}
|
\setlength{\tabcolsep}{3pt}
|
||||||
\pagestyle{empty}
|
\pagestyle{empty}
|
||||||
|
|
||||||
% A4 landscape height: 21cm
|
|
||||||
% Half height: 10.5cm from top of page
|
|
||||||
% Top margin: 0.8cm
|
|
||||||
% Row 4 should start at: 10.5 - 0.8 = 9.7cm from start of content
|
|
||||||
%
|
|
||||||
% Front side: each card is 8.5cm tall
|
|
||||||
% So rows 1-3 should be: 8.5cm total (2.833cm each)
|
|
||||||
% Gap after row 3: 9.7 - 8.5 = 1.2cm
|
|
||||||
|
|
||||||
\newcommand{\rowheight}{2.833cm}
|
\newcommand{\rowheight}{2.833cm}
|
||||||
|
|
||||||
\begin{document}
|
\begin{document}
|
||||||
@@ -29,91 +20,79 @@
|
|||||||
\noindent
|
\noindent
|
||||||
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|}
|
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|}
|
||||||
\hline
|
\hline
|
||||||
% Row 1 (Upper card) - height 2.833cm
|
% Row 1
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 1:} Km 0 - Unisport\\
|
\textbf{Nr. 1:} Km 0 -- Unisport\\
|
||||||
Nachtigallenweg 86, Bonn\\
|
Nachtigallenweg 86, Bonn\\
|
||||||
\\
|
\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 7:30\\
|
von: 7:30\\
|
||||||
bis: 8:30
|
bis: 8:30}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty for stamps
|
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 4:} Km 165 Mahlberg Ecke K50,\\
|
\textbf{Nr. 4:} Km 165 -- Mahlberg Ecke K50,\\
|
||||||
Breitestraße\\
|
Breitestraße\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 13:21\\
|
von: 13:21\\
|
||||||
bis: 19:30
|
bis: 19:30}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Kontrollfrage:}\\
|
\textbf{Kontrollfrage:}\\
|
||||||
Wann wurde das Kriegerdenkmal\\
|
Wann wurde das Kriegerdenkmal\\
|
||||||
eingerichtet?
|
eingerichtet?}
|
||||||
}
|
|
||||||
\\
|
\\
|
||||||
\hline
|
\hline
|
||||||
|
|
||||||
% Row 2 (Upper card) - height 2.833cm
|
% Row 2
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 2:} Km 57 -- „Nationalpark-Tor" im\\
|
\textbf{Nr. 2:} Km 57 -- ,,Nationalpark-Tor`` im\\
|
||||||
alten Bahnhofsgebäude, Heimbach\\
|
alten Bahnhofsgebäude, Heimbach\\
|
||||||
\\
|
\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 9:11\\
|
von: 9:11\\
|
||||||
bis: 12:21
|
bis: 12:21}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty for stamps
|
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 5:} Km 205 - Unisport\\
|
\textbf{Nr. 5:} Km 205 -- Unisport\\
|
||||||
Nachtigallenweg 86, Bonn\\
|
Nachtigallenweg 86, Bonn\\
|
||||||
\\
|
\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 13:23\\
|
von: 13:23\\
|
||||||
bis: 21:00
|
bis: 21:00}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty
|
|
||||||
}
|
}
|
||||||
\\
|
\\
|
||||||
\hline
|
\hline
|
||||||
|
|
||||||
% Row 3 (Upper card) - height 2.833cm
|
% Row 3
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 3:} Km 100 - Friterie „Au Petit\\
|
\textbf{Nr. 3:} Km 100 -- Friterie ,,Au Petit\\
|
||||||
Creux" oder Total-Tankstelle, Ecke Rue\\
|
Creux`` oder Total-Tankstelle, Ecke Rue\\
|
||||||
de Botrange/Rue de Charmilles, Waimes\\
|
de Botrange/Rue de Charmilles, Waimes\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 11:26\\
|
von: 11:26\\
|
||||||
bis: 15:10
|
bis: 15:10}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty for stamps
|
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty
|
|
||||||
}
|
|
||||||
&
|
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
|
||||||
% Empty
|
|
||||||
}
|
}
|
||||||
\\
|
\\
|
||||||
\hline
|
\hline
|
||||||
@@ -121,95 +100,83 @@ bis: 15:10
|
|||||||
|
|
||||||
\vspace{1.8cm}
|
\vspace{1.8cm}
|
||||||
|
|
||||||
% Lower card table (rows 4-6)
|
% Lower card table (rows 1-3, identical)
|
||||||
\noindent
|
\noindent
|
||||||
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|}
|
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|}
|
||||||
\hline
|
\hline
|
||||||
% Row 1 (Lower card)
|
% Row 1
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 1:} Km 0 - Unisport\\
|
\textbf{Nr. 1:} Km 0 -- Unisport\\
|
||||||
Nachtigallenweg 86, Bonn\\
|
Nachtigallenweg 86, Bonn\\
|
||||||
\\
|
\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 7:30\\
|
von: 7:30\\
|
||||||
bis: 8:30
|
bis: 8:30}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty for stamps
|
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 4:} Km 165 Mahlberg Ecke K50,\\
|
\textbf{Nr. 4:} Km 165 -- Mahlberg Ecke K50,\\
|
||||||
Breitestraße\\
|
Breitestraße\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 13:21\\
|
von: 13:21\\
|
||||||
bis: 19:30
|
bis: 19:30}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Kontrollfrage:}\\
|
\textbf{Kontrollfrage:}\\
|
||||||
Wann wurde das Kriegerdenkmal\\
|
Wann wurde das Kriegerdenkmal\\
|
||||||
eingerichtet?
|
eingerichtet?}
|
||||||
}
|
|
||||||
\\
|
\\
|
||||||
\hline
|
\hline
|
||||||
|
|
||||||
% Row 2 (Lower card) - height 2.833cm
|
% Row 2
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 2:} Km 57 -- „Nationalpark-Tor" im\\
|
\textbf{Nr. 2:} Km 57 -- ,,Nationalpark-Tor`` im\\
|
||||||
alten Bahnhofsgebäude, Heimbach\\
|
alten Bahnhofsgebäude, Heimbach\\
|
||||||
\\
|
\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 9:11\\
|
von: 9:11\\
|
||||||
bis: 12:21
|
bis: 12:21}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty for stamps
|
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 5:} Km 214 - Unisport\\
|
\textbf{Nr. 5:} Km 205 -- Unisport\\
|
||||||
Nachtigallenweg 86, Bonn\\
|
Nachtigallenweg 86, Bonn\\
|
||||||
\\
|
\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 13:23\\
|
von: 13:23\\
|
||||||
bis: 21:00
|
bis: 21:00}
|
||||||
}
|
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty
|
|
||||||
}
|
}
|
||||||
\\
|
\\
|
||||||
\hline
|
\hline
|
||||||
|
|
||||||
% Row 3 (Lower card) - height 2.833cm
|
% Row 3
|
||||||
\parbox[c][\rowheight][t]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
\vspace{2mm}
|
\vspace{2mm}
|
||||||
\textbf{Nr. 3:} Km 100 - Friterie „Au Petit\\
|
\textbf{Nr. 3:} Km 100 -- Friterie ,,Au Petit\\
|
||||||
Creux" oder Total-Tankstelle, Ecke Rue\\
|
Creux`` oder Total-Tankstelle, Ecke Rue\\
|
||||||
de Botrange/Rue de Charmilles, Waimes\\
|
de Botrange/Rue de Charmilles, Waimes\\
|
||||||
\textbf{Kontrollzeit}\\
|
\textbf{Kontrollzeit}\\
|
||||||
von: 11:26\\
|
von: 11:26\\
|
||||||
bis: 15:10
|
bis: 15:10}
|
||||||
|
&
|
||||||
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty for stamps
|
|
||||||
}
|
}
|
||||||
&
|
&
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
\parbox[c][\rowheight][t]{6.5cm}{%
|
||||||
% Empty
|
|
||||||
}
|
|
||||||
&
|
|
||||||
\parbox[c][\rowheight][c]{6.5cm}{%
|
|
||||||
% Empty
|
|
||||||
}
|
}
|
||||||
\\
|
\\
|
||||||
\hline
|
\hline
|
||||||
|
|||||||
@@ -71,7 +71,6 @@
|
|||||||
\item Rücksicht in den Kontrollen
|
\item Rücksicht in den Kontrollen
|
||||||
\end{itemize}}
|
\end{itemize}}
|
||||||
\hspace{0.3cm}$\Rightarrow$ \textbf{\underline{Bei Verstoß keine Wertung!}}\\[0.3cm]
|
\hspace{0.3cm}$\Rightarrow$ \textbf{\underline{Bei Verstoß keine Wertung!}}\\[0.3cm]
|
||||||
-
|
|
||||||
};
|
};
|
||||||
|
|
||||||
\node[anchor=south west,text width=6.6cm,font=\small,align=left] at (0.2,-7.0) {
|
\node[anchor=south west,text width=6.6cm,font=\small,align=left] at (0.2,-7.0) {
|
||||||
@@ -101,7 +100,7 @@
|
|||||||
PLZ/Ort: {{PLZ_ORT}}\\[0.4cm]
|
PLZ/Ort: {{PLZ_ORT}}\\[0.4cm]
|
||||||
Land: {{LAND}}\\[0.4cm]
|
Land: {{LAND}}\\[0.4cm]
|
||||||
Medaille: {{MEDAILLE}}\\[0.6cm]
|
Medaille: {{MEDAILLE}}\\[0.6cm]
|
||||||
Startzeit: 8:30
|
Startzeit: {{EVENT_STARTZEIT}}
|
||||||
};
|
};
|
||||||
|
|
||||||
% Column 4 - Event Info (right section)
|
% Column 4 - Event Info (right section)
|
||||||
@@ -110,12 +109,12 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
\node[anchor=north,text width=6.6cm,font=\small,align=center] at (24.15,-2.2) {
|
\node[anchor=north,text width=6.6cm,font=\small,align=center] at (24.15,-2.2) {
|
||||||
\textbf{Auf eine Pommes nach Belgien}\\
|
\textbf{{{EVENT_TITLE}}}\\
|
||||||
Randonnée über \textbf{200} km\\
|
Randonnée über \textbf{{{EVENT_KM}}} km\\
|
||||||
am \textbf{20. September 2025}\\
|
am \textbf{{{EVENT_DATE}}}\\
|
||||||
mit Start in \textbf{Bonn, Uni-Sportgelände}\\
|
mit Start in \textbf{{{EVENT_START}}}\\
|
||||||
von \textbf{ARA Rheinland}\\
|
von \textbf{{{EVENT_CLUB}}}\\
|
||||||
N° ACP du Club \textbf{111011}
|
N° ACP du Club \textbf{{{EVENT_CLUB_NR}}}
|
||||||
};
|
};
|
||||||
|
|
||||||
\node[anchor=south,text width=6.6cm,font=\scriptsize,align=center] at (24.15,-7.0) {
|
\node[anchor=south,text width=6.6cm,font=\scriptsize,align=center] at (24.15,-7.0) {
|
||||||
|
|||||||
63
event.yml.example
Normal file
63
event.yml.example
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Event-Konfiguration für die Brevetkarte
|
||||||
|
# Vorderseite - Veranstaltungsinfo (Spalte 4)
|
||||||
|
event:
|
||||||
|
title: "Name der Veranstaltung"
|
||||||
|
km: "200"
|
||||||
|
date: "1. Januar 2025"
|
||||||
|
start_location: "Stadt, Ort"
|
||||||
|
club: "Clubname"
|
||||||
|
club_nr: "000000"
|
||||||
|
startzeit: "8:00"
|
||||||
|
|
||||||
|
# Rückseite - Zelleninhalt (3 Zeilen × 4 Spalten)
|
||||||
|
# LaTeX-Syntax: \\ für Zeilenumbruch, \textbf{...} für Fett
|
||||||
|
# Leere Zellen (für Stempel) einfach leer lassen: ""
|
||||||
|
backside:
|
||||||
|
"1_1": |
|
||||||
|
\vspace{2mm}
|
||||||
|
\textbf{Nr. 1:} Km 0 -- Unisport\\
|
||||||
|
Nachtigallenweg 86, Bonn\\
|
||||||
|
\\
|
||||||
|
\textbf{Kontrollzeit}\\
|
||||||
|
von: 7:30\\
|
||||||
|
bis: 8:30
|
||||||
|
"1_2": ""
|
||||||
|
"1_3": |
|
||||||
|
\vspace{2mm}
|
||||||
|
\textbf{Nr. 4:} Km 000 -- Kontrollname\\
|
||||||
|
Straße\\
|
||||||
|
\textbf{Kontrollzeit}\\
|
||||||
|
von: 00:00\\
|
||||||
|
bis: 00:00
|
||||||
|
"1_4": |
|
||||||
|
\vspace{2mm}
|
||||||
|
\textbf{Kontrollfrage:}\\
|
||||||
|
Fragetext
|
||||||
|
"2_1": |
|
||||||
|
\vspace{2mm}
|
||||||
|
\textbf{Nr. 2:} Km 000 -- Kontrollname\\
|
||||||
|
Adresse\\
|
||||||
|
\\
|
||||||
|
\textbf{Kontrollzeit}\\
|
||||||
|
von: 00:00\\
|
||||||
|
bis: 00:00
|
||||||
|
"2_2": ""
|
||||||
|
"2_3": |
|
||||||
|
\vspace{2mm}
|
||||||
|
\textbf{Nr. 5:} Km 000 -- Kontrollname\\
|
||||||
|
Adresse\\
|
||||||
|
\\
|
||||||
|
\textbf{Kontrollzeit}\\
|
||||||
|
von: 00:00\\
|
||||||
|
bis: 00:00
|
||||||
|
"2_4": ""
|
||||||
|
"3_1": |
|
||||||
|
\vspace{2mm}
|
||||||
|
\textbf{Nr. 3:} Km 000 -- Kontrollname\\
|
||||||
|
Adresse\\
|
||||||
|
\textbf{Kontrollzeit}\\
|
||||||
|
von: 00:00\\
|
||||||
|
bis: 00:00
|
||||||
|
"3_2": ""
|
||||||
|
"3_3": ""
|
||||||
|
"3_4": ""
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Generate personalized brevet cards from CSV data.
|
Generate personalized brevet cards from CSV and event config.
|
||||||
"""
|
"""
|
||||||
import csv
|
import csv
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
except ImportError:
|
||||||
|
print("Error: PyYAML not installed. Run: pip install pyyaml", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def escape_latex(text):
|
def escape_latex(text):
|
||||||
"""Escape special LaTeX characters."""
|
"""Escape special LaTeX characters."""
|
||||||
if not text:
|
if not text:
|
||||||
@@ -27,8 +34,47 @@ def escape_latex(text):
|
|||||||
result = result.replace(char, replacement)
|
result = result.replace(char, replacement)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def load_event_config(config_file):
|
||||||
|
"""Load event configuration from YAML file."""
|
||||||
|
with open(config_file, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_event_placeholders(text, config):
|
||||||
|
"""Replace event-level placeholders in a template string."""
|
||||||
|
event = config.get('event', {})
|
||||||
|
replacements = {
|
||||||
|
'{{EVENT_TITLE}}': escape_latex(event.get('title', '')),
|
||||||
|
'{{EVENT_KM}}': escape_latex(event.get('km', '')),
|
||||||
|
'{{EVENT_DATE}}': escape_latex(event.get('date', '')),
|
||||||
|
'{{EVENT_START}}': escape_latex(event.get('start_location', '')),
|
||||||
|
'{{EVENT_CLUB}}': escape_latex(event.get('club', '')),
|
||||||
|
'{{EVENT_CLUB_NR}}': escape_latex(event.get('club_nr', '')),
|
||||||
|
'{{EVENT_STARTZEIT}}': escape_latex(event.get('startzeit', '')),
|
||||||
|
}
|
||||||
|
for placeholder, value in replacements.items():
|
||||||
|
text = text.replace(placeholder, value)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def generate_backside(template, config):
|
||||||
|
"""Fill back side template with cell content from event config."""
|
||||||
|
cells = config.get('backside', {})
|
||||||
|
result = template
|
||||||
|
for row in range(1, 4):
|
||||||
|
for col in range(1, 5):
|
||||||
|
key = f"{row}_{col}"
|
||||||
|
placeholder = f"{{{{CELL_{row}_{col}}}}}"
|
||||||
|
content = cells.get(key, "")
|
||||||
|
if content is None:
|
||||||
|
content = ""
|
||||||
|
result = result.replace(placeholder, content.strip())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def generate_card_from_template(template, data):
|
def generate_card_from_template(template, data):
|
||||||
"""Replace placeholders in template with participant data."""
|
"""Replace participant placeholders in template with participant data."""
|
||||||
name = f"{escape_latex(data['Vorname'])} {escape_latex(data['Nachname'])}"
|
name = f"{escape_latex(data['Vorname'])} {escape_latex(data['Nachname'])}"
|
||||||
street = escape_latex(data['Straße'])
|
street = escape_latex(data['Straße'])
|
||||||
plz_ort = f"{escape_latex(data['PLZ'])} {escape_latex(data['Ort'])}"
|
plz_ort = f"{escape_latex(data['PLZ'])} {escape_latex(data['Ort'])}"
|
||||||
@@ -36,7 +82,6 @@ def generate_card_from_template(template, data):
|
|||||||
medaille = "Ja" if data['Medaille'].lower() == 'ja' else "Nein"
|
medaille = "Ja" if data['Medaille'].lower() == 'ja' else "Nein"
|
||||||
startnr = escape_latex(data['Startnr'])
|
startnr = escape_latex(data['Startnr'])
|
||||||
|
|
||||||
# Replace placeholders
|
|
||||||
card = template.replace('{{NAME}}', name)
|
card = template.replace('{{NAME}}', name)
|
||||||
card = card.replace('{{STREET}}', street)
|
card = card.replace('{{STREET}}', street)
|
||||||
card = card.replace('{{PLZ_ORT}}', plz_ort)
|
card = card.replace('{{PLZ_ORT}}', plz_ort)
|
||||||
@@ -46,30 +91,33 @@ def generate_card_from_template(template, data):
|
|||||||
|
|
||||||
return card
|
return card
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
csv_file = Path("Export Brevetkarte.csv")
|
csv_file = Path("Export Brevetkarte.csv")
|
||||||
template_file = Path("brevetkarte-template.tex")
|
template_file = Path("brevetkarte-template.tex")
|
||||||
|
backside_template_file = Path("brevetkarte-rueckseite-template.tex")
|
||||||
|
event_config_file = Path("event.yml")
|
||||||
output_file = Path("brevetkarte-personalized.tex")
|
output_file = Path("brevetkarte-personalized.tex")
|
||||||
|
backside_output_file = Path("brevetkarte-rueckseite.tex")
|
||||||
|
|
||||||
if not csv_file.exists():
|
for f in [csv_file, template_file, backside_template_file, event_config_file]:
|
||||||
print(f"Error: {csv_file} not found!", file=sys.stderr)
|
if not f.exists():
|
||||||
|
print(f"Error: {f} not found!", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if not template_file.exists():
|
print(f"Reading event config from {event_config_file}...")
|
||||||
print(f"Error: {template_file} not found!", file=sys.stderr)
|
event_config = load_event_config(event_config_file)
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Read template
|
|
||||||
print(f"Reading template from {template_file}...")
|
print(f"Reading template from {template_file}...")
|
||||||
template = template_file.read_text(encoding='utf-8')
|
template = template_file.read_text(encoding='utf-8')
|
||||||
|
template = apply_event_placeholders(template, event_config)
|
||||||
|
|
||||||
# Read CSV data
|
|
||||||
print(f"Reading participant data from {csv_file}...")
|
print(f"Reading participant data from {csv_file}...")
|
||||||
participants = []
|
participants = []
|
||||||
with open(csv_file, 'r', encoding='utf-8-sig') as f: # utf-8-sig handles BOM
|
with open(csv_file, 'r', encoding='utf-8-sig') as f:
|
||||||
reader = csv.DictReader(f)
|
reader = csv.DictReader(f)
|
||||||
for row in reader:
|
for row in reader:
|
||||||
if row['Startnr']: # Skip empty rows
|
if row['Startnr']:
|
||||||
participants.append(row)
|
participants.append(row)
|
||||||
|
|
||||||
if not participants:
|
if not participants:
|
||||||
@@ -78,35 +126,34 @@ def main():
|
|||||||
|
|
||||||
print(f"Found {len(participants)} participant(s)")
|
print(f"Found {len(participants)} participant(s)")
|
||||||
|
|
||||||
# Generate document with all cards
|
# Generate personalized front side
|
||||||
cards = []
|
cards = []
|
||||||
for i, participant in enumerate(participants):
|
for participant in participants:
|
||||||
card = generate_card_from_template(template, participant)
|
card = generate_card_from_template(template, participant)
|
||||||
cards.append(card)
|
cards.append(card)
|
||||||
|
|
||||||
# Combine cards with spacing/page breaks
|
|
||||||
document_parts = []
|
document_parts = []
|
||||||
for i, card in enumerate(cards):
|
for i, card in enumerate(cards):
|
||||||
document_parts.append(card)
|
document_parts.append(card)
|
||||||
|
|
||||||
# Add vertical space between cards on same page
|
|
||||||
if i % 2 == 0 and i < len(cards) - 1:
|
if i % 2 == 0 and i < len(cards) - 1:
|
||||||
document_parts.append("\n\\vspace{0.8cm}\n\n")
|
document_parts.append("\n\\vspace{0.8cm}\n\n")
|
||||||
|
|
||||||
# Add page break after every 2 cards (except at the end)
|
|
||||||
if i % 2 == 1 and i < len(cards) - 1:
|
if i % 2 == 1 and i < len(cards) - 1:
|
||||||
document_parts.append("\n\\newpage\n\n")
|
document_parts.append("\n\\newpage\n\n")
|
||||||
|
|
||||||
# Close document
|
|
||||||
document_parts.append("\n\\end{document}\n")
|
document_parts.append("\n\\end{document}\n")
|
||||||
|
output_file.write_text(''.join(document_parts), encoding='utf-8')
|
||||||
# Write output
|
|
||||||
document = ''.join(document_parts)
|
|
||||||
output_file.write_text(document, encoding='utf-8')
|
|
||||||
|
|
||||||
print(f"Generated {output_file}")
|
print(f"Generated {output_file}")
|
||||||
|
|
||||||
|
# Generate personalized back side
|
||||||
|
print(f"Reading back side template from {backside_template_file}...")
|
||||||
|
backside_template = backside_template_file.read_text(encoding='utf-8')
|
||||||
|
backside_output = generate_backside(backside_template, event_config)
|
||||||
|
backside_output_file.write_text(backside_output, encoding='utf-8')
|
||||||
|
print(f"Generated {backside_output_file}")
|
||||||
|
|
||||||
print(f"Total pages: {(len(participants) + 1) // 2}")
|
print(f"Total pages: {(len(participants) + 1) // 2}")
|
||||||
print(f"Compile with: make build-personalized")
|
print(f"Compile with: make build-personalized")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
Reference in New Issue
Block a user