"""ImageMagick slide renderer for presentation videos.""" import logging import os import subprocess import textwrap logger = logging.getLogger(__name__) # Slide dimensions WIDTH = 1920 HEIGHT = 1080 HEADER_HEIGHT = 120 FOOTER_HEIGHT = 60 FONT = "DejaVu-Sans" FONT_BOLD = "DejaVu-Sans-Bold" def render_slide( heading: str, text: str, bullet_points: list[str], slide_number: int, total_slides: int, module_code: str, output_path: str, ) -> None: """Render a single slide as PNG using ImageMagick.""" cmd = [ "convert", "-size", f"{WIDTH}x{HEIGHT}", "xc:white", # Blue header bar "-fill", "#1e3a5f", "-draw", f"rectangle 0,0 {WIDTH},{HEADER_HEIGHT}", # Header text "-fill", "white", "-font", FONT_BOLD, "-pointsize", "42", "-gravity", "NorthWest", "-annotate", f"+60+{(HEADER_HEIGHT - 42) // 2}", heading[:80], ] y_pos = HEADER_HEIGHT + 40 # Main text if text: wrapped = textwrap.fill(text, width=80) for line in wrapped.split("\n")[:6]: cmd.extend([ "-fill", "#333333", "-font", FONT, "-pointsize", "28", "-gravity", "NorthWest", "-annotate", f"+60+{y_pos}", line, ]) y_pos += 38 y_pos += 20 # Bullet points for bp in bullet_points[:8]: wrapped_bp = textwrap.fill(bp, width=75) first_line = True for line in wrapped_bp.split("\n"): prefix = " • " if first_line else " " cmd.extend([ "-fill", "#444444", "-font", FONT, "-pointsize", "26", "-gravity", "NorthWest", "-annotate", f"+60+{y_pos}", f"{prefix}{line}", ]) y_pos += 34 first_line = False y_pos += 8 # Footer bar cmd.extend([ "-fill", "#f0f0f0", "-draw", f"rectangle 0,{HEIGHT - FOOTER_HEIGHT} {WIDTH},{HEIGHT}", "-fill", "#888888", "-font", FONT, "-pointsize", "20", "-gravity", "SouthWest", "-annotate", f"+60+{(FOOTER_HEIGHT - 20) // 2}", f"{module_code}", "-gravity", "SouthEast", "-annotate", f"+60+{(FOOTER_HEIGHT - 20) // 2}", f"Folie {slide_number}/{total_slides}", ]) cmd.append(output_path) result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: raise RuntimeError(f"ImageMagick failed: {result.stderr}") def render_title_slide( title: str, subtitle: str, output_path: str, ) -> None: """Render a title slide.""" cmd = [ "convert", "-size", f"{WIDTH}x{HEIGHT}", "xc:white", # Full blue background "-fill", "#1e3a5f", "-draw", f"rectangle 0,0 {WIDTH},{HEIGHT}", # Title "-fill", "white", "-font", FONT_BOLD, "-pointsize", "56", "-gravity", "Center", "-annotate", "+0-60", title[:60], # Subtitle "-fill", "#b0c4de", "-font", FONT, "-pointsize", "32", "-gravity", "Center", "-annotate", "+0+40", subtitle[:80], # Footer "-fill", "#6688aa", "-pointsize", "22", "-gravity", "South", "-annotate", "+0+30", "BreakPilot Compliance Training", output_path, ] result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: raise RuntimeError(f"ImageMagick title slide failed: {result.stderr}") def render_checkpoint_slide( title: str, question_preview: str, question_count: int, output_path: str, ) -> None: """Render a checkpoint slide with red border and quiz preview.""" border_width = 12 cmd = [ "convert", "-size", f"{WIDTH}x{HEIGHT}", "xc:white", # Red border (full rectangle, then white inner) "-fill", "#c0392b", "-draw", f"rectangle 0,0 {WIDTH},{HEIGHT}", "-fill", "white", "-draw", f"rectangle {border_width},{border_width} {WIDTH - border_width},{HEIGHT - border_width}", # Red header bar "-fill", "#c0392b", "-draw", f"rectangle {border_width},{border_width} {WIDTH - border_width},{HEADER_HEIGHT + border_width}", # CHECKPOINT label "-fill", "white", "-font", FONT_BOLD, "-pointsize", "48", "-gravity", "NorthWest", "-annotate", f"+{60 + border_width}+{(HEADER_HEIGHT - 48) // 2 + border_width}", f"CHECKPOINT: {title[:50]}", ] y_pos = HEADER_HEIGHT + border_width + 60 # Instruction text cmd.extend([ "-fill", "#333333", "-font", FONT, "-pointsize", "32", "-gravity", "NorthWest", "-annotate", f"+80+{y_pos}", "Bitte beantworten Sie die folgenden Fragen,", ]) y_pos += 44 cmd.extend([ "-fill", "#333333", "-font", FONT, "-pointsize", "32", "-gravity", "NorthWest", "-annotate", f"+80+{y_pos}", "um mit der Schulung fortzufahren.", ]) y_pos += 80 # Question preview if question_preview: preview = textwrap.fill(question_preview, width=70) cmd.extend([ "-fill", "#666666", "-font", FONT, "-pointsize", "26", "-gravity", "NorthWest", "-annotate", f"+80+{y_pos}", f"Erste Frage: {preview[:120]}...", ]) y_pos += 50 # Question count cmd.extend([ "-fill", "#888888", "-font", FONT, "-pointsize", "24", "-gravity", "NorthWest", "-annotate", f"+80+{y_pos}", f"{question_count} Fragen in diesem Checkpoint", ]) # Footer cmd.extend([ "-fill", "#f0f0f0", "-draw", f"rectangle {border_width},{HEIGHT - FOOTER_HEIGHT - border_width} {WIDTH - border_width},{HEIGHT - border_width}", "-fill", "#c0392b", "-font", FONT_BOLD, "-pointsize", "22", "-gravity", "South", "-annotate", f"+0+{(FOOTER_HEIGHT - 22) // 2 + border_width}", "Video wird pausiert — Quiz im Player beantworten", ]) cmd.append(output_path) result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: raise RuntimeError(f"ImageMagick checkpoint slide failed: {result.stderr}")