Compare commits

...

11 Commits

Author SHA1 Message Date
Peter Adam
331e6e70a9 Rename CSV file to export_brevetcard.csv 2026-02-28 11:29:30 +01:00
Peter Adam
9b42f824f9 Remove static tex files, simplify Dockerfile and build workflow
- Remove brevetkarte.tex (unused static demo, superseded by template)
- Dockerfile: remove unnecessary COPY, workspace is volume-mounted at runtime
- Makefile: remove build-front, default target is now build-personalized
- README: remove references to removed files and targets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 11:15:07 +01:00
Peter Adam
c40fe447b0 Remove brevetkarte-rueckseite.tex 2026-02-28 10:58:27 +01:00
Peter Adam
82deb5ac15 Update .gitignore to exclude brevetkarte-rueckseite.tex 2026-02-28 10:57:44 +01:00
Peter Adam
74269c4fbb Add back side template and update README with event.yml workflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:53:36 +01:00
Peter Adam
84d8cfc2dc Add example YAML event configuration and update .gitignore
- Introduce `event.yml.example` providing a template for event-level configuration.
- Extend `.gitignore` to include `event.yml` to prevent committing real event data.
2026-02-28 10:47:31 +01:00
Peter Adam
9c4c7c6ccb Refactor PDF generation workflow, integrate YAML-based event config, and simplify Makefile scripts.
- Replace `generate-personalized` with unified `generate` target in Makefile, supporting event config integration.
- Add YAML parsing to `generate_cards.py` for event-level placeholders and back side generation.
- Update templates to include `EVENT_*` placeholders and dynamic content rendering.
- Simplify `build` and `build-personalized` targets; consolidate redundant logic.
- Enhance `make help` documentation for updated workflow.
- Adjust LaTeX formatting for back side templates, removing hardcoded spacing.
2026-02-28 10:43:57 +01:00
Peter Adam
f59c52551b Clarify README instructions for building both sides of the PDF 2026-02-28 10:18:54 +01:00
Peter Adam
b3e7d99ec6 Translate README content to German 2026-02-28 10:17:16 +01:00
Peter Adam
fbd55f841a Update README: document CSV workflow and file list
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 10:15:07 +01:00
Peter Adam
dbc3dc18ac Remove outdated 2025 brevet card PDF templates from README. 2026-02-28 10:14:19 +01:00
11 changed files with 387 additions and 549 deletions

8
.gitignore vendored
View File

@@ -11,11 +11,15 @@
*.pdf *.pdf
*.bak *.bak
# Real participant data (copy to "Export Brevetkarte.csv" and fill in) # Real participant data (copy export_brevetcard.csv.example and fill in)
Export Brevetkarte.csv export_brevetcard.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
brevetkarte-rueckseite.tex
# macOS # macOS
.DS_Store .DS_Store

View File

@@ -7,12 +7,4 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
make \ make \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Set working directory WORKDIR /workspace
WORKDIR /workspace
# Copy project files
COPY brevetkarte.tex .
COPY cyclist-logo.png .
# Default command
CMD ["pdflatex", "-interaction=nonstopmode", "brevetkarte.tex"]

View File

@@ -1,42 +1,24 @@
# 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_BACK := brevetkarte-rueckseite.tex
TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex
PDF_FILE_FRONT := brevetkarte.pdf TEX_FILE_BACK := brevetkarte-rueckseite.tex
PDF_FILE_BACK := brevetkarte-rueckseite.pdf
PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf
CSV_FILE := Export Brevetkarte.csv PDF_FILE_BACK := brevetkarte-rueckseite.pdf
CSV_FILE := export_brevetcard.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-back generate build-personalized run shell help
# Default target # Default target
all: build all: build-personalized
# Build both PDFs (builds image if needed, then compiles)
build: build-image build-front build-back
# 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 back side PDF (after generate)
build-pdf: build-front build-back build-back: build-image
# Compile front side PDF
build-front:
@echo "Compiling front side LaTeX to PDF..."
docker run --rm \
-v $(PWD):/workspace \
$(IMAGE_NAME) \
pdflatex -interaction=nonstopmode $(TEX_FILE_FRONT)
@echo "PDF generated: $(PDF_FILE_FRONT)"
# Compile back side PDF
build-back:
@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 +26,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:
@@ -88,22 +75,20 @@ clean-all: clean
docker rmi $(IMAGE_NAME) 2>/dev/null || true docker rmi $(IMAGE_NAME) 2>/dev/null || true
# Rebuild from scratch # Rebuild from scratch
rebuild: clean-all build rebuild: clean-all build-personalized
# Show help # Show help
help: 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 - Generate and compile front + back side PDFs (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 generate - Generate tex files from CSV + event.yml"
@echo " make build-front - Compile front side PDF only" @echo " make build-personalized - Generate and compile front + back side PDFs"
@echo " make build-back - Compile back side PDF only" @echo " make build-back - Compile back side PDF only (after generate)"
@echo " make generate-personalized - Generate personalized cards from CSV" @echo " make shell - Open interactive shell in container"
@echo " make build-personalized - Generate and compile personalized cards" @echo " make clean - Remove generated files (aux, log, pdf)"
@echo " make shell - Open interactive shell in container" @echo " make clean-all - Remove all files and Docker image"
@echo " make clean - Remove generated files (aux, log, pdf)" @echo " make rebuild - Clean everything and rebuild"
@echo " make clean-all - Remove all files and Docker image" @echo " make help - Show this help message"
@echo " make rebuild - Clean everything and rebuild"
@echo " make help - Show this help message"

184
README.md
View File

@@ -1,124 +1,108 @@
# Brevet Card PDF Generator # Brevetkarten-PDF-Generator
LaTeX-based generator for Audax Randonneurs Deutschland brevet cards with front and back sides for duplex printing. LaTeX-basierter Generator für Audax Randonneurs Allemagne Brevetkarten mit Vorder- und Rückseite für den Duplexdruck.
## Quick Start ## Voraussetzungen
```bash
# Build everything and generate both PDFs
make build
# The PDFs will be created as:
# - brevetkarte.pdf (front side with participant info)
# - brevetkarte-rueckseite.pdf (back side with control points)
```
## Prerequisites
- Docker - Docker
- Make - Make
- Python 3 + PyYAML (`pip install pyyaml`)
## Usage ## Konfigurationsdateien
Vor dem ersten Build zwei Dateien aus den Beispielen anlegen und befüllen:
### Build Both PDFs
```bash ```bash
make build cp export_brevetcard.csv.example export_brevetcard.csv
cp event.yml.example event.yml
``` ```
This will: Beide Dateien sind in `.gitignore` eingetragen und werden nicht ins Repository übernommen.
1. Build the Docker image with TeXLive
2. Compile `brevetkarte.tex` to `brevetkarte.pdf` (front side)
3. Compile `brevetkarte-rueckseite.tex` to `brevetkarte-rueckseite.pdf` (back side)
### Build Individual Sides ### 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_brevetcard.csv.example` für das vollständige Format.
## Verwendung
### Personalisierte Karten erzeugen und bauen
```bash ```bash
# Just build front side make build-personalized
make build-front ```
# Just build back side Führt folgende Schritte aus:
1. `generate_cards.py` liest `Export Brevetkarte.csv` + `event.yml`
2. Erzeugt `brevetkarte-personalized.tex` (Vorderseite, eine Karte pro Teilnehmer)
3. Erzeugt `brevetkarte-rueckseite.tex` (Rückseite mit Kontrollpunkten aus event.yml)
4. Kompiliert beide .tex-Dateien zu PDFs
### Einzelne Schritte
```bash
# Nur tex-Dateien generieren (ohne Docker)
make generate
# Nur Rückseite kompilieren (nach generate)
make build-back make build-back
# Build both sides (without rebuilding Docker image)
make build-pdf
``` ```
### Other Commands ### Weitere Befehle
```bash ```bash
# Just build the Docker image make build-image # Docker-Image bauen
make build-image make shell # Interaktive Shell im Container
make clean # Erzeugte Dateien löschen (.aux, .log, .pdf)
# Open interactive shell in container make clean-all # Alles löschen inkl. Docker-Image
make shell make help # Alle Befehle anzeigen
# Clean generated files (.aux, .log, .pdf)
make clean
# Clean everything including Docker image
make clean-all
# Rebuild from scratch
make rebuild
# Show help
make help
``` ```
## Files ## Dateien
- `brevetkarte.tex` - LaTeX source for the front side (participant info) | Datei | Beschreibung |
- `brevetkarte-rueckseite.tex` - LaTeX source for the back side (control points) |---|---|
- `cyclist-logo.png` - Audax Randonneurs logo (extracted from original) | `event.yml.example` | Vorlage für Veranstaltungsdaten (→ als `event.yml` kopieren) |
- `Dockerfile` - Docker image definition with TeXLive | `export_brevetcard.csv.example` | Vorlage für Teilnehmerdaten (→ als `export_brevetcard.csv` kopieren) |
- `Makefile` - Build automation | `brevetkarte-template.tex` | Vorlage Vorderseite (Platzhalter aus CSV + event.yml) |
- `Brevet200km_2025_09_20_Namensseite_blanko.pdf` - Original front side template | `brevetkarte-rueckseite-template.tex` | Vorlage Rückseite (Zellplatzhalter aus event.yml) |
- `Brevet200km_2025_09_20_Kontrollseite.pdf` - Original back side template | `generate_cards.py` | Generiert .tex-Dateien aus Templates + Konfiguration |
| `cyclist-logo.png` | Audax Randonneurs Logo |
| `Dockerfile` | Docker-Image-Definition (debian:bookworm-slim + TeX Live) |
| `Makefile` | Build-Automatisierung |
## Output ## Duplexdruck
### Front Side (brevetkarte.pdf) 1. `brevetkarte-personalized.pdf` auf einer Seite drucken
Contains two identical brevet cards that can be cut in half. Each card includes: 2. `brevetkarte-rueckseite.pdf` auf der Rückseite drucken
- Participant information fields (Name, Address, etc.) 3. Blatt horizontal in der Mitte schneiden → zwei separate Brevetkarten
- Event details (200km "Auf eine Pommes nach Belgien")
- Randonneur Mondiaux rules
- Homologation section
- Start time: 8:30
### Back Side (brevetkarte-rueckseite.pdf)
Contains control points table (4 columns × 6 rows):
- **Rows 1-3**: Control points for upper card
- 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 - Finish (Unisport, Bonn)
- **Rows 4-6**: Control points for lower card (same as above, but Nr. 5: Km 214)
- Empty columns for stamps/signatures
- Control question for verification
## Duplex Printing
The PDFs are designed for duplex (double-sided) printing:
1. Print `brevetkarte.pdf` on one side
2. Print `brevetkarte-rueckseite.pdf` on the reverse side
3. The columns and rows are aligned so that:
- Front side columns match back side columns
- Upper card (rows 1-3) aligns with front side upper card
- Lower card (rows 4-6) aligns with front side lower card
4. Cut the sheet in half horizontally to create two separate brevet cards
## Customization
Edit `brevetkarte.tex` to modify front side:
- Event name, date, and location
- Distance (200km)
- Start time
- Club information
- Brevet number format
Edit `brevetkarte-rueckseite.tex` to modify back side:
- Control point locations
- Control times (von/bis)
- Distances
- Control questions

View 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}

View File

@@ -1,218 +0,0 @@
\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}
% 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}
\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 (Upper card) - height 2.833cm
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 1:} Km 0 - Unisport\\
Nachtigallenweg 86, Bonn\\
\\
\textbf{Kontrollzeit}\\
von: 7:30\\
bis: 8:30
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty for stamps
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 4:} Km 165 Mahlberg Ecke K50,\\
Breitestraße\\
\textbf{Kontrollzeit}\\
von: 13:21\\
bis: 19:30
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Kontrollfrage:}\\
Wann wurde das Kriegerdenkmal\\
eingerichtet?
}
\\
\hline
% Row 2 (Upper card) - height 2.833cm
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 2:} Km 57 -- „Nationalpark-Tor" im\\
alten Bahnhofsgebäude, Heimbach\\
\\
\textbf{Kontrollzeit}\\
von: 9:11\\
bis: 12:21
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty for stamps
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 5:} Km 205 - Unisport\\
Nachtigallenweg 86, Bonn\\
\\
\textbf{Kontrollzeit}\\
von: 13:23\\
bis: 21:00
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty
}
\\
\hline
% Row 3 (Upper card) - height 2.833cm
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 3:} Km 100 - Friterie „Au Petit\\
Creux" oder Total-Tankstelle, Ecke Rue\\
de Botrange/Rue de Charmilles, Waimes\\
\textbf{Kontrollzeit}\\
von: 11:26\\
bis: 15:10
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty for stamps
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty
}
\\
\hline
\end{tabular}
\vspace{1.8cm}
% Lower card table (rows 4-6)
\noindent
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|}
\hline
% Row 1 (Lower card)
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 1:} Km 0 - Unisport\\
Nachtigallenweg 86, Bonn\\
\\
\textbf{Kontrollzeit}\\
von: 7:30\\
bis: 8:30
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty for stamps
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 4:} Km 165 Mahlberg Ecke K50,\\
Breitestraße\\
\textbf{Kontrollzeit}\\
von: 13:21\\
bis: 19:30
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Kontrollfrage:}\\
Wann wurde das Kriegerdenkmal\\
eingerichtet?
}
\\
\hline
% Row 2 (Lower card) - height 2.833cm
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 2:} Km 57 -- „Nationalpark-Tor" im\\
alten Bahnhofsgebäude, Heimbach\\
\\
\textbf{Kontrollzeit}\\
von: 9:11\\
bis: 12:21
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty for stamps
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 5:} Km 214 - Unisport\\
Nachtigallenweg 86, Bonn\\
\\
\textbf{Kontrollzeit}\\
von: 13:23\\
bis: 21:00
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty
}
\\
\hline
% Row 3 (Lower card) - height 2.833cm
\parbox[c][\rowheight][t]{6.5cm}{%
\vspace{2mm}
\textbf{Nr. 3:} Km 100 - Friterie „Au Petit\\
Creux" oder Total-Tankstelle, Ecke Rue\\
de Botrange/Rue de Charmilles, Waimes\\
\textbf{Kontrollzeit}\\
von: 11:26\\
bis: 15:10
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty for stamps
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty
}
&
\parbox[c][\rowheight][c]{6.5cm}{%
% Empty
}
\\
\hline
\end{tabular}
\end{document}

View File

@@ -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) {

View File

@@ -1,139 +0,0 @@
\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{graphicx}
\usepackage{xcolor}
\usepackage{tikz}
\usepackage{helvet}
\usepackage{hyperref}
% Set sans-serif font as default
\renewcommand{\familydefault}{\sfdefault}
% Define colors
\definecolor{headerblack}{RGB}{0,0,0}
% Configure hyperlinks to be black
\hypersetup{
colorlinks=true,
linkcolor=black,
urlcolor=black,
citecolor=black
}
\setlength{\parindent}{0pt}
\pagestyle{empty}
% Command to create one brevet card
\newcommand{\brevetcard}{%
\noindent
\begin{tikzpicture}[x=1cm,y=1cm]
% Black vertical separator lines (drawn first, extend through headers)
\draw[black,line width=0.5pt] (6.9,-7.2) -- (6.9,1.3);
\draw[black,line width=0.5pt] (13.8,-7.2) -- (13.8,1.3);
\draw[black,line width=0.5pt] (20.7,-7.2) -- (20.7,1.3);
% Black header boxes (drawn on top)
\fill[headerblack] (0,0) rectangle (6.9,1.3);
\node[white,align=center,font=\tiny,text width=6.6cm] at (3.45,0.65) {
Jeder Teilnehmer muss diese Brevetkarte zu jeder Zeit\\
mit sich führen und an den Kontrollen abstempeln lassen\\
bzw. Fotos erstellen.\\
\textbf{Ohne Kontrollzeiten und Zielzeit keine Wertung!}
};
\fill[headerblack] (6.9,0) rectangle (13.8,1.3);
\node[white,font=\Large] at (10.35,0.65) {HOMOLOGATION};
\fill[headerblack] (13.8,0) rectangle (20.7,1.3);
\node[white,font=\Large] at (17.25,0.65) {TEILNEHMER/-IN};
\fill[headerblack] (20.7,0) rectangle (27.6,1.3);
\node[white,align=center,font=\normalsize] at (24.15,0.75) {BREVET DES RANDONNEURS};
\node[white,font=\Large] at (24.15,0.35) {MONDIAUX};
% Column 1 - Rules (left section)
\node[anchor=north west,text width=6.6cm,font=\small,align=left] at (0.2,-0.3) {
\textbf{Es gelten die Regeln von}\\
\textbf{Randonneur Mondiaux}\\
\textbf{insbesondere:}
{\setlength{\leftmargini}{0.4cm}%
\begin{itemize}%
\setlength{\itemsep}{1pt}\setlength{\topsep}{2pt}\setlength{\parsep}{0pt}
\item Einhaltung der StVO
\item Beleuchtung und Sicherheitsweste/-Gurt
\item keine Abkürzungen
\item keine Begleitfahrzeuge
\item Rücksicht auf Teilnehmer und Umwelt
\item Rücksicht in den Kontrollen
\end{itemize}}
\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) {
\textbf{AUDAX RANDONNEURS ALLEMAGNE E.V.}\\
\href{http://www.audax-randonneure.de}{www.audax-randonneure.de}\\
- gegründet 1992 in Hamburg -
};
% Column 2 - Homologation (middle-left section)
\node[anchor=north,text width=6.6cm,font=\small,align=center] at (10.35,-0.5) {
Der Randonnée wurde beendet in:\\[0.6cm]
\makebox[2cm]{\dotfill}h\makebox[2cm]{\dotfill}min
};
\node[anchor=center,font=\Large] at (10.35,-4.0) {
HOMOLOGATION
};
\node[anchor=south,text width=6.6cm,font=\small,align=center] at (10.35,-6.8) {
Brevet N° \makebox[5cm]{\dotfill}
};
% Column 3 - Participant Info (middle-right section)
\node[anchor=north west,text width=6.6cm,font=\small,align=left] at (14.0,-0.5) {
Name:\\[0.4cm]
Straße:\\[0.4cm]
PLZ/Ort:\\[0.4cm]
Land:\\[0.4cm]
Medaille:\\[0.6cm]
Startzeit: 8:30
};
% Column 4 - Event Info (right section)
\node[anchor=north,text width=6.6cm,align=center] at (24.15,-0.4) {
\includegraphics[width=5.5cm]{cyclist-logo.png}
};
\node[anchor=north,text width=6.6cm,font=\small,align=center] at (24.15,-2.2) {
\textbf{Auf eine Pommes nach Belgien}\\
Randonnée über \textbf{200} km\\
am \textbf{20. September 2025}\\
mit Start in \textbf{Bonn, Uni-Sportgelände}\\
von \textbf{ARA Rheinland}\\
N° ACP du Club \textbf{111011}
};
\node[anchor=south,text width=6.6cm,font=\scriptsize,align=center] at (24.15,-7.0) {
CONTRÔLÉE ET HOMOLOGUÉE EXCLUSIVEMENT PAR\\
\href{http://www.audax-club-parisien.com}{www.audax-club-parisien.com}\\
- Société fondée en 1904 -
};
\end{tikzpicture}
}
\begin{document}
% First brevet card
\brevetcard
\vspace{0.8cm}
% Second brevet card (identical)
\brevetcard
\end{document}

63
event.yml.example Normal file
View 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": ""

View File

@@ -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_brevetcard.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():
sys.exit(1) print(f"Error: {f} not found!", file=sys.stderr)
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()