Zum Hauptinhalt springen

Rendering Architecture

heycreo verwendet zwei Rendering-Systeme: ein Frontend-Renderer für die Live-Vorschau und ein Python-Renderer für den Export.

Übersicht

┌─────────────────────────────────────────────────────────────┐
│ User Input │
│ (Template + Control Values) │
└─────────────────────────┬───────────────────────────────────┘

┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Frontend Renderer │ │ Python Renderer │
│ (React + Canvas) │ │ (PIL + FFmpeg) │
├─────────────────────┤ ├─────────────────────┤
│ • Live-Vorschau │ │ • PNG/JPEG Export │
│ • Interaktiv │ │ • MP4 Video │
│ • Browser-basiert │ │ • Batch-Processing │
└─────────────────────┘ └─────────────────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Browser Display │ │ File Download │
└─────────────────────┘ └─────────────────────┘

Frontend Renderer

Zweck: Live-Vorschau im Editor während der Bearbeitung

Technologien:

  • React Components (TemplatePreview.tsx, DraggableElement.tsx)
  • CSS für Styling und Layout
  • Canvas API für Text-Messung

Dateien:

frontend/src/
├── components/templates/
│ ├── TemplatePreview.tsx # Hauptkomponente
│ └── DraggableElement.tsx # Element-Rendering
├── lib/utils/
│ ├── textAutoResize.ts # Text-Größenberechnung
│ └── containerLayoutEngine.ts # Layout-Berechnung

Vorteile:

  • ✅ Echtzeit-Updates
  • ✅ Native Browser-Rendering (CSS, Fonts)
  • ✅ Interaktiv (Drag & Drop, Editing)

Nachteile:

  • ❌ Nicht für Export geeignet (Screenshot-Qualität)
  • ❌ Browser-abhängig

Python Renderer

Zweck: Hochqualitätiver Export von Bildern und Videos

Technologien:

  • PIL/Pillow für Raster-Grafiken
  • FFmpeg für Video-Encoding
  • Multiprocessing für paralleles Frame-Rendering

Dateien:

renderer/src/
├── core/
│ ├── renderer.py # Hauptrenderer
│ └── video_encoder.py # FFmpeg Wrapper
├── elements/
│ └── renderer.py # Element-Rendering
├── layout/
│ └── engine.py # Container-Layout
├── utils/
│ └── text_utils.py # Shared Text-Utilities
├── animations/
│ ├── easing.py # Easing-Funktionen
│ └── timeline.py # Animation-Timeline
├── effects/
│ ├── gradient.py # Gradient-Rendering
│ └── shadow.py # Schatten-Effekte
└── media/
├── fonts.py # Font-Management
├── font_cache.py # Font-Caching
└── image_cache.py # Image-Caching

Vorteile:

  • ✅ Konsistente Ausgabe (unabhängig vom Browser)
  • ✅ Hohe Qualität (300+ DPI möglich)
  • ✅ Video-Support mit Animationen

Nachteile:

  • ❌ Muss Browser-Verhalten nachbilden
  • ❌ Text-Messung unterscheidet sich leicht

Shared Components

Um konsistentes Verhalten zu gewährleisten, teilen beide Renderer die gleiche Logik:

                    ┌─────────────────────┐
│ TextUtils │
│ (Python + TS) │
├─────────────────────┤
│ • wrap_text() │
│ • check_words_fit() │
│ • calculate_ │
│ optimal_font_size │
└─────────────────────┘

┌───────────────┴───────────────┐
│ │
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Frontend │ │ Python Renderer │
│ textAutoResize.ts │ │ text_utils.py │
└─────────────────────┘ └─────────────────────┘

Rendering Pipeline

1. Template Processing (API)

// api/src/modules/export/template-processor.service.ts

// Control Values anwenden
element.text = controlValue;
element.content = controlValue;

// Visibility Controls
if (visibilityControlValue === false) {
element.hidden = true;
}

2. Layout Calculation

# renderer/src/layout/engine.py

layout_engine = ContainerLayoutEngine(font_manager)
elements = layout_engine.calculate_container_layouts(elements)

3. Element Rendering

# renderer/src/elements/renderer.py

for element in sorted_elements:
if element['type'] == 'text':
self._render_text(draw, canvas, element)
elif element['type'] == 'image':
self._render_image(draw, canvas, element)
elif element['type'] == 'shape':
self._render_shape(draw, canvas, element)

4. Video Encoding

# renderer/src/core/video_encoder.py

# Frames parallel rendern
with Pool(workers) as pool:
frames = pool.map(render_frame, frame_numbers)

# FFmpeg für Encoding
ffmpeg -framerate 30 -i frame_%04d.png -c:v libx264 output.mp4

Browser vs PIL Unterschiede

Text-Messung

AspektBrowserPIL
EngineVaries (Blink, Gecko, WebKit)FreeType
PrecisionSub-pixelInteger
Width~2-3% schmaler~2-3% breiter

Lösung: 2% Toleranz in Word-Fit Checks

tolerance = max_width * 0.02
if word_width > max_width + tolerance:
return False

Descenders (Unterlängen)

Bei line-height: 1 können Buchstaben wie g, p, y abgeschnitten werden:

Browser (CSS):                  PIL (ohne Buffer):
┌─────────────────┐ ┌─────────────────┐
│ Typography │ │ Typograph │ ← 'y' abgeschnitten!
└─────────────────┘ └─────────────────┘

CSS erlaubt
visuelles Overflow

Lösung: Descender-Buffer für Container-Kinder

if is_container_child:
actual_line_box = line_height_px
actual_font_height = ascent + descent
descender_overflow = max(0, actual_font_height - actual_line_box)
padded_height = int(final_height + descender_overflow)

Line-Height

CSS line-height vs PIL:

CSS line-height: 1.2
font-size: 20px
→ Zeilenhöhe: 24px (20 × 1.2)

PIL:
line_height_px = font_size * line_height
total_height = num_lines * line_height_px

Performance-Optimierungen

Font Caching

# renderer/src/media/font_cache.py
class FontCache:
def get_font(self, family, size, weight, style):
cache_key = f"{family}-{size}-{weight}-{style}"
if cache_key not in self.cache:
self.cache[cache_key] = load_font(...)
return self.cache[cache_key]

Image Caching

# renderer/src/media/image_cache.py
class ImageCache:
def get_image(self, url):
if url not in self.cache:
self.cache[url] = download_and_load(url)
return self.cache[url]

Parallel Frame Rendering

# Für Videos: Frames parallel rendern
from multiprocessing import Pool

with Pool(processes=4) as pool:
frames = pool.map(render_frame, range(total_frames))

Debugging

Frontend → Renderer Vergleich

  1. Screenshot im Frontend machen
  2. Export im Renderer machen
  3. Visuell vergleichen:
    • Text-Größe identisch?
    • Positionen identisch?
    • Farben identisch?

Debug-Logging

# In renderer/src/elements/renderer.py
print(f"Text '{text[:20]}': font_size={font_size}, lines={len(lines)}")
print(f"Position: x={x}, y={y}, width={width}, height={height}")

Common Issues

SymptomUrsacheLösung
Text zu kleinPIL misst breiter2% Toleranz prüfen
Text abgeschnittenDescendersBuffer hinzufügen
Falsche PositionLayout nicht berechnetcalculate_container_layouts() aufrufen
Schrift falschFont nicht gefundenFont-Pfade prüfen

Siehe auch