diff --git a/.gitignore b/.gitignore index 96bfe5b..3be11ac 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ # Rendered PDFs *.pdf +# Generated files +brevetkarte-personalized.tex + # macOS .DS_Store diff --git a/Makefile b/Makefile index 8220941..ab7ec74 100644 --- a/Makefile +++ b/Makefile @@ -4,10 +4,13 @@ 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 PDF_FILE_FRONT := brevetkarte.pdf PDF_FILE_BACK := brevetkarte-rueckseite.pdf +PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf +CSV_FILE := Export Brevetkarte.csv -.PHONY: all build clean build-image build-pdf build-front build-back run shell help +.PHONY: all build clean build-image build-pdf build-front build-back generate-personalized build-personalized run shell help # Default target all: build @@ -41,6 +44,25 @@ build-back: pdflatex -interaction=nonstopmode $(TEX_FILE_BACK) @echo "PDF generated: $(PDF_FILE_BACK)" +# Generate personalized cards from CSV +generate-personalized: + @echo "Generating personalized cards from $(CSV_FILE)..." + @if [ ! -f "$(CSV_FILE)" ]; then \ + echo "Error: $(CSV_FILE) not found!"; \ + exit 1; \ + fi + python3 generate_cards.py + @echo "Generated $(TEX_FILE_PERSONALIZED)" + +# Build personalized cards PDF +build-personalized: generate-personalized build-image + @echo "Compiling personalized cards to PDF..." + docker run --rm \ + -v $(PWD):/workspace \ + $(IMAGE_NAME) \ + pdflatex -interaction=nonstopmode $(TEX_FILE_PERSONALIZED) + @echo "PDF generated: $(PDF_FILE_PERSONALIZED)" + # Run container interactively run: docker run --rm -it \ @@ -73,13 +95,15 @@ help: @echo "Brevet Card PDF Builder" @echo "" @echo "Available targets:" - @echo " make build - Build Docker image and compile both PDFs (default)" - @echo " make build-image - Build Docker image only" - @echo " make build-pdf - Compile both front and back PDFs" - @echo " make build-front - Compile front side PDF only" - @echo " make build-back - Compile back side PDF only" - @echo " make shell - Open interactive shell in container" - @echo " make clean - Remove generated files (aux, log, pdf)" - @echo " make clean-all - Remove all files and Docker image" - @echo " make rebuild - Clean everything and rebuild" - @echo " make help - Show this help message" + @echo " make build - Build Docker image and compile both PDFs (default)" + @echo " make build-image - Build Docker image only" + @echo " make build-pdf - Compile both front and back PDFs" + @echo " make build-front - Compile front side PDF only" + @echo " make build-back - Compile back side PDF only" + @echo " make generate-personalized - Generate personalized cards from CSV" + @echo " make build-personalized - Generate and compile personalized cards" + @echo " make shell - Open interactive shell in container" + @echo " make clean - Remove generated files (aux, log, pdf)" + @echo " make clean-all - Remove all files and Docker image" + @echo " make rebuild - Clean everything and rebuild" + @echo " make help - Show this help message" diff --git a/brevetkarte-template.tex b/brevetkarte-template.tex new file mode 100644 index 0000000..5417697 --- /dev/null +++ b/brevetkarte-template.tex @@ -0,0 +1,126 @@ +\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} +\usepackage{enumitem} + +% 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} + +\begin{document} + +% Brevet card for {{NAME}} (Start #{{STARTNR}}) +\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:} + \begin{itemize}[leftmargin=0.4cm,itemsep=1pt,topsep=2pt,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: {{NAME}}\\[0.4cm] + Straße: {{STREET}}\\[0.4cm] + PLZ/Ort: {{PLZ_ORT}}\\[0.4cm] + Land: {{LAND}}\\[0.4cm] + Medaille: {{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} diff --git a/generate_cards.py b/generate_cards.py new file mode 100755 index 0000000..933bba1 --- /dev/null +++ b/generate_cards.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +Generate personalized brevet cards from CSV data. +""" +import csv +import sys +from pathlib import Path + +def escape_latex(text): + """Escape special LaTeX characters.""" + if not text: + return "" + replacements = { + '&': r'\&', + '%': r'\%', + '$': r'\$', + '#': r'\#', + '_': r'\_', + '{': r'\{', + '}': r'\}', + '~': r'\textasciitilde{}', + '^': r'\textasciicircum{}', + '\\': r'\textbackslash{}', + } + result = str(text) + for char, replacement in replacements.items(): + result = result.replace(char, replacement) + return result + +def generate_card_from_template(template, data): + """Replace placeholders in template with participant data.""" + name = f"{escape_latex(data['Vorname'])} {escape_latex(data['Nachname'])}" + street = escape_latex(data['Straße']) + plz_ort = f"{escape_latex(data['PLZ'])} {escape_latex(data['Ort'])}" + land = escape_latex(data['Land']) + medaille = "Ja" if data['Medaille'].lower() == 'ja' else "Nein" + startnr = escape_latex(data['Startnr']) + + # Replace placeholders + card = template.replace('{{NAME}}', name) + card = card.replace('{{STREET}}', street) + card = card.replace('{{PLZ_ORT}}', plz_ort) + card = card.replace('{{LAND}}', land) + card = card.replace('{{MEDAILLE}}', medaille) + card = card.replace('{{STARTNR}}', startnr) + + return card + +def main(): + csv_file = Path("Export Brevetkarte.csv") + template_file = Path("brevetkarte-template.tex") + output_file = Path("brevetkarte-personalized.tex") + + if not csv_file.exists(): + print(f"Error: {csv_file} not found!", file=sys.stderr) + sys.exit(1) + + if not template_file.exists(): + print(f"Error: {template_file} not found!", file=sys.stderr) + sys.exit(1) + + # Read template + print(f"Reading template from {template_file}...") + template = template_file.read_text(encoding='utf-8') + + # Read CSV data + print(f"Reading participant data from {csv_file}...") + participants = [] + with open(csv_file, 'r', encoding='utf-8-sig') as f: # utf-8-sig handles BOM + reader = csv.DictReader(f) + for row in reader: + if row['Startnr']: # Skip empty rows + participants.append(row) + + if not participants: + print("No participants found in CSV file!", file=sys.stderr) + sys.exit(1) + + print(f"Found {len(participants)} participant(s)") + + # Generate document with all cards + cards = [] + for i, participant in enumerate(participants): + card = generate_card_from_template(template, participant) + cards.append(card) + + # Combine cards with spacing/page breaks + document_parts = [] + for i, card in enumerate(cards): + document_parts.append(card) + + # Add vertical space between cards on same page + if i % 2 == 0 and i < len(cards) - 1: + 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: + document_parts.append("\n\\newpage\n\n") + + # Close document + document_parts.append("\n\\end{document}\n") + + # Write output + document = ''.join(document_parts) + output_file.write_text(document, encoding='utf-8') + + print(f"Generated {output_file}") + print(f"Total pages: {(len(participants) + 1) // 2}") + print(f"Compile with: make build-personalized") + +if __name__ == "__main__": + main()