LaTeX dynamic building

This commit is contained in:
Frederik Beimgraben 2025-09-01 14:26:38 +02:00
parent 289db0cb02
commit f1d022b19b
17 changed files with 889 additions and 36 deletions

7
.gitignore vendored
View File

@ -102,6 +102,13 @@ uploads/
downloads/ downloads/
cache/ cache/
# LaTeX worktrees
backend/latex-qsm/
backend/latex-vsm/
# Generated PDFs
backend/assets/*.pdf
# Certificates # Certificates
*.pem *.pem
*.key *.key

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "backend/latex-templates"]
path = backend/latex-templates
url = ssh://git@git.beimgraben.net:222/frederik/PA_Vorlage.git

View File

@ -13,6 +13,7 @@ A comprehensive system for managing student parliament (STUPA) funding applicati
- **API Documentation**: Auto-generated OpenAPI/Swagger documentation - **API Documentation**: Auto-generated OpenAPI/Swagger documentation
- **Rate Limiting**: Configurable rate limits for API protection - **Rate Limiting**: Configurable rate limits for API protection
- **Database UI**: Integrated Adminer for database management - **Database UI**: Integrated Adminer for database management
- **LaTeX Integration**: Integrated LaTeX templates for QSM and VSM forms
## 📋 Prerequisites ## 📋 Prerequisites
@ -34,6 +35,9 @@ stupa-pdf-api/
│ │ ├── pdf_to_struct.py # PDF parsing │ │ ├── pdf_to_struct.py # PDF parsing
│ │ └── ... │ │ └── ...
│ ├── assets/ # PDF templates │ ├── assets/ # PDF templates
│ ├── latex-templates/ # LaTeX source (git submodule)
│ ├── latex-qsm/ # QSM LaTeX worktree
│ ├── latex-vsm/ # VSM LaTeX worktree
│ ├── requirements.txt # Python dependencies │ ├── requirements.txt # Python dependencies
│ └── Dockerfile # Backend container definition │ └── Dockerfile # Backend container definition
├── frontend/ # React frontend application ├── frontend/ # React frontend application
@ -129,6 +133,7 @@ Comprehensive documentation is available in the `/docs` directory:
- [Quick Start Guide](./docs/QUICK_START.md) - [Quick Start Guide](./docs/QUICK_START.md)
- [API Reference](./docs/API_REFERENCE.md) - [API Reference](./docs/API_REFERENCE.md)
- [Development Guide](./docs/DEVELOPMENT.md) - [Development Guide](./docs/DEVELOPMENT.md)
- [PDF Integration](./docs/PDF_INTEGRATION.md)
## 🔧 Configuration ## 🔧 Configuration

103
backend/.dockerignore Normal file
View File

@ -0,0 +1,103 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.tox/
.nox/
.hypothesis/
.mypy_cache/
.dmypy.json
dmypy.json
.pyre/
.ruff_cache/
# Virtual environments
venv/
ENV/
env/
.venv/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# Git worktrees (we need the main latex-templates repo, but not the worktrees)
# Note: latex-qsm and latex-vsm are needed for Docker build, so they're not ignored
# LaTeX build artifacts
*.aux
*.log
*.out
*.fdb_latexmk
*.fls
*.synctex.gz
*.bbl
*.blg
*.idx
*.ilg
*.ind
*.toc
*.lot
*.lof
*.nav
*.snm
*.vrb
# Documentation
*.md
docs/
LICENSE
# Development
dev.sh
*.log
# Environment files
.env
.env.*
# Temporary files
tmp/
temp/
*.tmp
*.bak
*.backup
# Test data
tests/
test_*.py
*_test.py
# CI/CD
.github/
.gitlab-ci.yml
.travis.yml
# Assets folder (we'll generate PDFs in Docker, not copy existing ones)
assets/

View File

@ -1,10 +1,41 @@
# ---------- Base ---------- # ---------- LaTeX Builder Stage ----------
FROM texlive/texlive:latest AS latex-builder
# Install additional dependencies for LaTeX
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
inkscape \
make \
fonts-liberation \
fonts-dejavu-core \
&& rm -rf /var/lib/apt/lists/* \
&& inkscape --version
WORKDIR /latex
# Copy the LaTeX source files from local worktrees
COPY latex-qsm /latex/qsm
COPY latex-vsm /latex/vsm
# Build QSM PDF
WORKDIR /latex/qsm
RUN latexmk -xelatex -interaction=nonstopmode -halt-on-error -shell-escape Main.tex && \
cp Main.pdf /latex/qsm.pdf && \
latexmk -c
# Build VSM PDF
WORKDIR /latex/vsm
RUN latexmk -xelatex -interaction=nonstopmode -shell-escape Main.tex && \
cp Main.pdf /latex/vsm.pdf && \
latexmk -c
# ---------- Base Python Stage ----------
FROM python:3.11-slim AS base FROM python:3.11-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 PYTHONUNBUFFERED=1
# System deps (optional: tzdata for correct time) # System deps
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
tzdata ca-certificates \ tzdata ca-certificates \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
@ -16,31 +47,21 @@ COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt RUN pip install --no-cache-dir -r /app/requirements.txt
# ---------- App ---------- # ---------- App ----------
# Struktur-Annahme: # Copy application code
# - src/ (alle .py Module, inkl. service_api.py, pdf_to_struct.py, pdf_filler.py, etc.)
# - assets/ (qsm.pdf, vsm.pdf)
COPY src/ /app/src/ COPY src/ /app/src/
COPY assets/ /app/assets/
# Falls deine Module relative Imports nutzen, src ins PYTHONPATH aufnehmen # Copy pre-built PDFs from latex-builder stage
COPY --from=latex-builder /latex/qsm.pdf /app/assets/qsm.pdf
COPY --from=latex-builder /latex/vsm.pdf /app/assets/vsm.pdf
# Set Python path
ENV PYTHONPATH=/app/src ENV PYTHONPATH=/app/src
# pdf_filler.py sucht standardmäßig assets relativ zum Modul. # Configure PDF template paths
# Wir überschreiben die Template-Pfade per ENV, da die PDFs im Build-Root unter /app/assets liegen.
ENV QSM_TEMPLATE=/app/assets/qsm.pdf \ ENV QSM_TEMPLATE=/app/assets/qsm.pdf \
VSM_TEMPLATE=/app/assets/vsm.pdf VSM_TEMPLATE=/app/assets/vsm.pdf
# Optional: Master-Key / DB-Config kommen zur Laufzeit per -e oder .env (docker run --env-file)
# ENV MASTER_KEY=change_me \
# MYSQL_HOST=mysql \
# MYSQL_PORT=3306 \
# MYSQL_DB=stupa \
# MYSQL_USER=stupa \
# MYSQL_PASSWORD=secret
EXPOSE 8000 EXPOSE 8000
# ---------- Run ---------- # ---------- Run ----------
# Starte die FastAPI
# Hinweis: service_api.py muss in src/ liegen und die App als "app" exportieren.
CMD ["uvicorn", "service_api:app", "--host", "0.0.0.0", "--port", "8000"] CMD ["uvicorn", "service_api:app", "--host", "0.0.0.0", "--port", "8000"]

Binary file not shown.

Binary file not shown.

@ -0,0 +1 @@
Subproject commit b14c230d5a7d550122abf53eca5de15924ab2085

View File

@ -0,0 +1,156 @@
#!/usr/bin/env python3
"""
Make all form fields in LaTeX documents readonly by adding readonly=true attribute.
This script carefully parses LaTeX commands to avoid breaking the syntax.
"""
import re
import sys
from pathlib import Path
def add_readonly_to_textfield(content):
"""Add readonly=true to CustomTextFieldDefault commands."""
# Pattern to match CustomTextFieldDefault{...}{...}{...}{params}
pattern = r'(\\CustomTextFieldDefault\{[^}]*\}\{[^}]*\}\{[^}]*\}\{)([^}]*)\}'
def replacer(match):
prefix = match.group(1)
params = match.group(2)
# Check if readonly is already present
if 'readonly=' in params:
return match.group(0)
# Add readonly=true to the parameters
if params.strip():
new_params = params + ',readonly=true'
else:
new_params = 'readonly=true'
return prefix + new_params + '}'
return re.sub(pattern, replacer, content)
def add_readonly_to_choicemenu(content):
"""Add readonly=true to CustomChoiceMenuDefault commands."""
# Pattern to match CustomChoiceMenuDefault{...}{...}{params}{...}
pattern = r'(\\CustomChoiceMenuDefault\{[^}]*\}\{[^}]*\}\{)([^}]*)\}(\{[^}]*\})'
def replacer(match):
prefix = match.group(1)
params = match.group(2)
suffix = match.group(3)
# Check if readonly is already present
if 'readonly=' in params:
return match.group(0)
# Add readonly=true to the parameters
if params.strip():
new_params = params + ',readonly=true'
else:
new_params = 'readonly=true'
return prefix + new_params + '}' + suffix
return re.sub(pattern, replacer, content)
def add_readonly_to_checkbox(content):
"""Add readonly=true to CheckBox commands."""
# Pattern to match CheckBox[params]
pattern = r'(\\CheckBox\[)([^\]]*)\]'
def replacer(match):
prefix = match.group(1)
params = match.group(2)
# Check if readonly is already present
if 'readonly=' in params:
return match.group(0)
# Add readonly=true to the parameters
params_lines = params.split('\n')
# Find a good place to insert readonly=true (after first parameter)
for i, line in enumerate(params_lines):
if line.strip() and not line.strip().startswith('%'):
# Insert after this line
params_lines.insert(i + 1, '\t\t\t\treadonly=true,')
break
new_params = '\n'.join(params_lines)
return prefix + new_params + ']'
return re.sub(pattern, replacer, content, flags=re.MULTILINE | re.DOTALL)
def add_readonly_to_textfield_multiline(content):
"""Add readonly=true to TextField commands (multiline text fields)."""
# Pattern to match TextField[params] for multiline fields
pattern = r'(\\TextField\[)([^\]]*name=pa-project-description[^\]]*)\]'
def replacer(match):
prefix = match.group(1)
params = match.group(2)
# Check if readonly is already present
if 'readonly=' in params:
return match.group(0)
# Add readonly=true to the parameters
params_lines = params.split('\n')
# Find a good place to insert readonly=true (after multiline parameter)
for i, line in enumerate(params_lines):
if 'multiline' in line:
# Insert after this line
params_lines.insert(i + 1, '\t\t\t\treadonly=true,')
break
new_params = '\n'.join(params_lines)
return prefix + new_params + ']'
return re.sub(pattern, replacer, content, flags=re.MULTILINE | re.DOTALL)
def process_file(filepath):
"""Process a single LaTeX file to make all fields readonly."""
print(f"Processing {filepath}...")
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Apply transformations
content = add_readonly_to_textfield(content)
content = add_readonly_to_choicemenu(content)
content = add_readonly_to_checkbox(content)
content = add_readonly_to_textfield_multiline(content)
# Write back
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f"✓ Processed {filepath}")
def main():
"""Main function to process QSM and VSM LaTeX files."""
base_dir = Path(__file__).parent.parent
files_to_process = [
base_dir / "latex-qsm" / "Content" / "01_content.tex",
base_dir / "latex-vsm" / "Content" / "01_content.tex",
]
for filepath in files_to_process:
if filepath.exists():
process_file(filepath)
else:
print(f"✗ File not found: {filepath}")
if __name__ == "__main__":
main()

View File

@ -22,12 +22,6 @@ TEXT_MAPPING_COMMON: dict = {
}, },
# --- Applicant --- # --- Applicant ---
'pa-applicant-type': {
'required': True,
'target-key': 'pa.applicant.type',
'type': 'enum',
'values': [('person', 'Person'), ('institution', 'Institution')]
},
'pa-institution-type': { 'pa-institution-type': {
'required': True, 'required': True,
'target-key': 'pa.applicant.institution.type', 'target-key': 'pa.applicant.institution.type',
@ -88,8 +82,7 @@ TEXT_MAPPING_COMMON: dict = {
'pa-cost-{a;1:24}-name': {'required': True, 'target-key': 'pa.project.costs[{a}].name', 'type': str}, 'pa-cost-{a;1:24}-name': {'required': True, 'target-key': 'pa.project.costs[{a}].name', 'type': str},
'pa-cost-{a;1:24}-amount-euro': {'required': True, 'target-key': 'pa.project.costs[{a}].amountEur', 'type': float}, 'pa-cost-{a;1:24}-amount-euro': {'required': True, 'target-key': 'pa.project.costs[{a}].amountEur', 'type': float},
# --- Attachments common ---
'pa-anh-vergleichsangebote': {'required': False, 'target-key': 'pa.attachments.comparativeOffers', 'type': bool},
# --- Misc --- # --- Misc ---
'warning-not-supported': {'required': False, 'target-key': 'warning.notSupported', 'type': str}, 'warning-not-supported': {'required': False, 'target-key': 'warning.notSupported', 'type': str},
@ -119,7 +112,7 @@ TEXT_MAPPING_QSM = {
'pa-qsm-individuell': {'required': False, 'target-key': 'pa.project.financing.qsm.flags.individuell', 'type': bool}, 'pa-qsm-individuell': {'required': False, 'target-key': 'pa.project.financing.qsm.flags.individuell', 'type': bool},
'pa-qsm-exkursion-genehmigt': {'required': False, 'target-key': 'pa.project.financing.qsm.flags.exkursionGenehmigt', 'type': bool}, 'pa-qsm-exkursion-genehmigt': {'required': False, 'target-key': 'pa.project.financing.qsm.flags.exkursionGenehmigt', 'type': bool},
'pa-qsm-exkursion-bezuschusst': {'required': False, 'target-key': 'pa.project.financing.qsm.flags.exkursionBezuschusst', 'type': bool}, 'pa-qsm-exkursion-bezuschusst': {'required': False, 'target-key': 'pa.project.financing.qsm.flags.exkursionBezuschusst', 'type': bool},
'pa-anh-fakultaet': {'required': False, 'target-key': 'pa.attachments.fakultaet', 'type': bool},
} }
# --- VSM-specific fields (first variant; include for completeness) --- # --- VSM-specific fields (first variant; include for completeness) ---

View File

@ -160,13 +160,11 @@ def fill_pdf(payload: Dict[str, Any], variant: str, out_path: Optional[str] = No
# Calculate total amount from costs # Calculate total amount from costs
total_amount = 0.0 total_amount = 0.0
costs = flat.get("pa.project.costs", []) # After flattening, costs are in format: pa.project.costs[0].amountEur, pa.project.costs[1].amountEur, etc.
if isinstance(costs, list): for key, value in flat.items():
for cost in costs: if key.startswith("pa.project.costs[") and key.endswith("].amountEur"):
if isinstance(cost, dict) and "amountEur" in cost: if value is not None and isinstance(value, (int, float)):
amount = cost.get("amountEur") total_amount += float(value)
if amount is not None and isinstance(amount, (int, float)):
total_amount += float(amount)
# AcroForm übernehmen + NeedAppearances # AcroForm übernehmen + NeedAppearances
try: try:

View File

@ -73,9 +73,17 @@ services:
- action: sync - action: sync
path: ./backend/assets path: ./backend/assets
target: /app/assets target: /app/assets
- action: sync
path: ./backend/latex-qsm
target: /app/latex-qsm
- action: sync
path: ./backend/latex-vsm
target: /app/latex-vsm
volumes: volumes:
- ./backend/src:/app/src - ./backend/src:/app/src
- ./backend/assets:/app/assets - ./backend/assets:/app/assets
- ./backend/latex-qsm:/app/latex-qsm
- ./backend/latex-vsm:/app/latex-vsm
adminer: adminer:
image: adminer:4 image: adminer:4

260
docs/PDF_INTEGRATION.md Normal file
View File

@ -0,0 +1,260 @@
# PDF Integration Guide
This document describes how the STUPA PDF API integrates with LaTeX templates to generate and process PDF forms.
## Overview
The STUPA PDF API system works with two types of funding application forms:
- **QSM** (Qualitätssicherungsmittel) - Quality assurance funding
- **VSM** (Verfasste Studierendenschaft Mittel) - Student body funding
## LaTeX Templates
### Repository Structure
The LaTeX templates are maintained in a separate Git repository and integrated as a submodule:
- Repository: `git@git.beimgraben.net:frederik/PA_Vorlage.git`
- Location: `/backend/latex-templates/`
### Branch Organization
Different form types are maintained in separate branches:
- `v1.2/QSM` - QSM application forms
- `v1.2/VSM` - VSM application forms
- `v1.2/VGL` - VGL forms (deprecated, not used)
### Working with Templates
The project uses Git worktrees to access multiple template versions simultaneously:
```bash
# Templates are checked out to:
/backend/latex-qsm/ # QSM templates (branch: v1.2/QSM)
/backend/latex-vsm/ # VSM templates (branch: v1.2/VSM)
```
## PDF Generation Workflow
### 1. Template Compilation
LaTeX templates are compiled to PDF using XeLaTeX:
```bash
# Build PDFs from LaTeX sources
./scripts/build-pdfs.sh
```
This script:
- Sets up Git worktrees for QSM and VSM branches
- Compiles LaTeX to PDF using `latexmk -xelatex`
- Copies generated PDFs to `/backend/assets/`
### 2. Form Field Mapping
The system uses pre-compiled PDFs with form fields:
- `/backend/assets/qsm.pdf` - QSM form template
- `/backend/assets/vsm.pdf` - VSM form template
These PDFs contain named form fields that correspond to the application data structure.
### 3. PDF Processing Pipeline
```
User uploads PDF → Parse form data → Store in database → Generate filled PDF
```
## LaTeX Template Structure
### Main Components
```
Main.tex # Main document file
Content/
├── 01_content.tex # Form content and fields
└── 99_glossary.tex # Glossary definitions
HSRTReport/ # Custom document class
TeX/
├── Preamble.tex # Package imports and settings
└── Settings/ # Configuration files
```
### Form Fields
LaTeX form fields are defined using custom commands:
```latex
\CustomTextFieldDefault{pa-project-name}{}{Projektname}{width=\linewidth}
\CustomChoiceMenuDefault{pa-course}{}{width=\linewidth,default=-}{-,INF,ESB,LS,TEC,TEX,NXT}
\CheckBox[name=pa-qsm-studierende,width=1em,height=1em]{}
```
Field naming convention: `pa-` prefix followed by the field identifier.
## Docker Integration
### Build Requirements
The Docker image includes all necessary LaTeX packages:
```dockerfile
RUN apt-get install -y \
texlive-full \
texlive-xetex \
texlive-lang-german \
texlive-fonts-extra \
latexmk \
git
```
### Environment Variables
```env
# PDF template paths
QSM_TEMPLATE=/app/assets/qsm.pdf
VSM_TEMPLATE=/app/assets/vsm.pdf
# LaTeX source paths
LATEX_QSM_PATH=/app/latex-qsm
LATEX_VSM_PATH=/app/latex-vsm
```
## Development Workflow
### 1. Modifying Templates
To modify PDF templates:
1. Navigate to the appropriate worktree:
```bash
cd backend/latex-qsm # or latex-vsm
```
2. Edit the LaTeX files
3. Build the PDF:
```bash
./scripts/build-pdfs.sh
```
4. Test the new PDF with the application
### 2. Adding New Form Fields
1. Add field definition in LaTeX:
```latex
\CustomTextFieldDefault{pa-new-field}{}{Field Label}{width=\linewidth}
```
2. Update the field mapping in `pdf_field_mapping.py`
3. Add corresponding database fields if needed
4. Rebuild the PDF template
### 3. Testing PDF Generation
```bash
# Test PDF generation in Docker
docker compose exec api python -c "
from pdf_filler import fill_pdf
# Test code here
"
```
## Field Mapping Reference
### Common Fields (Both QSM and VSM)
| LaTeX Field | Database Field | Description |
|-------------|----------------|-------------|
| `pa-applicant-type` | `applicantType` | Person or Institution |
| `pa-institution` | `institution` | Institution name |
| `pa-first-name` | `firstName` | Applicant first name |
| `pa-last-name` | `lastName` | Applicant last name |
| `pa-email` | `email` | Contact email |
| `pa-phone` | `phone` | Phone number |
| `pa-project-name` | `name` | Project name |
### QSM-Specific Fields
| LaTeX Field | Database Field | Description |
|-------------|----------------|-------------|
| `pa-qsm-*` | Various | QSM-specific checkboxes |
| `pa-cost-*` | `costs[].name/amountEur` | Cost positions |
### VSM-Specific Fields
| LaTeX Field | Database Field | Description |
|-------------|----------------|-------------|
| `pa-vsm-*` | Various | VSM-specific fields |
| `pa-financing-*` | `financing.*` | Financing options |
## Troubleshooting
### Common Issues
1. **PDF build fails with XeLaTeX error**
- Ensure all LaTeX dependencies are installed
- Check for syntax errors in .tex files
- Verify fonts are available
2. **Form fields not filling**
- Check field names match between LaTeX and mapping
- Verify PDF has form fields (use PDF reader)
- Check data types match expected format
3. **Git worktree errors**
- Remove existing worktrees: `git worktree prune`
- Re-run setup script
### Debugging Commands
```bash
# List form fields in PDF
docker compose exec api python -c "
import PyPDF2
with open('/app/assets/qsm.pdf', 'rb') as f:
pdf = PyPDF2.PdfReader(f)
fields = pdf.get_form_text_fields()
for name, value in fields.items():
print(f'{name}: {value}')
"
# Check LaTeX compilation log
cd backend/latex-qsm
cat Main.log
```
## Best Practices
1. **Version Control**
- Keep LaTeX templates in sync with main repo
- Tag releases when updating PDF templates
- Document field changes in commit messages
2. **Testing**
- Test both empty and filled PDFs
- Verify all form fields are accessible
- Check PDF compatibility across readers
3. **Performance**
- Pre-compile PDFs rather than generating on demand
- Cache compiled PDFs in Docker image
- Minimize LaTeX package dependencies
## Future Enhancements
1. **Dynamic PDF Generation**
- Generate PDFs on-demand from LaTeX
- Support custom form layouts
- Template versioning system
2. **Field Validation**
- Implement LaTeX-side validation
- Sync validation rules with frontend
- Generate field documentation from LaTeX
3. **Multi-language Support**
- Internationalize LaTeX templates
- Support multiple PDF languages
- Dynamic language selection

View File

@ -28,6 +28,7 @@ Welcome to the STUPA PDF API documentation. This directory contains comprehensiv
- **[Attachment Feature](./ATTACHMENT_FEATURE.md)** - File attachment functionality - **[Attachment Feature](./ATTACHMENT_FEATURE.md)** - File attachment functionality
- **[Comparison Offers Feature](./COMPARISON_OFFERS_FEATURE.md)** - Managing comparison offers - **[Comparison Offers Feature](./COMPARISON_OFFERS_FEATURE.md)** - Managing comparison offers
- **[PDF Processing](./PDF_PROCESSING.md)** - PDF generation and parsing - **[PDF Processing](./PDF_PROCESSING.md)** - PDF generation and parsing
- **[PDF Integration](./PDF_INTEGRATION.md)** - LaTeX templates and PDF form integration
### 🚢 Deployment ### 🚢 Deployment

View File

@ -136,7 +136,7 @@ const AdminDashboard: React.FC = () => {
Admin Dashboard Admin Dashboard
</Typography> </Typography>
<Typography variant="body1" color="text.secondary"> <Typography variant="body1" color="text.secondary">
Verwaltung aller Studienanträge Verwaltung aller Projektanträge
</Typography> </Typography>
{/* Messages */} {/* Messages */}

206
scripts/build-pdfs.sh Executable file
View File

@ -0,0 +1,206 @@
#!/bin/bash
# STUPA PDF API - Build PDFs from LaTeX sources
# This script compiles the LaTeX templates into PDF forms
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Project directories
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
BACKEND_DIR="${PROJECT_ROOT}/backend"
ASSETS_DIR="${BACKEND_DIR}/assets"
# LaTeX template directories
LATEX_TEMPLATES_DIR="${BACKEND_DIR}/latex-templates"
LATEX_QSM_DIR="${BACKEND_DIR}/latex-qsm"
LATEX_VSM_DIR="${BACKEND_DIR}/latex-vsm"
# Function to check if required tools are installed
check_requirements() {
echo -e "${BLUE}Checking requirements...${NC}"
local missing=()
# Check for git
if ! command -v git &> /dev/null; then
missing+=("git")
fi
# Check for xelatex
if ! command -v xelatex &> /dev/null; then
missing+=("xelatex (texlive-xetex)")
fi
# Check for latexmk
if ! command -v latexmk &> /dev/null; then
missing+=("latexmk")
fi
if [ ${#missing[@]} -ne 0 ]; then
echo -e "${RED}Missing required tools: ${missing[*]}${NC}"
echo "Please install them first:"
echo " sudo apt-get install git texlive-full texlive-xetex texlive-lang-german texlive-fonts-extra latexmk"
exit 1
fi
echo -e "${GREEN}All requirements satisfied!${NC}"
}
# Function to setup git worktrees
setup_worktrees() {
echo -e "${BLUE}Setting up LaTeX template worktrees...${NC}"
cd "${LATEX_TEMPLATES_DIR}"
# Remove existing worktrees if they exist
if [ -d "${LATEX_QSM_DIR}" ]; then
echo -e "${YELLOW}Removing existing QSM worktree...${NC}"
git worktree remove --force "${LATEX_QSM_DIR}" 2>/dev/null || true
fi
if [ -d "${LATEX_VSM_DIR}" ]; then
echo -e "${YELLOW}Removing existing VSM worktree...${NC}"
git worktree remove --force "${LATEX_VSM_DIR}" 2>/dev/null || true
fi
# Create new worktrees
echo -e "${BLUE}Creating QSM worktree...${NC}"
git worktree add "${LATEX_QSM_DIR}" v1.2/QSM
echo -e "${BLUE}Creating VSM worktree...${NC}"
git worktree add "${LATEX_VSM_DIR}" v1.2/VSM
echo -e "${GREEN}Worktrees created successfully!${NC}"
}
# Function to build a PDF from LaTeX
build_pdf() {
local template_name=$1
local source_dir=$2
local output_name=$3
echo -e "${BLUE}Building ${template_name} PDF...${NC}"
# Change to source directory
cd "${source_dir}"
# Clean previous builds
rm -f *.aux *.log *.out *.fdb_latexmk *.fls *.synctex.gz *.pdf
# Build PDF using latexmk with xelatex
echo -e "${YELLOW}Running latexmk with xelatex...${NC}"
if latexmk -xelatex -interaction=nonstopmode -halt-on-error Main.tex; then
echo -e "${GREEN}Build successful!${NC}"
# Copy to assets directory
if [ -f "Main.pdf" ]; then
cp "Main.pdf" "${ASSETS_DIR}/${output_name}"
echo -e "${GREEN}Copied to ${ASSETS_DIR}/${output_name}${NC}"
else
echo -e "${RED}Error: Main.pdf not found after build${NC}"
return 1
fi
else
echo -e "${RED}Build failed for ${template_name}${NC}"
return 1
fi
# Clean up build artifacts
echo -e "${YELLOW}Cleaning up...${NC}"
rm -f *.aux *.log *.out *.fdb_latexmk *.fls *.synctex.gz
}
# Function to verify PDFs
verify_pdfs() {
echo -e "${BLUE}Verifying generated PDFs...${NC}"
local all_good=true
# Check QSM PDF
if [ -f "${ASSETS_DIR}/qsm.pdf" ]; then
local size=$(stat -c%s "${ASSETS_DIR}/qsm.pdf" 2>/dev/null || stat -f%z "${ASSETS_DIR}/qsm.pdf" 2>/dev/null)
if [ "$size" -gt 10000 ]; then
echo -e "${GREEN}✓ qsm.pdf ($(($size / 1024)) KB)${NC}"
else
echo -e "${RED}✗ qsm.pdf is too small (${size} bytes)${NC}"
all_good=false
fi
else
echo -e "${RED}✗ qsm.pdf not found${NC}"
all_good=false
fi
# Check VSM PDF
if [ -f "${ASSETS_DIR}/vsm.pdf" ]; then
local size=$(stat -c%s "${ASSETS_DIR}/vsm.pdf" 2>/dev/null || stat -f%z "${ASSETS_DIR}/vsm.pdf" 2>/dev/null)
if [ "$size" -gt 10000 ]; then
echo -e "${GREEN}✓ vsm.pdf ($(($size / 1024)) KB)${NC}"
else
echo -e "${RED}✗ vsm.pdf is too small (${size} bytes)${NC}"
all_good=false
fi
else
echo -e "${RED}✗ vsm.pdf not found${NC}"
all_good=false
fi
if [ "$all_good" = true ]; then
echo -e "${GREEN}All PDFs verified successfully!${NC}"
return 0
else
echo -e "${RED}PDF verification failed!${NC}"
return 1
fi
}
# Main execution
main() {
echo -e "${GREEN}STUPA PDF Builder${NC}"
echo "=================="
echo ""
# Check requirements
check_requirements
# Create assets directory if it doesn't exist
mkdir -p "${ASSETS_DIR}"
# Setup worktrees if needed
if [ ! -d "${LATEX_QSM_DIR}" ] || [ ! -d "${LATEX_VSM_DIR}" ]; then
setup_worktrees
fi
# Build QSM PDF
if ! build_pdf "QSM" "${LATEX_QSM_DIR}" "qsm.pdf"; then
echo -e "${RED}Failed to build QSM PDF${NC}"
exit 1
fi
# Build VSM PDF
if ! build_pdf "VSM" "${LATEX_VSM_DIR}" "vsm.pdf"; then
echo -e "${RED}Failed to build VSM PDF${NC}"
exit 1
fi
# Verify results
echo ""
verify_pdfs
echo ""
echo -e "${GREEN}PDF build complete!${NC}"
echo ""
echo "Generated PDFs:"
echo " - ${ASSETS_DIR}/qsm.pdf"
echo " - ${ASSETS_DIR}/vsm.pdf"
}
# Run main function
main "$@"

View File

@ -0,0 +1,91 @@
#!/bin/bash
# STUPA PDF API - Setup LaTeX Worktrees
# This script sets up the Git worktrees needed for building PDFs
set -e
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Project directories
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
BACKEND_DIR="${PROJECT_ROOT}/backend"
# LaTeX directories
LATEX_TEMPLATES_DIR="${BACKEND_DIR}/latex-templates"
LATEX_QSM_DIR="${BACKEND_DIR}/latex-qsm"
LATEX_VSM_DIR="${BACKEND_DIR}/latex-vsm"
echo -e "${GREEN}STUPA PDF API - LaTeX Worktree Setup${NC}"
echo "====================================="
echo ""
# Check if latex-templates submodule is initialized
if [ ! -d "${LATEX_TEMPLATES_DIR}" ] || [ ! -f "${LATEX_TEMPLATES_DIR}/.git" ]; then
echo -e "${YELLOW}Initializing latex-templates submodule...${NC}"
cd "${PROJECT_ROOT}"
git submodule update --init --recursive
fi
# Navigate to latex-templates directory
cd "${LATEX_TEMPLATES_DIR}"
# Fetch latest branches
echo -e "${BLUE}Fetching latest branches...${NC}"
git fetch origin
# Remove existing worktrees if they exist
if [ -d "${LATEX_QSM_DIR}" ]; then
echo -e "${YELLOW}Removing existing QSM worktree...${NC}"
git worktree remove --force "${LATEX_QSM_DIR}" 2>/dev/null || rm -rf "${LATEX_QSM_DIR}"
fi
if [ -d "${LATEX_VSM_DIR}" ]; then
echo -e "${YELLOW}Removing existing VSM worktree...${NC}"
git worktree remove --force "${LATEX_VSM_DIR}" 2>/dev/null || rm -rf "${LATEX_VSM_DIR}"
fi
# Clean up any stale worktree entries
git worktree prune
# Create QSM worktree
echo -e "${BLUE}Creating QSM worktree...${NC}"
git worktree add "${LATEX_QSM_DIR}" origin/v1.2/QSM
# Create VSM worktree
echo -e "${BLUE}Creating VSM worktree...${NC}"
git worktree add "${LATEX_VSM_DIR}" origin/v1.2/VSM
# Verify worktrees
echo ""
echo -e "${GREEN}Verifying worktrees...${NC}"
if [ -f "${LATEX_QSM_DIR}/Main.tex" ]; then
echo -e "${GREEN}✓ QSM worktree created successfully${NC}"
else
echo -e "${RED}✗ QSM worktree creation failed${NC}"
exit 1
fi
if [ -f "${LATEX_VSM_DIR}/Main.tex" ]; then
echo -e "${GREEN}✓ VSM worktree created successfully${NC}"
else
echo -e "${RED}✗ VSM worktree creation failed${NC}"
exit 1
fi
echo ""
echo -e "${GREEN}LaTeX worktrees setup complete!${NC}"
echo ""
echo "Worktree locations:"
echo " QSM: ${LATEX_QSM_DIR}"
echo " VSM: ${LATEX_VSM_DIR}"
echo ""
echo "You can now build the Docker image with:"
echo " docker compose build api"