Files
brevetcard/generate_cards.py
T
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

194 lines
6.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Generate personalized brevet cards from CSV and event config.
"""
import argparse
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:
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 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 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'])}"
land = escape_latex(data['Land'])
medaille = "Ja" if data['Medaille'].lower() == 'ja' else "Nein"
startnr = escape_latex(data['Startnr'])
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 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():
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")
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")
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)
print(f"Reading event config from {event_config_file}...")
event_config = load_event_config(event_config_file)
print(f"Reading template from {template_file}...")
template = template_file.read_text(encoding='utf-8')
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)
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}...")
participants = []
with open(csv_file, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f)
for row in reader:
if row['Startnr']:
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 personalized front side
cards = []
for participant in participants:
card = generate_card_from_template(body, participant)
cards.append(card)
document_parts = [preamble]
for i, card in enumerate(cards):
document_parts.append(card)
if i % 2 == 0 and i < len(cards) - 1:
document_parts.append("\n\\vspace{0.6cm}\n\n")
if i % 2 == 1 and i < len(cards) - 1:
document_parts.append("\n\\newpage\n\n")
document_parts.append("\n\\end{document}\n")
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()