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.
This commit is contained in:
Peter Adam
2026-02-28 10:43:57 +01:00
parent f59c52551b
commit 9c4c7c6ccb
4 changed files with 156 additions and 143 deletions

View File

@@ -1,7 +1,6 @@
# 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_FRONT := brevetkarte.tex
TEX_FILE_BACK := brevetkarte-rueckseite.tex TEX_FILE_BACK := brevetkarte-rueckseite.tex
TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex TEX_FILE_PERSONALIZED := brevetkarte-personalized.tex
@@ -10,23 +9,20 @@ PDF_FILE_BACK := brevetkarte-rueckseite.pdf
PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf PDF_FILE_PERSONALIZED := brevetkarte-personalized.pdf
CSV_FILE := Export Brevetkarte.csv 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 # Default target
all: build all: build
# Build both PDFs (builds image if needed, then compiles) # Build static demo front side (builds image if needed)
build: build-image build-front build-back 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 both PDFs in Docker container # Compile static demo front side PDF
build-pdf: build-front build-back
# Compile front side PDF
build-front: build-front:
@echo "Compiling front side LaTeX to PDF..." @echo "Compiling front side LaTeX to PDF..."
docker run --rm \ docker run --rm \
@@ -35,8 +31,8 @@ build-front:
pdflatex -interaction=nonstopmode $(TEX_FILE_FRONT) pdflatex -interaction=nonstopmode $(TEX_FILE_FRONT)
@echo "PDF generated: $(PDF_FILE_FRONT)" @echo "PDF generated: $(PDF_FILE_FRONT)"
# Compile back side PDF # Compile back side PDF (always generated from event.yml via generate)
build-back: build-back: build-image
@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 +40,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:
@@ -95,13 +96,12 @@ 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 build - Build Docker image and compile static front side (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 build-front - Compile static demo front side PDF"
@echo " make build-front - Compile front side PDF only" @echo " make generate - Generate tex files from CSV + event.yml"
@echo " make build-back - Compile back side PDF only" @echo " make build-personalized - Generate and compile front + back side PDFs"
@echo " make generate-personalized - Generate personalized cards from CSV" @echo " make build-back - Compile back side PDF (after generate)"
@echo " make build-personalized - Generate and compile personalized cards"
@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"

View File

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

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