Compare commits

..

10 Commits

Author SHA1 Message Date
Peter Adam ca995a1c1f Add duplex PDF build targets with pdftk page interleaving
- Add pdftk-java to Docker image for PDF page manipulation
- Extend generate_backside() to repeat back content num_pages times so
  front and back PDFs have matching page counts for pdftk shuffle
- Add build-duplex and build-blanko-duplex Makefile targets that combine
  front and back PDFs with alternating pages (front1, back1, front2, back2)
- Document duplex targets in README

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 16:16:20 +02:00
Peter Adam f811c3fd80 Move Python generation step into Docker, no local Python required
Add python3 and python3-yaml to the Docker image so generate_cards.py
runs inside the container. Both the generate and build-blanko Makefile
targets now use docker run instead of a local python3 call.
Remove Python/PyYAML from the README prerequisites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:40:48 +02:00
Peter Adam 0c4d630d8e Add build-blanko documentation to README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:39:14 +02:00
Peter Adam b22f1f74a8 Increase card height to 9.5cm, add printer margins, and fix second card placement
- Extend card bottom from y=-7.2 to y=-8.2 (8.5cm → 9.5cm per card) so the
  second card naturally lands in the lower half of the page
- Increase all margins from 0.4cm to 0.6cm for printer compatibility; scale
  front tikzpicture x-axis to 0.990cm to compensate for narrower printable width
- Increase back side row height from 2.833cm to 3.167cm (matching new card height)
  and reduce column width from 7.0cm to 6.9cm to fit within new margins
- Reduce inter-card vspace from 0.8cm to 0.6cm to prevent second card page break

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:32:43 +02:00
Peter Adam f2eda5bf43 Add brevetkarte-blanko.tex to gitignore and clean target
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 09:35:34 +02:00
Peter Adam a2b5c6dc4a Add blanko build target, fix preamble duplication, update event config and margins
- Add --blanko flag to generate_cards.py for blank cards (no CSV needed), 2-up layout
- Fix preamble duplication bug affecting both blanko and multi-participant personalized builds
- Add make build-blanko target; default make now builds personalized + blanko
- Reduce page margins from 0.8cm to 0.4cm for Kyocera P6026
- Widen tikzpicture columns (6.9→7.2cm) and tabular columns (6.6→7.0cm) to fill page width
- Update event.yml for BRM400 Bonn–Lüttich–Bastogne–Bonn, 9. Mai 2026, with 6 controls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 09:33:04 +02:00
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
10 changed files with 198 additions and 431 deletions
+4 -2
View File
@@ -11,14 +11,16 @@
*.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) # Real event data (copy event.yml.example to event.yml and fill in)
event.yml event.yml
# Generated files # Generated files
brevetkarte-personalized.tex brevetkarte-personalized.tex
brevetkarte-blanko.tex
brevetkarte-rueckseite.tex
# macOS # macOS
.DS_Store .DS_Store
+3 -8
View File
@@ -5,14 +5,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
texlive-pictures \ texlive-pictures \
texlive-fonts-recommended \ texlive-fonts-recommended \
make \ make \
python3 \
python3-yaml \
pdftk-java \
&& 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"]
+59 -26
View File
@@ -1,37 +1,27 @@
# Makefile for building Brevet card PDF in Docker # Makefile for building Brevet card PDF in Docker
IMAGE_NAME := brevetcard-builder IMAGE_NAME := brevetcard-builder
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_BLANKO := brevetkarte-blanko.tex
PDF_FILE_BACK := brevetkarte-rueckseite.pdf TEX_FILE_BACK := brevetkarte-rueckseite.tex
PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf
CSV_FILE := Export Brevetkarte.csv PDF_FILE_BLANKO := brevetkarte-blanko.pdf
PDF_FILE_BACK := brevetkarte-rueckseite.pdf
PDF_FILE_DUPLEX := brevetkarte-duplex.pdf
PDF_FILE_BLANKO_DUPLEX := brevetkarte-blanko-duplex.pdf
CSV_FILE := export_brevetcard.csv
.PHONY: all build clean build-image build-front build-back generate build-personalized run shell help .PHONY: all build clean build-image build-back generate build-personalized build-blanko build-duplex build-blanko-duplex run shell help
# Default target # Default target
all: build all: build-personalized build-blanko
# Build static demo front side (builds image if needed)
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 static demo front side PDF # Compile back side PDF (after generate)
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 (always generated from event.yml via generate)
build-back: build-image build-back: build-image
@echo "Compiling back side LaTeX to PDF..." @echo "Compiling back side LaTeX to PDF..."
docker run --rm \ docker run --rm \
@@ -41,12 +31,15 @@ build-back: build-image
@echo "PDF generated: $(PDF_FILE_BACK)" @echo "PDF generated: $(PDF_FILE_BACK)"
# Generate all tex files from CSV + event.yml # Generate all tex files from CSV + event.yml
generate: generate: build-image
@echo "Generating cards from $(CSV_FILE) + event.yml..." @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
docker run --rm \
-v $(PWD):/workspace \
$(IMAGE_NAME) \
python3 generate_cards.py python3 generate_cards.py
# Build personalized front + event back side PDFs # Build personalized front + event back side PDFs
@@ -64,6 +57,44 @@ build-personalized: generate build-image
pdflatex -interaction=nonstopmode $(TEX_FILE_BACK) pdflatex -interaction=nonstopmode $(TEX_FILE_BACK)
@echo "PDF generated: $(PDF_FILE_BACK)" @echo "PDF generated: $(PDF_FILE_BACK)"
# Build blank (blanko) front + event back side PDFs (no CSV required)
build-blanko: build-image
@echo "Generating blank card..."
docker run --rm \
-v $(PWD):/workspace \
$(IMAGE_NAME) \
python3 generate_cards.py --blanko
@echo "Compiling blank front side to PDF..."
docker run --rm \
-v $(PWD):/workspace \
$(IMAGE_NAME) \
pdflatex -interaction=nonstopmode $(TEX_FILE_BLANKO)
@echo "PDF generated: $(PDF_FILE_BLANKO)"
@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)"
# Build duplex PDF (personalized front + back interleaved for duplex printing)
build-duplex: build-personalized
@echo "Combining front and back for duplex printing..."
docker run --rm \
-v $(PWD):/workspace \
$(IMAGE_NAME) \
pdftk A=$(PDF_FILE_PERSONALIZED) B=$(PDF_FILE_BACK) shuffle A B output $(PDF_FILE_DUPLEX)
@echo "PDF generated: $(PDF_FILE_DUPLEX)"
# Build duplex PDF (blank front + back interleaved for duplex printing)
build-blanko-duplex: build-blanko
@echo "Combining blank front and back for duplex printing..."
docker run --rm \
-v $(PWD):/workspace \
$(IMAGE_NAME) \
pdftk A=$(PDF_FILE_BLANKO) B=$(PDF_FILE_BACK) shuffle A B output $(PDF_FILE_BLANKO_DUPLEX)
@echo "PDF generated: $(PDF_FILE_BLANKO_DUPLEX)"
# Run container interactively # Run container interactively
run: run:
docker run --rm -it \ docker run --rm -it \
@@ -81,7 +112,7 @@ shell:
# Clean generated files # Clean generated files
clean: clean:
@echo "Cleaning generated files..." @echo "Cleaning generated files..."
rm -f *.aux *.log *.out *.toc *.pdf rm -f *.aux *.log *.out *.toc *.pdf brevetkarte-personalized.tex brevetkarte-blanko.tex brevetkarte-rueckseite.tex $(PDF_FILE_DUPLEX) $(PDF_FILE_BLANKO_DUPLEX)
# Clean everything including Docker image # Clean everything including Docker image
clean-all: clean clean-all: clean
@@ -89,19 +120,21 @@ 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 static front side (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-front - Compile static demo front side PDF"
@echo " make generate - Generate tex files from CSV + event.yml" @echo " make generate - Generate tex files from CSV + event.yml"
@echo " make build-personalized - Generate and compile front + back side PDFs" @echo " make build-personalized - Generate and compile front + back side PDFs"
@echo " make build-back - Compile back side PDF (after generate)" @echo " make build-blanko - Generate and compile blank card (no CSV needed)"
@echo " make build-duplex - Build duplex PDF (front+back interleaved)"
@echo " make build-blanko-duplex - Build blank duplex PDF"
@echo " make build-back - Compile back side PDF only (after generate)"
@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"
+23 -9
View File
@@ -6,14 +6,13 @@ LaTeX-basierter Generator für Audax Randonneurs Allemagne Brevetkarten mit Vord
- Docker - Docker
- Make - Make
- Python 3 + PyYAML (`pip install pyyaml`)
## Konfigurationsdateien ## Konfigurationsdateien
Vor dem ersten Build zwei Dateien aus den Beispielen anlegen und befüllen: Vor dem ersten Build zwei Dateien aus den Beispielen anlegen und befüllen:
```bash ```bash
cp "Export Brevetkarte.csv.example" "Export Brevetkarte.csv" cp export_brevetcard.csv.example export_brevetcard.csv
cp event.yml.example event.yml cp event.yml.example event.yml
``` ```
@@ -52,7 +51,7 @@ Teilnehmerdaten im Format:
Startnr, Nachname, Vorname, Straße, PLZ, Ort, Land, Medaille Startnr, Nachname, Vorname, Straße, PLZ, Ort, Land, Medaille
``` ```
Siehe `Export Brevetkarte.csv.example` für das vollständige Format. Siehe `export_brevetcard.csv.example` für das vollständige Format.
## Verwendung ## Verwendung
@@ -68,6 +67,25 @@ Führt folgende Schritte aus:
3. Erzeugt `brevetkarte-rueckseite.tex` (Rückseite mit Kontrollpunkten aus event.yml) 3. Erzeugt `brevetkarte-rueckseite.tex` (Rückseite mit Kontrollpunkten aus event.yml)
4. Kompiliert beide .tex-Dateien zu PDFs 4. Kompiliert beide .tex-Dateien zu PDFs
### Duplex-PDF erzeugen
```bash
make build-duplex # personalisierte Karten
make build-blanko-duplex # Blanko-Karten
```
Erzeugt ein einzelnes PDF mit abwechselnden Vorder- und Rückseiten für den direkten Duplexdruck: Seite 1 Vorderseite, Seite 2 Rückseite, Seite 3 nächste Vorderseite, usw. Erfordert kein manuelles Zusammenführen zweier Dateien.
### Blanko-Karten erzeugen und bauen
```bash
make build-blanko
```
Erzeugt Blanko-Karten ohne Teilnehmerdaten — keine `export_brevetcard.csv` erforderlich. Sinnvoll für Nachmeldungen oder als Reservekarten vor Ort. Die Veranstaltungsdaten aus `event.yml` werden übernommen, die Teilnehmerfelder bleiben leer.
Ausgabe: `brevetkarte-blanko.pdf` (Vorderseite) + `brevetkarte-rueckseite.pdf` (Rückseite)
### Einzelne Schritte ### Einzelne Schritte
```bash ```bash
@@ -76,9 +94,6 @@ make generate
# Nur Rückseite kompilieren (nach generate) # Nur Rückseite kompilieren (nach generate)
make build-back make build-back
# Statische Demo-Vorderseite bauen (ohne CSV, für Tests)
make build-front
``` ```
### Weitere Befehle ### Weitere Befehle
@@ -96,11 +111,10 @@ make help # Alle Befehle anzeigen
| Datei | Beschreibung | | Datei | Beschreibung |
|---|---| |---|---|
| `event.yml.example` | Vorlage für Veranstaltungsdaten (→ als `event.yml` kopieren) | | `event.yml.example` | Vorlage für Veranstaltungsdaten (→ als `event.yml` kopieren) |
| `Export Brevetkarte.csv.example` | Vorlage für Teilnehmerdaten (→ als `Export Brevetkarte.csv` kopieren) | | `export_brevetcard.csv.example` | Vorlage für Teilnehmerdaten (→ als `export_brevetcard.csv` kopieren) |
| `brevetkarte-template.tex` | Vorlage Vorderseite (Platzhalter aus CSV + event.yml) | | `brevetkarte-template.tex` | Vorlage Vorderseite (Platzhalter aus CSV + event.yml) |
| `brevetkarte-rueckseite-template.tex` | Vorlage Rückseite (Zellplatzhalter aus event.yml) | | `brevetkarte-rueckseite-template.tex` | Vorlage Rückseite (Zellplatzhalter aus event.yml) |
| `brevetkarte.tex` | Statische Demo-Vorderseite (ohne Personalisierung) | | `generate_cards.py` | Generiert .tex-Dateien aus Templates + Konfiguration |
| `generate_cards.py` | Generiert personalisierte .tex-Dateien |
| `cyclist-logo.png` | Audax Randonneurs Logo | | `cyclist-logo.png` | Audax Randonneurs Logo |
| `Dockerfile` | Docker-Image-Definition (debian:bookworm-slim + TeX Live) | | `Dockerfile` | Docker-Image-Definition (debian:bookworm-slim + TeX Live) |
| `Makefile` | Build-Automatisierung | | `Makefile` | Build-Automatisierung |
+29 -29
View File
@@ -1,7 +1,7 @@
\documentclass[a4paper,10pt,landscape]{article} \documentclass[a4paper,10pt,landscape]{article}
\usepackage[utf8]{inputenc} \usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc} \usepackage[T1]{fontenc}
\usepackage[landscape,top=0.8cm,bottom=0.8cm,left=0.8cm,right=0.8cm]{geometry} \usepackage[landscape,top=0.6cm,bottom=0.6cm,left=0.6cm,right=0.6cm]{geometry}
\usepackage{array} \usepackage{array}
\usepackage{helvet} \usepackage{helvet}
@@ -12,107 +12,107 @@
\setlength{\tabcolsep}{3pt} \setlength{\tabcolsep}{3pt}
\pagestyle{empty} \pagestyle{empty}
\newcommand{\rowheight}{2.833cm} \newcommand{\rowheight}{3.167cm}
\begin{document} \begin{document}
% Upper card table (rows 1-3) % Upper card table (rows 1-3)
\noindent \noindent
\begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|} \begin{tabular}{|p{6.9cm}|p{6.9cm}|p{6.9cm}|p{6.9cm}|}
\hline \hline
% Row 1 % Row 1
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_1}}} {{CELL_1_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_2}}} {{CELL_1_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_3}}} {{CELL_1_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_4}}} {{CELL_1_4}}}
\\ \\
\hline \hline
% Row 2 % Row 2
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_1}}} {{CELL_2_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_2}}} {{CELL_2_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_3}}} {{CELL_2_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_4}}} {{CELL_2_4}}}
\\ \\
\hline \hline
% Row 3 % Row 3
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_1}}} {{CELL_3_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_2}}} {{CELL_3_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_3}}} {{CELL_3_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_4}}} {{CELL_3_4}}}
\\ \\
\hline \hline
\end{tabular} \end{tabular}
\vspace{1.8cm} \vspace{0.6cm}
% Lower card table (rows 1-3, identical) % 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.9cm}|p{6.9cm}|p{6.9cm}|p{6.9cm}|}
\hline \hline
% Row 1 % Row 1
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_1}}} {{CELL_1_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_2}}} {{CELL_1_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_3}}} {{CELL_1_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_1_4}}} {{CELL_1_4}}}
\\ \\
\hline \hline
% Row 2 % Row 2
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_1}}} {{CELL_2_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_2}}} {{CELL_2_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_3}}} {{CELL_2_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_2_4}}} {{CELL_2_4}}}
\\ \\
\hline \hline
% Row 3 % Row 3
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_1}}} {{CELL_3_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_2}}} {{CELL_3_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_3}}} {{CELL_3_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.8cm}{%
{{CELL_3_4}}} {{CELL_3_4}}}
\\ \\
\hline \hline
-185
View File
@@ -1,185 +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}
\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}{%
\vspace{2mm}
\textbf{Nr. 1:} Km 0 -- Unisport\\
Nachtigallenweg 86, Bonn\\
\\
\textbf{Kontrollzeit}\\
von: 7:30\\
bis: 8:30}
&
\parbox[c][\rowheight][t]{6.5cm}{%
}
&
\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
\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][t]{6.5cm}{%
}
&
\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][t]{6.5cm}{%
}
\\
\hline
% Row 3
\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][t]{6.5cm}{%
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
}
\\
\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}{%
\vspace{2mm}
\textbf{Nr. 1:} Km 0 -- Unisport\\
Nachtigallenweg 86, Bonn\\
\\
\textbf{Kontrollzeit}\\
von: 7:30\\
bis: 8:30}
&
\parbox[c][\rowheight][t]{6.5cm}{%
}
&
\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
\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][t]{6.5cm}{%
}
&
\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][t]{6.5cm}{%
}
\\
\hline
% Row 3
\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][t]{6.5cm}{%
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
}
&
\parbox[c][\rowheight][t]{6.5cm}{%
}
\\
\hline
\end{tabular}
\end{document}
+23 -23
View File
@@ -1,7 +1,7 @@
\documentclass[a4paper,10pt,landscape]{article} \documentclass[a4paper,10pt,landscape]{article}
\usepackage[utf8]{inputenc} \usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc} \usepackage[T1]{fontenc}
\usepackage[landscape,top=0.8cm,bottom=0.8cm,left=0.8cm,right=0.8cm]{geometry} \usepackage[landscape,top=0.6cm,bottom=0.6cm,left=0.6cm,right=0.6cm]{geometry}
\usepackage{graphicx} \usepackage{graphicx}
\usepackage{xcolor} \usepackage{xcolor}
\usepackage{tikz} \usepackage{tikz}
@@ -29,34 +29,34 @@
% Brevet card for {{NAME}} (Start #{{STARTNR}}) % Brevet card for {{NAME}} (Start #{{STARTNR}})
\noindent \noindent
\begin{tikzpicture}[x=1cm,y=1cm] \begin{tikzpicture}[x=0.990cm,y=1cm]
% Black vertical separator lines (drawn first, extend through headers) % 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] (7.2,-8.2) -- (7.2,1.3);
\draw[black,line width=0.5pt] (13.8,-7.2) -- (13.8,1.3); \draw[black,line width=0.5pt] (14.4,-8.2) -- (14.4,1.3);
\draw[black,line width=0.5pt] (20.7,-7.2) -- (20.7,1.3); \draw[black,line width=0.5pt] (21.6,-8.2) -- (21.6,1.3);
% Black header boxes (drawn on top) % Black header boxes (drawn on top)
\fill[headerblack] (0,0) rectangle (6.9,1.3); \fill[headerblack] (0,0) rectangle (7.2,1.3);
\node[white,align=center,font=\tiny,text width=6.6cm] at (3.45,0.65) { \node[white,align=center,font=\tiny,text width=6.9cm] at (3.6,0.65) {
Jeder Teilnehmer muss diese Brevetkarte zu jeder Zeit\\ Jeder Teilnehmer muss diese Brevetkarte zu jeder Zeit\\
mit sich führen und an den Kontrollen abstempeln lassen\\ mit sich führen und an den Kontrollen abstempeln lassen\\
bzw. Fotos erstellen.\\ bzw. Fotos erstellen.\\
\textbf{Ohne Kontrollzeiten und Zielzeit keine Wertung!} \textbf{Ohne Kontrollzeiten und Zielzeit keine Wertung!}
}; };
\fill[headerblack] (6.9,0) rectangle (13.8,1.3); \fill[headerblack] (7.2,0) rectangle (14.4,1.3);
\node[white,font=\Large] at (10.35,0.65) {HOMOLOGATION}; \node[white,font=\Large] at (10.8,0.65) {HOMOLOGATION};
\fill[headerblack] (13.8,0) rectangle (20.7,1.3); \fill[headerblack] (14.4,0) rectangle (21.6,1.3);
\node[white,font=\Large] at (17.25,0.65) {TEILNEHMER/-IN}; \node[white,font=\Large] at (18.0,0.65) {TEILNEHMER/-IN};
\fill[headerblack] (20.7,0) rectangle (27.6,1.3); \fill[headerblack] (21.6,0) rectangle (28.8,1.3);
\node[white,align=center,font=\normalsize] at (24.15,0.75) {BREVET DES RANDONNEURS}; \node[white,align=center,font=\normalsize] at (25.2,0.75) {BREVET DES RANDONNEURS};
\node[white,font=\Large] at (24.15,0.35) {MONDIAUX}; \node[white,font=\Large] at (25.2,0.35) {MONDIAUX};
% Column 1 - Rules (left section) % Column 1 - Rules (left section)
\node[anchor=north west,text width=6.6cm,font=\small,align=left] at (0.2,-0.3) { \node[anchor=north west,text width=6.9cm,font=\small,align=left] at (0.2,-0.3) {
\textbf{Es gelten die Regeln von}\\ \textbf{Es gelten die Regeln von}\\
\textbf{Randonneur Mondiaux}\\ \textbf{Randonneur Mondiaux}\\
\textbf{insbesondere:} \textbf{insbesondere:}
@@ -73,28 +73,28 @@
\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.9cm,font=\small,align=left] at (0.2,-8.0) {
\textbf{AUDAX RANDONNEURS ALLEMAGNE E.V.}\\ \textbf{AUDAX RANDONNEURS ALLEMAGNE E.V.}\\
\href{http://www.audax-randonneure.de}{www.audax-randonneure.de}\\ \href{http://www.audax-randonneure.de}{www.audax-randonneure.de}\\
- gegründet 1992 in Hamburg - - gegründet 1992 in Hamburg -
}; };
% Column 2 - Homologation (middle-left section) % Column 2 - Homologation (middle-left section)
\node[anchor=north,text width=6.6cm,font=\small,align=center] at (10.35,-0.5) { \node[anchor=north,text width=6.9cm,font=\small,align=center] at (10.8,-0.5) {
Der Randonnée wurde beendet in:\\[0.6cm] Der Randonnée wurde beendet in:\\[0.6cm]
\makebox[2cm]{\dotfill}h\makebox[2cm]{\dotfill}min \makebox[2cm]{\dotfill}h\makebox[2cm]{\dotfill}min
}; };
\node[anchor=center,font=\Large] at (10.35,-4.0) { \node[anchor=center,font=\Large] at (10.8,-4.0) {
HOMOLOGATION HOMOLOGATION
}; };
\node[anchor=south,text width=6.6cm,font=\small,align=center] at (10.35,-6.8) { \node[anchor=south,text width=6.9cm,font=\small,align=center] at (10.8,-7.8) {
Brevet N° \makebox[5cm]{\dotfill} Brevet N° \makebox[5cm]{\dotfill}
}; };
% Column 3 - Participant Info (middle-right section) % Column 3 - Participant Info (middle-right section)
\node[anchor=north west,text width=6.6cm,font=\small,align=left] at (14.0,-0.5) { \node[anchor=north west,text width=6.9cm,font=\small,align=left] at (14.6,-0.5) {
Name: {{NAME}}\\[0.4cm] Name: {{NAME}}\\[0.4cm]
Straße: {{STREET}}\\[0.4cm] Straße: {{STREET}}\\[0.4cm]
PLZ/Ort: {{PLZ_ORT}}\\[0.4cm] PLZ/Ort: {{PLZ_ORT}}\\[0.4cm]
@@ -104,11 +104,11 @@
}; };
% Column 4 - Event Info (right section) % Column 4 - Event Info (right section)
\node[anchor=north,text width=6.6cm,align=center] at (24.15,-0.4) { \node[anchor=north,text width=6.9cm,align=center] at (25.2,-0.4) {
\includegraphics[width=5.5cm]{cyclist-logo.png} \includegraphics[width=5.5cm]{cyclist-logo.png}
}; };
\node[anchor=north,text width=6.6cm,font=\small,align=center] at (24.15,-2.2) { \node[anchor=north,text width=6.9cm,font=\small,align=center] at (25.2,-2.2) {
\textbf{{{EVENT_TITLE}}}\\ \textbf{{{EVENT_TITLE}}}\\
Randonnée über \textbf{{{EVENT_KM}}} km\\ Randonnée über \textbf{{{EVENT_KM}}} km\\
am \textbf{{{EVENT_DATE}}}\\ am \textbf{{{EVENT_DATE}}}\\
@@ -117,7 +117,7 @@
N° ACP du Club \textbf{{{EVENT_CLUB_NR}}} 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.9cm,font=\scriptsize,align=center] at (25.2,-8.0) {
CONTRÔLÉE ET HOMOLOGUÉE EXCLUSIVEMENT PAR\\ CONTRÔLÉE ET HOMOLOGUÉE EXCLUSIVEMENT PAR\\
\href{http://www.audax-club-parisien.com}{www.audax-club-parisien.com}\\ \href{http://www.audax-club-parisien.com}{www.audax-club-parisien.com}\\
- Société fondée en 1904 - - Société fondée en 1904 -
-139
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}
+54 -7
View File
@@ -2,6 +2,7 @@
""" """
Generate personalized brevet cards from CSV and event config. Generate personalized brevet cards from CSV and event config.
""" """
import argparse
import csv import csv
import sys import sys
from pathlib import Path from pathlib import Path
@@ -58,7 +59,7 @@ def apply_event_placeholders(text, config):
return text return text
def generate_backside(template, config): def generate_backside(template, config, num_pages=1):
"""Fill back side template with cell content from event config.""" """Fill back side template with cell content from event config."""
cells = config.get('backside', {}) cells = config.get('backside', {})
result = template result = template
@@ -70,8 +71,19 @@ def generate_backside(template, config):
if content is None: if content is None:
content = "" content = ""
result = result.replace(placeholder, content.strip()) result = result.replace(placeholder, content.strip())
if num_pages <= 1:
return result return result
# Repeat body between \begin{document} and \end{document} num_pages times
begin_marker = '\\begin{document}\n'
end_marker = '\\end{document}'
begin_idx = result.find(begin_marker) + len(begin_marker)
end_idx = result.rfind(end_marker)
preamble = result[:begin_idx]
body = result[begin_idx:end_idx].rstrip()
return preamble + ('\n\n\\newpage\n\n').join([body] * num_pages) + '\n' + result[end_idx:]
def generate_card_from_template(template, data): def generate_card_from_template(template, data):
"""Replace participant placeholders in template with participant data.""" """Replace participant placeholders in template with participant data."""
@@ -92,8 +104,24 @@ def generate_card_from_template(template, data):
return card return card
def generate_blanko_card(template):
"""Generate a blank card with empty participant fields."""
card = template.replace('{{NAME}}', '')
card = card.replace('{{STREET}}', '')
card = card.replace('{{PLZ_ORT}}', '')
card = card.replace('{{LAND}}', '')
card = card.replace('{{MEDAILLE}}', '')
card = card.replace('{{STARTNR}}', '')
return card
def main(): def main():
csv_file = Path("Export Brevetkarte.csv") parser = argparse.ArgumentParser()
parser.add_argument('--blanko', action='store_true',
help='Generate blank card without participant data')
args = parser.parse_args()
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") backside_template_file = Path("brevetkarte-rueckseite-template.tex")
event_config_file = Path("event.yml") event_config_file = Path("event.yml")
@@ -112,6 +140,24 @@ def main():
template = template_file.read_text(encoding='utf-8') template = template_file.read_text(encoding='utf-8')
template = apply_event_placeholders(template, event_config) template = apply_event_placeholders(template, event_config)
# Split preamble from body so \begin{document} appears only once per file
marker = '\\begin{document}'
doc_idx = template.find(marker)
preamble = template[:doc_idx + len(marker)]
body = template[doc_idx + len(marker):]
if args.blanko:
blanko_output_file = Path("brevetkarte-blanko.tex")
blanko_body = generate_blanko_card(body)
blanko_output = preamble + blanko_body + "\n\\vspace{0.6cm}\n\n" + blanko_body + "\n\\end{document}\n"
blanko_output_file.write_text(blanko_output, encoding='utf-8')
print(f"Generated {blanko_output_file}")
backside_template = backside_template_file.read_text(encoding='utf-8')
backside_output = generate_backside(backside_template, event_config, num_pages=1)
backside_output_file.write_text(backside_output, encoding='utf-8')
print(f"Generated {backside_output_file}")
return
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: with open(csv_file, 'r', encoding='utf-8-sig') as f:
@@ -129,14 +175,14 @@ def main():
# Generate personalized front side # Generate personalized front side
cards = [] cards = []
for participant in participants: for participant in participants:
card = generate_card_from_template(template, participant) card = generate_card_from_template(body, participant)
cards.append(card) cards.append(card)
document_parts = [] document_parts = [preamble]
for i, card in enumerate(cards): for i, card in enumerate(cards):
document_parts.append(card) document_parts.append(card)
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.6cm}\n\n")
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")
@@ -144,10 +190,11 @@ def main():
output_file.write_text(''.join(document_parts), encoding='utf-8') output_file.write_text(''.join(document_parts), encoding='utf-8')
print(f"Generated {output_file}") print(f"Generated {output_file}")
# Generate personalized back side # Generate personalized back side (one page per front page for duplex)
num_pages = (len(participants) + 1) // 2
print(f"Reading back side template from {backside_template_file}...") print(f"Reading back side template from {backside_template_file}...")
backside_template = backside_template_file.read_text(encoding='utf-8') backside_template = backside_template_file.read_text(encoding='utf-8')
backside_output = generate_backside(backside_template, event_config) backside_output = generate_backside(backside_template, event_config, num_pages=num_pages)
backside_output_file.write_text(backside_output, encoding='utf-8') backside_output_file.write_text(backside_output, encoding='utf-8')
print(f"Generated {backside_output_file}") print(f"Generated {backside_output_file}")