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:
@@ -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()
|
||||
Reference in New Issue
Block a user