> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/coleam00/excalidraw-diagram-skill/llms.txt
> Use this file to discover all available pages before exploring further.

# Render Pipeline

> How the Python render script converts Excalidraw JSON to PNG using Playwright and headless Chromium

## Overview

The render pipeline converts `.excalidraw` JSON files into high-quality PNG images using a Python script that orchestrates Playwright, headless Chromium, and the Excalidraw library.

**Location:** `references/render_excalidraw.py`

## Quick Start

### First-Time Setup

```bash theme={null}
cd .claude/skills/excalidraw-diagram/references
uv sync
uv run playwright install chromium
```

### Basic Usage

```bash theme={null}
cd .claude/skills/excalidraw-diagram/references
uv run python render_excalidraw.py <path-to-file.excalidraw>
```

This creates `<filename>.png` next to the source `.excalidraw` file.

### Advanced Options

```bash theme={null}
uv run python render_excalidraw.py diagram.excalidraw \
  --output custom-name.png \
  --scale 2 \
  --width 1920
```

<ParamField path="input" type="path" required>
  Path to the `.excalidraw` JSON file to render
</ParamField>

<ParamField path="--output, -o" type="path">
  Output PNG path (default: same name as input with `.png` extension)
</ParamField>

<ParamField path="--scale, -s" type="int" default="2">
  Device scale factor for high-DPI rendering (2 = retina quality)
</ParamField>

<ParamField path="--width, -w" type="int" default="1920">
  Maximum viewport width in pixels
</ParamField>

## Architecture

### Pipeline Stages

```
JSON File → Validation → Bounding Box → Viewport Setup → Browser Render → Screenshot → PNG
```

### 1. Validation

The script validates the Excalidraw JSON structure before attempting to render:

```python references/render_excalidraw.py theme={null}
def validate_excalidraw(data: dict) -> list[str]:
    """Validate Excalidraw JSON structure. Returns list of errors (empty = valid)."""
    errors: list[str] = []

    if data.get("type") != "excalidraw":
        errors.append(f"Expected type 'excalidraw', got '{data.get('type')}'")

    if "elements" not in data:
        errors.append("Missing 'elements' array")
    elif not isinstance(data["elements"], list):
        errors.append("'elements' must be an array")
    elif len(data["elements"]) == 0:
        errors.append("'elements' array is empty — nothing to render")

    return errors
```

**What it checks:**

* `type` field must be `"excalidraw"`
* `elements` array must exist and be a valid array
* At least one element must be present (no empty diagrams)

### 2. Bounding Box Calculation

The script computes the smallest rectangle that contains all diagram elements:

```python references/render_excalidraw.py theme={null}
def compute_bounding_box(elements: list[dict]) -> tuple[float, float, float, float]:
    """Compute bounding box (min_x, min_y, max_x, max_y) across all elements."""
    min_x = float("inf")
    min_y = float("inf")
    max_x = float("-inf")
    max_y = float("-inf")

    for el in elements:
        if el.get("isDeleted"):
            continue
        x = el.get("x", 0)
        y = el.get("y", 0)
        w = el.get("width", 0)
        h = el.get("height", 0)

        # For arrows/lines, points array defines the shape relative to x,y
        if el.get("type") in ("arrow", "line") and "points" in el:
            for px, py in el["points"]:
                min_x = min(min_x, x + px)
                min_y = min(min_y, y + py)
                max_x = max(max_x, x + px)
                max_y = max(max_y, y + py)
        else:
            min_x = min(min_x, x)
            min_y = min(min_y, y)
            max_x = max(max_x, x + abs(w))
            max_y = max(max_y, y + abs(h))

    if min_x == float("inf"):
        return (0, 0, 800, 600)

    return (min_x, min_y, max_x, max_y)
```

**Special handling:**

* Skips deleted elements (`isDeleted: true`)
* Handles arrows/lines by iterating through their `points` array
* Adds 80px padding on all sides
* Falls back to 800×600 if no elements found

### 3. Viewport Configuration

```python references/render_excalidraw.py theme={null}
min_x, min_y, max_x, max_y = compute_bounding_box(elements)
padding = 80
diagram_w = max_x - min_x + padding * 2
diagram_h = max_y - min_y + padding * 2

# Cap viewport width, let height be natural
vp_width = min(int(diagram_w), max_width)
vp_height = max(int(diagram_h), 600)
```

**Sizing logic:**

* Calculates natural diagram dimensions from bounding box
* Adds 80px padding on all sides
* Caps width at `max_width` (default 1920px)
* Ensures minimum height of 600px

### 4. Browser Rendering

The script uses Playwright to control a headless Chromium browser:

```python references/render_excalidraw.py theme={null}
with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page(
        viewport={"width": vp_width, "height": vp_height},
        device_scale_factor=scale,
    )

    # Load the template
    page.goto(template_url)

    # Wait for the ES module to load (imports from esm.sh)
    page.wait_for_function("window.__moduleReady === true", timeout=30000)

    # Inject the diagram data and render
    json_str = json.dumps(data)
    result = page.evaluate(f"window.renderDiagram({json_str})")

    # Wait for render completion signal
    page.wait_for_function("window.__renderComplete === true", timeout=15000)

    # Screenshot the SVG element
    svg_el = page.query_selector("#root svg")
    svg_el.screenshot(path=str(output_path))
    browser.close()
```

**Render sequence:**

1. Launch headless Chromium with specified viewport and scale
2. Load `render_template.html` from the same directory
3. Wait for Excalidraw library to load from esm.sh CDN
4. Call `window.renderDiagram()` with the JSON data
5. Wait for render completion signal
6. Screenshot the SVG element (not the whole page)

### 5. HTML Template

The template (`render_template.html`) provides a minimal HTML page that loads Excalidraw:

```html references/render_template.html theme={null}
<script type="module">
  import { exportToSvg } from "https://esm.sh/@excalidraw/excalidraw?bundle";

  window.renderDiagram = async function(jsonData) {
    try {
      const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
      const elements = data.elements || [];
      const appState = data.appState || {};
      const files = data.files || {};

      // Force white background in appState
      appState.viewBackgroundColor = appState.viewBackgroundColor || "#ffffff";
      appState.exportWithDarkMode = false;

      const svg = await exportToSvg({
        elements: elements,
        appState: {
          ...appState,
          exportBackground: true,
        },
        files: files,
      });

      // Clear any previous render
      const root = document.getElementById("root");
      root.innerHTML = "";
      root.appendChild(svg);

      window.__renderComplete = true;
      window.__renderError = null;
      return { success: true, width: svg.getAttribute("width"), height: svg.getAttribute("height") };
    } catch (err) {
      window.__renderComplete = true;
      window.__renderError = err.message;
      return { success: false, error: err.message };
    }
  };

  // Signal that the module is loaded and ready
  window.__moduleReady = true;
</script>
```

**Key features:**

* Loads `@excalidraw/excalidraw` from esm.sh CDN (no build step)
* Forces white background for consistent exports
* Sets coordination flags (`__moduleReady`, `__renderComplete`) for Python to await
* Returns structured success/error response

## Dependencies

From `pyproject.toml`:

```toml references/pyproject.toml theme={null}
[project]
name = "excalidraw-render"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "playwright>=1.40.0",
]
```

**Runtime dependencies:**

* Python 3.11+
* Playwright Python library
* Chromium browser (installed via `playwright install chromium`)
* Internet connection (to load Excalidraw library from esm.sh)

## Error Handling

The script provides clear error messages for common failure modes:

### Invalid JSON

```
ERROR: Invalid JSON in diagram.excalidraw: Expecting ',' delimiter: line 42 column 5 (char 1234)
```

### Invalid Excalidraw Structure

```
ERROR: Invalid Excalidraw file:
  - Expected type 'excalidraw', got 'null'
  - Missing 'elements' array
```

### Playwright Not Installed

```
ERROR: playwright not installed.
Run: cd .claude/skills/excalidraw-diagram/references && uv sync && uv run playwright install chromium
```

### Chromium Not Installed

```
ERROR: Chromium not installed for Playwright.
Run: cd .claude/skills/excalidraw-diagram/references && uv run playwright install chromium
```

### Render Failure

```
ERROR: Render failed: Cannot read property 'x' of undefined
```

### Missing SVG Element

```
ERROR: No SVG element found after render.
```

## Output Format

On success, the script prints the output path to stdout:

```
/path/to/diagram.png
```

This allows the render command to be used in scripts:

```bash theme={null}
PNG_PATH=$(uv run python render_excalidraw.py diagram.excalidraw)
echo "Rendered to: $PNG_PATH"
```

## Performance Characteristics

* **Cold start:** \~3-5 seconds (browser launch + library load)
* **Warm render:** \~1-2 seconds (subsequent renders in same process)
* **Memory:** \~150-300 MB per browser instance
* **Network:** Downloads Excalidraw library (\~500KB) on first page load

## Troubleshooting

<AccordionGroup>
  <Accordion title="Script hangs at 'waiting for module'">
    The template can't load the Excalidraw library from esm.sh. Check your internet connection or firewall settings.
  </Accordion>

  <Accordion title="Rendered image is clipped or cut off">
    The bounding box calculation may not account for all elements. Try increasing the `--width` parameter or checking for elements with unusual coordinate values.
  </Accordion>

  <Accordion title="Text looks blurry">
    Increase the `--scale` parameter. Default is 2 (retina quality). Try `--scale 3` for even sharper text.
  </Accordion>

  <Accordion title="Colors look different than in Excalidraw editor">
    The render uses Excalidraw's `exportToSvg` function, which may render colors slightly differently than the interactive editor. This is expected behavior.
  </Accordion>
</AccordionGroup>

## Integration with Workflow

The render pipeline is a mandatory step in the diagram creation workflow (see [Quality Checklist](/reference/quality-checklist)):

```
Generate JSON → Render to PNG → View PNG → Identify Issues → Fix JSON → Re-render → Repeat
```

See the **Render & Validate** section in `SKILL.md` for the complete validation loop.
