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>
This commit is contained in:
Peter Adam
2026-05-05 09:33:04 +02:00
parent 331e6e70a9
commit a2b5c6dc4a
4 changed files with 108 additions and 53 deletions
+22 -2
View File
@@ -2,15 +2,17 @@
IMAGE_NAME := brevetcard-builder IMAGE_NAME := brevetcard-builder
TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex
TEX_FILE_BLANKO := brevetkarte-blanko.tex
TEX_FILE_BACK := brevetkarte-rueckseite.tex TEX_FILE_BACK := brevetkarte-rueckseite.tex
PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf
PDF_FILE_BLANKO := brevetkarte-blanko.pdf
PDF_FILE_BACK := brevetkarte-rueckseite.pdf PDF_FILE_BACK := brevetkarte-rueckseite.pdf
CSV_FILE := export_brevetcard.csv CSV_FILE := export_brevetcard.csv
.PHONY: all build clean build-image build-back generate build-personalized run shell help .PHONY: all build clean build-image build-back generate build-personalized build-blanko run shell help
# Default target # Default target
all: build-personalized all: build-personalized build-blanko
# Build Docker image # Build Docker image
build-image: build-image:
@@ -50,6 +52,23 @@ 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..."
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)"
# Run container interactively # Run container interactively
run: run:
docker run --rm -it \ docker run --rm -it \
@@ -86,6 +105,7 @@ help:
@echo " make build-image - Build Docker image only" @echo " make build-image - Build Docker image only"
@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-blanko - Generate and compile blank card (no CSV needed)"
@echo " make build-back - Compile back side PDF only (after generate)" @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)"
+27 -27
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.4cm,bottom=0.4cm,left=0.4cm,right=0.4cm]{geometry}
\usepackage{array} \usepackage{array}
\usepackage{helvet} \usepackage{helvet}
@@ -18,49 +18,49 @@
% 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{7.0cm}|p{7.0cm}|p{7.0cm}|p{7.0cm}|}
\hline \hline
% Row 1 % Row 1
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_1}}} {{CELL_1_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_2}}} {{CELL_1_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_3}}} {{CELL_1_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_4}}} {{CELL_1_4}}}
\\ \\
\hline \hline
% Row 2 % Row 2
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_1}}} {{CELL_2_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_2}}} {{CELL_2_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_3}}} {{CELL_2_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_4}}} {{CELL_2_4}}}
\\ \\
\hline \hline
% Row 3 % Row 3
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_1}}} {{CELL_3_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_2}}} {{CELL_3_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_3}}} {{CELL_3_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_4}}} {{CELL_3_4}}}
\\ \\
\hline \hline
@@ -70,49 +70,49 @@
% 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{7.0cm}|p{7.0cm}|p{7.0cm}|p{7.0cm}|}
\hline \hline
% Row 1 % Row 1
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_1}}} {{CELL_1_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_2}}} {{CELL_1_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_3}}} {{CELL_1_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_1_4}}} {{CELL_1_4}}}
\\ \\
\hline \hline
% Row 2 % Row 2
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_1}}} {{CELL_2_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_2}}} {{CELL_2_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_3}}} {{CELL_2_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_2_4}}} {{CELL_2_4}}}
\\ \\
\hline \hline
% Row 3 % Row 3
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_1}}} {{CELL_3_1}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_2}}} {{CELL_3_2}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_3}}} {{CELL_3_3}}}
& &
\parbox[c][\rowheight][t]{6.5cm}{% \parbox[c][\rowheight][t]{6.9cm}{%
{{CELL_3_4}}} {{CELL_3_4}}}
\\ \\
\hline \hline
+22 -22
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.4cm,bottom=0.4cm,left=0.4cm,right=0.4cm]{geometry}
\usepackage{graphicx} \usepackage{graphicx}
\usepackage{xcolor} \usepackage{xcolor}
\usepackage{tikz} \usepackage{tikz}
@@ -32,31 +32,31 @@
\begin{tikzpicture}[x=1cm,y=1cm] \begin{tikzpicture}[x=1cm,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,-7.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,-7.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,-7.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,-7.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,-6.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,-7.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 -
+37 -2
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
@@ -92,7 +93,23 @@ 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():
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") 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")
@@ -112,6 +129,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.8cm}\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)
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,10 +164,10 @@ 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: