From 9c4c7c6ccbaaabd1024a865a46b2ff1ca9302c4c Mon Sep 17 00:00:00 2001 From: Peter Adam Date: Sat, 28 Feb 2026 10:43:57 +0100 Subject: [PATCH] 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. --- Makefile | 58 ++++++++--------- brevetkarte-rueckseite.tex | 125 ++++++++++++++----------------------- brevetkarte-template.tex | 15 +++-- generate_cards.py | 101 ++++++++++++++++++++++-------- 4 files changed, 156 insertions(+), 143 deletions(-) diff --git a/Makefile b/Makefile index ab7ec74..1de6b78 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,6 @@ # Makefile for building Brevet card PDF in Docker 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 @@ -10,23 +9,20 @@ 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 generate-personalized build-personalized run shell help +.PHONY: all build clean build-image build-front build-back generate build-personalized run shell help # Default target all: build -# Build both PDFs (builds image if needed, then compiles) -build: build-image build-front build-back +# Build static demo front side (builds image if needed) +build: build-image build-front # Build Docker image build-image: @echo "Building Docker image..." docker build -t $(IMAGE_NAME) . -# Compile both PDFs in Docker container -build-pdf: build-front build-back - -# Compile front side PDF +# Compile static demo front side PDF build-front: @echo "Compiling front side LaTeX to PDF..." docker run --rm \ @@ -35,8 +31,8 @@ build-front: pdflatex -interaction=nonstopmode $(TEX_FILE_FRONT) @echo "PDF generated: $(PDF_FILE_FRONT)" -# Compile back side PDF -build-back: +# Compile back side PDF (always generated from event.yml via generate) +build-back: build-image @echo "Compiling back side LaTeX to PDF..." docker run --rm \ -v $(PWD):/workspace \ @@ -44,24 +40,29 @@ 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)..." +# Generate all tex files from CSV + event.yml +generate: + @echo "Generating cards from $(CSV_FILE) + event.yml..." @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..." +# Build personalized front + event back side PDFs +build-personalized: generate build-image + @echo "Compiling personalized front side to PDF..." docker run --rm \ -v $(PWD):/workspace \ $(IMAGE_NAME) \ pdflatex -interaction=nonstopmode $(TEX_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: @@ -95,15 +96,14 @@ 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 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" + @echo " make build - Build Docker image and compile static front side (default)" + @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 build-personalized - Generate and compile front + back side PDFs" + @echo " make build-back - Compile back side PDF (after generate)" + @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-rueckseite.tex b/brevetkarte-rueckseite.tex index 48fd574..40947ad 100644 --- a/brevetkarte-rueckseite.tex +++ b/brevetkarte-rueckseite.tex @@ -12,15 +12,6 @@ \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} @@ -29,91 +20,79 @@ \noindent \begin{tabular}{|p{6.6cm}|p{6.6cm}|p{6.6cm}|p{6.6cm}|} \hline -% Row 1 (Upper card) - height 2.833cm +% Row 1 \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 1:} Km 0 - Unisport\\ +\textbf{Nr. 1:} Km 0 -- Unisport\\ Nachtigallenweg 86, Bonn\\ \\ \textbf{Kontrollzeit}\\ von: 7:30\\ -bis: 8:30 -} +bis: 8:30} & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty for stamps +\parbox[c][\rowheight][t]{6.5cm}{% } & \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 4:} Km 165 Mahlberg Ecke K50,\\ +\textbf{Nr. 4:} Km 165 -- Mahlberg Ecke K50,\\ Breitestraße\\ \textbf{Kontrollzeit}\\ von: 13:21\\ -bis: 19:30 -} +bis: 19:30} & \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} \textbf{Kontrollfrage:}\\ Wann wurde das Kriegerdenkmal\\ -eingerichtet? -} +eingerichtet?} \\ \hline -% Row 2 (Upper card) - height 2.833cm +% Row 2 \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 2:} Km 57 -- „Nationalpark-Tor" im\\ +\textbf{Nr. 2:} Km 57 -- ,,Nationalpark-Tor`` im\\ alten Bahnhofsgebäude, Heimbach\\ \\ \textbf{Kontrollzeit}\\ von: 9:11\\ -bis: 12:21 -} +bis: 12:21} & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty for stamps +\parbox[c][\rowheight][t]{6.5cm}{% } & \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 5:} Km 205 - Unisport\\ +\textbf{Nr. 5:} Km 205 -- Unisport\\ Nachtigallenweg 86, Bonn\\ \\ \textbf{Kontrollzeit}\\ von: 13:23\\ -bis: 21:00 -} +bis: 21:00} & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty +\parbox[c][\rowheight][t]{6.5cm}{% } \\ \hline -% Row 3 (Upper card) - height 2.833cm +% Row 3 \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 3:} Km 100 - Friterie „Au Petit\\ -Creux" oder Total-Tankstelle, Ecke Rue\\ +\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 +bis: 15:10} +& +\parbox[c][\rowheight][t]{6.5cm}{% } & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty for stamps +\parbox[c][\rowheight][t]{6.5cm}{% } & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty -} -& -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty +\parbox[c][\rowheight][t]{6.5cm}{% } \\ \hline @@ -121,98 +100,86 @@ bis: 15:10 \vspace{1.8cm} -% Lower card table (rows 4-6) +% 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 (Lower card) +% Row 1 \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 1:} Km 0 - Unisport\\ +\textbf{Nr. 1:} Km 0 -- Unisport\\ Nachtigallenweg 86, Bonn\\ \\ \textbf{Kontrollzeit}\\ von: 7:30\\ -bis: 8:30 -} +bis: 8:30} & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty for stamps +\parbox[c][\rowheight][t]{6.5cm}{% } & \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 4:} Km 165 Mahlberg Ecke K50,\\ +\textbf{Nr. 4:} Km 165 -- Mahlberg Ecke K50,\\ Breitestraße\\ \textbf{Kontrollzeit}\\ von: 13:21\\ -bis: 19:30 -} +bis: 19:30} & \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} \textbf{Kontrollfrage:}\\ Wann wurde das Kriegerdenkmal\\ -eingerichtet? -} +eingerichtet?} \\ \hline -% Row 2 (Lower card) - height 2.833cm +% Row 2 \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 2:} Km 57 -- „Nationalpark-Tor" im\\ +\textbf{Nr. 2:} Km 57 -- ,,Nationalpark-Tor`` im\\ alten Bahnhofsgebäude, Heimbach\\ \\ \textbf{Kontrollzeit}\\ von: 9:11\\ -bis: 12:21 -} +bis: 12:21} & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty for stamps +\parbox[c][\rowheight][t]{6.5cm}{% } & \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 5:} Km 214 - Unisport\\ +\textbf{Nr. 5:} Km 205 -- Unisport\\ Nachtigallenweg 86, Bonn\\ \\ \textbf{Kontrollzeit}\\ von: 13:23\\ -bis: 21:00 -} +bis: 21:00} & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty +\parbox[c][\rowheight][t]{6.5cm}{% } \\ \hline -% Row 3 (Lower card) - height 2.833cm +% Row 3 \parbox[c][\rowheight][t]{6.5cm}{% \vspace{2mm} -\textbf{Nr. 3:} Km 100 - Friterie „Au Petit\\ -Creux" oder Total-Tankstelle, Ecke Rue\\ +\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 +bis: 15:10} +& +\parbox[c][\rowheight][t]{6.5cm}{% } & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty for stamps +\parbox[c][\rowheight][t]{6.5cm}{% } & -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty -} -& -\parbox[c][\rowheight][c]{6.5cm}{% -% Empty +\parbox[c][\rowheight][t]{6.5cm}{% } \\ \hline \end{tabular} -\end{document} +\end{document} \ No newline at end of file diff --git a/brevetkarte-template.tex b/brevetkarte-template.tex index 32f9a83..3e7cd43 100644 --- a/brevetkarte-template.tex +++ b/brevetkarte-template.tex @@ -71,7 +71,6 @@ \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) { @@ -101,7 +100,7 @@ PLZ/Ort: {{PLZ_ORT}}\\[0.4cm] Land: {{LAND}}\\[0.4cm] Medaille: {{MEDAILLE}}\\[0.6cm] - Startzeit: 8:30 + Startzeit: {{EVENT_STARTZEIT}} }; % 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) { - \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} + \textbf{{{EVENT_TITLE}}}\\ + Randonnée über \textbf{{{EVENT_KM}}} km\\ + am \textbf{{{EVENT_DATE}}}\\ + mit Start in \textbf{{{EVENT_START}}}\\ + von \textbf{{{EVENT_CLUB}}}\\ + N° ACP du Club \textbf{{{EVENT_CLUB_NR}}} }; \node[anchor=south,text width=6.6cm,font=\scriptsize,align=center] at (24.15,-7.0) { diff --git a/generate_cards.py b/generate_cards.py index 933bba1..7853692 100755 --- a/generate_cards.py +++ b/generate_cards.py @@ -1,11 +1,18 @@ #!/usr/bin/env python3 """ -Generate personalized brevet cards from CSV data. +Generate personalized brevet cards from CSV and event config. """ import csv import sys 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): """Escape special LaTeX characters.""" if not text: @@ -27,8 +34,47 @@ def escape_latex(text): result = result.replace(char, replacement) 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): - """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'])}" street = escape_latex(data['Straße']) 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" startnr = escape_latex(data['Startnr']) - # Replace placeholders card = template.replace('{{NAME}}', name) card = card.replace('{{STREET}}', street) card = card.replace('{{PLZ_ORT}}', plz_ort) @@ -46,30 +91,33 @@ def generate_card_from_template(template, data): return card + def main(): csv_file = Path("Export Brevetkarte.csv") 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") + backside_output_file = Path("brevetkarte-rueckseite.tex") - if not csv_file.exists(): - print(f"Error: {csv_file} not found!", file=sys.stderr) - sys.exit(1) + for f in [csv_file, template_file, backside_template_file, event_config_file]: + if not f.exists(): + print(f"Error: {f} 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) + print(f"Reading event config from {event_config_file}...") + event_config = load_event_config(event_config_file) - # Read template print(f"Reading template from {template_file}...") 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}...") 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) for row in reader: - if row['Startnr']: # Skip empty rows + if row['Startnr']: participants.append(row) if not participants: @@ -78,35 +126,34 @@ def main(): print(f"Found {len(participants)} participant(s)") - # Generate document with all cards + # Generate personalized front side cards = [] - for i, participant in enumerate(participants): + for participant in 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') - + output_file.write_text(''.join(document_parts), encoding='utf-8') 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"Compile with: make build-personalized") + if __name__ == "__main__": - main() + main() \ No newline at end of file