# PlantUML Rendering Implementation Guide

## Overview

This guide documents how to implement PlantUML diagram rendering in web applications. PlantUML is a powerful tool for generating UML diagrams from plain text descriptions. This implementation was successfully used in the Database Structure visualization feature.

## What is PlantUML?

**PlantUML** is a component that allows you to create diagrams from plain text using a simple and intuitive language. It supports:
- Class diagrams
- Sequence diagrams
- Use case diagrams
- Entity-relationship diagrams (ERD)
- Activity diagrams
- Component diagrams
- State diagrams
- And many more...

## Architecture Overview

There are three main approaches to rendering PlantUML diagrams:

### 1. Public PlantUML Server (Recommended for Prototypes)
- **Pros**: No server setup, works immediately, always up-to-date
- **Cons**: Requires internet connection, potential privacy concerns, rate limiting
- **URL**: `https://www.plantuml.com/plantuml/`

### 2. Self-Hosted PlantUML Server
- **Pros**: Full control, privacy, no rate limits
- **Cons**: Requires Java, GraphViz, and server maintenance
- **Setup**: Docker image available: `plantuml/plantuml-server`

### 3. Local PlantUML CLI
- **Pros**: Complete offline functionality, no external dependencies
- **Cons**: Requires Java and GraphViz installation, more complex integration
- **Setup**: Download JAR from plantuml.com

## Implementation: Client-Side Rendering (Used in This Project)

### Required Components

#### 1. JavaScript Compression Library
PlantUML requires proper DEFLATE compression. Use **pako.js**:

```html
<!-- Include pako for deflate compression -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
```

**Why pako?**
- Provides proper DEFLATE compression (required by PlantUML)
- Small footprint (~45KB minified)
- No dependencies
- Works in all modern browsers

#### 2. Encoding Functions

PlantUML uses a specific encoding scheme:
**UTF-8 Text → DEFLATE Compression → Custom Base64 Encoding**

```javascript
function encodePlantUML(plantuml) {
    // Step 1: Convert string to UTF-8 bytes
    const utf8 = unescape(encodeURIComponent(plantuml));

    // Step 2: Compress using DEFLATE (pako library)
    const compressed = pako.deflateRaw(utf8, { level: 9 });

    // Step 3: Encode to PlantUML-specific base64
    return encode64(compressed);
}

function encode64(data) {
    // PlantUML uses custom base64 alphabet
    let r = "";
    const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";

    for (let i = 0; i < data.length; i += 3) {
        if (i + 2 == data.length) {
            r += append3bytes(data[i], data[i + 1], 0);
        } else if (i + 1 == data.length) {
            r += append3bytes(data[i], 0, 0);
        } else {
            r += append3bytes(data[i], data[i + 1], data[i + 2]);
        }
    }
    return r;
}

function append3bytes(b1, b2, b3) {
    const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
    const c1 = b1 >> 2;
    const c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
    const c3 = ((b2 & 0xF) << 2) | (b3 >> 6);
    const c4 = b3 & 0x3F;
    return chars.charAt(c1) + chars.charAt(c2) + chars.charAt(c3) + chars.charAt(c4);
}
```

#### 3. Rendering the Diagram

```javascript
async function renderDiagram(plantumlCode) {
    // Encode the PlantUML code
    const encoded = encodePlantUML(plantumlCode);

    // Build the URL (svg, png, or txt format)
    const plantUMLServer = 'https://www.plantuml.com/plantuml/svg/';
    const imageUrl = plantUMLServer + encoded;

    // Create and display image
    const img = document.createElement('img');
    img.src = imageUrl;
    img.alt = 'PlantUML Diagram';

    // Handle errors
    img.onerror = () => {
        console.error('Failed to load PlantUML diagram');
        // Show error message to user
    };

    // Add to page
    document.getElementById('diagram-container').appendChild(img);
}
```

## PlantUML Syntax Guide

### Basic Structure

All PlantUML diagrams start with `@startuml` and end with `@enduml`:

```plantuml
@startuml
' Your diagram code here
@enduml
```

### Comments
- Single-line: `' This is a comment`
- Multi-line: `/' Multi-line comment '/`

### Entity Relationship Diagrams (ERD)

**Our Implementation Example:**

```plantuml
@startuml
!define table(x) entity x

' Define entities
entity "customers" {
    * customer_id : INTEGER <<PK>>
    --
    customer_name : VARCHAR(255)
    phone : VARCHAR(20)
    email : VARCHAR(255)
    address_id : INTEGER <<FK>>
}

entity "orders" {
    * order_id : INTEGER <<PK>>
    --
    customer_id : INTEGER <<FK>>
    order_date : DATE
    total_amount : DECIMAL
}

' Define relationships
customers ||--o{ orders : "places"

@enduml
```

**Relationship Symbols:**
- `||--||` : One to one
- `}o--o{` : Many to many
- `||--o{` : One to many
- `}o--||` : Many to one

**Key Symbols:**
- `* field_name` : Primary key
- `field_name <<PK>>` : Primary key (explicit)
- `field_name <<FK>>` : Foreign key
- `--` : Separator line

### Class Diagrams

```plantuml
@startuml
class Customer {
    - customer_id: int
    - name: String
    - email: String
    + getOrders(): List<Order>
    + addOrder(order: Order): void
}

class Order {
    - order_id: int
    - order_date: Date
    - total: Decimal
    + getCustomer(): Customer
}

Customer "1" -- "0..*" Order : places
@enduml
```

### Sequence Diagrams

```plantuml
@startuml
actor User
participant "Web App" as App
database "PostgreSQL" as DB

User -> App: Request data
App -> DB: SELECT * FROM customers
DB --> App: Result set
App --> User: Display data
@enduml
```

### Database Schema Diagrams (Advanced)

```plantuml
@startuml

' Styling
skinparam linetype ortho
skinparam roundcorner 10

entity customers {
    * customer_id : SERIAL <<PK>>
    --
    customer_name : VARCHAR(255)
    phone : VARCHAR(20)
    email : VARCHAR(255)
    address_id : INTEGER <<FK>>
    created_at : TIMESTAMP
}

entity addresses {
    * address_id : SERIAL <<PK>>
    --
    street_address : VARCHAR(500)
    city : VARCHAR(100)
    state : VARCHAR(2)
    zip_code : VARCHAR(10)
}

entity orders {
    * order_id : SERIAL <<PK>>
    --
    customer_id : INTEGER <<FK>>
    order_date : DATE
    total_amount : DECIMAL(10,2)
    contractor_id : INTEGER <<FK>>
}

entity contractors {
    * contractor_id : SERIAL <<PK>>
    --
    contractor_name : VARCHAR(100)
    phone : VARCHAR(20)
}

customers }o--|| addresses : has
orders }o--|| customers : placed_by
orders }o--|| contractors : assigned_to

@enduml
```

## Complete HTML/JavaScript Implementation Example

```html
<!DOCTYPE html>
<html>
<head>
    <title>PlantUML Viewer</title>
    <style>
        .diagram-container {
            text-align: center;
            padding: 20px;
        }
        .diagram-container img {
            max-width: 100%;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .source-code {
            text-align: left;
            background: #f5f5f5;
            padding: 15px;
            border-radius: 4px;
            font-family: monospace;
            white-space: pre-wrap;
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <div class="diagram-container">
        <h2>PlantUML Diagram</h2>
        <div id="diagram"></div>
        <div class="source-code" id="source"></div>
    </div>

    <!-- Include pako.js for compression -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>

    <script>
        // Encoding functions
        function encodePlantUML(plantuml) {
            const utf8 = unescape(encodeURIComponent(plantuml));
            const compressed = pako.deflateRaw(utf8, { level: 9 });
            return encode64(compressed);
        }

        function encode64(data) {
            let r = "";
            const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";

            for (let i = 0; i < data.length; i += 3) {
                if (i + 2 == data.length) {
                    r += append3bytes(data[i], data[i + 1], 0);
                } else if (i + 1 == data.length) {
                    r += append3bytes(data[i], 0, 0);
                } else {
                    r += append3bytes(data[i], data[i + 1], data[i + 2]);
                }
            }
            return r;
        }

        function append3bytes(b1, b2, b3) {
            const chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_";
            const c1 = b1 >> 2;
            const c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
            const c3 = ((b2 & 0xF) << 2) | (b3 >> 6);
            const c4 = b3 & 0x3F;
            return chars.charAt(c1) + chars.charAt(c2) + chars.charAt(c3) + chars.charAt(c4);
        }

        // Render diagram
        function renderDiagram(plantumlCode) {
            // Show source
            document.getElementById('source').textContent = plantumlCode;

            // Encode and render
            const encoded = encodePlantUML(plantumlCode);
            const plantUMLServer = 'https://www.plantuml.com/plantuml/svg/';
            const imageUrl = plantUMLServer + encoded;

            const img = document.createElement('img');
            img.src = imageUrl;
            img.alt = 'PlantUML Diagram';
            img.onerror = () => {
                document.getElementById('diagram').innerHTML =
                    '<p style="color: red;">Error rendering diagram</p>';
            };

            document.getElementById('diagram').innerHTML = '';
            document.getElementById('diagram').appendChild(img);
        }

        // Example usage
        const exampleDiagram = `@startuml
entity customers {
    * customer_id : INTEGER <<PK>>
    --
    customer_name : VARCHAR(255)
    phone : VARCHAR(20)
    email : VARCHAR(255)
}

entity orders {
    * order_id : INTEGER <<PK>>
    --
    customer_id : INTEGER <<FK>>
    order_date : DATE
    total_amount : DECIMAL
}

customers ||--o{ orders : places
@enduml`;

        // Render on page load
        renderDiagram(exampleDiagram);
    </script>
</body>
</html>
```

## Backend Implementation (Python/Flask)

### Generating PlantUML from Database Schema

```python
from flask import Flask, jsonify
import psycopg2

app = Flask(__name__)

@app.route('/api/db_structure/plantuml')
def generate_plantuml():
    """Generate PlantUML code from database schema"""

    conn = psycopg2.connect(
        host='localhost',
        database='your_database',
        user='postgres',
        password='your_password'
    )
    cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)

    plantuml = ["@startuml", ""]

    # Get all tables
    cursor.execute("""
        SELECT table_name
        FROM information_schema.tables
        WHERE table_schema = 'public'
        AND table_type = 'BASE TABLE'
        ORDER BY table_name
    """)
    tables = [row['table_name'] for row in cursor.fetchall()]

    # Generate entity definitions
    for table in tables:
        plantuml.append(f'entity "{table}" {{')

        # Get columns
        cursor.execute("""
            SELECT
                column_name,
                data_type,
                character_maximum_length,
                is_nullable
            FROM information_schema.columns
            WHERE table_name = %s
            ORDER BY ordinal_position
        """, (table,))
        columns = cursor.fetchall()

        # Get primary keys
        cursor.execute("""
            SELECT kcu.column_name
            FROM information_schema.table_constraints tc
            JOIN information_schema.key_column_usage kcu
                ON tc.constraint_name = kcu.constraint_name
            WHERE tc.table_name = %s
            AND tc.constraint_type = 'PRIMARY KEY'
        """, (table,))
        pk_columns = [row['column_name'] for row in cursor.fetchall()]

        # Add columns to diagram
        for idx, col in enumerate(columns):
            col_name = col['column_name']
            data_type = col['data_type'].upper()

            if col['character_maximum_length']:
                data_type += f"({col['character_maximum_length']})"

            # Mark primary keys
            prefix = "* " if col_name in pk_columns else ""
            suffix = " <<PK>>" if col_name in pk_columns else ""

            # Add separator after primary keys
            if idx == len(pk_columns) and idx > 0:
                plantuml.append("    --")

            plantuml.append(f"    {prefix}{col_name} : {data_type}{suffix}")

        plantuml.append("}")
        plantuml.append("")

    # Get foreign key relationships
    cursor.execute("""
        SELECT
            tc.table_name AS table_name,
            kcu.column_name AS column_name,
            ccu.table_name AS foreign_table_name,
            ccu.column_name AS foreign_column_name
        FROM information_schema.table_constraints AS tc
        JOIN information_schema.key_column_usage AS kcu
            ON tc.constraint_name = kcu.constraint_name
            AND tc.table_schema = kcu.table_schema
        JOIN information_schema.constraint_column_usage AS ccu
            ON ccu.constraint_name = tc.constraint_name
            AND ccu.table_schema = tc.table_schema
        WHERE tc.constraint_type = 'FOREIGN KEY'
    """)
    relationships = cursor.fetchall()

    # Add relationships to diagram
    for rel in relationships:
        table = rel['table_name']
        foreign_table = rel['foreign_table_name']
        plantuml.append(f'"{table}" }}o--|| "{foreign_table}" : "{rel["column_name"]}"')

    plantuml.append("")
    plantuml.append("@enduml")

    cursor.close()
    conn.close()

    return jsonify({
        'plantuml': '\n'.join(plantuml),
        'table_count': len(tables)
    })
```

## Output Format Options

PlantUML supports multiple output formats via URL paths:

```javascript
// SVG (scalable, recommended)
const svgUrl = 'https://www.plantuml.com/plantuml/svg/' + encoded;

// PNG (raster image)
const pngUrl = 'https://www.plantuml.com/plantuml/png/' + encoded;

// TXT (ASCII art)
const txtUrl = 'https://www.plantuml.com/plantuml/txt/' + encoded;

// URL to view/edit online
const editUrl = 'https://www.plantuml.com/plantuml/uml/' + encoded;
```

## Advanced Features

### 1. Styling and Themes

```plantuml
@startuml
!theme blueprint

skinparam linetype ortho
skinparam roundcorner 10
skinparam backgroundColor #FEFEFE
skinparam shadowing false

entity customers {
    ...
}
@enduml
```

### 2. Colors and Formatting

```plantuml
@startuml
entity customers #lightblue {
    * customer_id : INTEGER <<PK>>
    --
    customer_name : VARCHAR(255)
}

entity orders #lightgreen {
    * order_id : INTEGER <<PK>>
}
@enduml
```

### 3. Notes and Annotations

```plantuml
@startuml
entity customers {
    * customer_id : INTEGER
}

note right of customers
    This table stores
    customer information
end note
@enduml
```

### 4. Grouping and Packages

```plantuml
@startuml
package "Core Tables" {
    entity customers
    entity addresses
}

package "Transaction Tables" {
    entity orders
    entity order_items
}
@enduml
```

## Common Pitfalls and Solutions

### 1. **Encoding Issues**
❌ **Problem**: Diagram doesn't render, shows error
✅ **Solution**: Ensure proper DEFLATE compression with pako.js, not just URL encoding

### 2. **Special Characters in Strings**
❌ **Problem**: Quotes or special characters break diagram
✅ **Solution**: Escape quotes in entity names or use single quotes

```plantuml
' Wrong
entity "customer's data" {

' Right
entity "customer data" {
```

### 3. **F-String Issues in Python**
❌ **Problem**: `SyntaxError: f-string: single '}' is not allowed`
✅ **Solution**: Escape braces in f-strings

```python
# Wrong
plantuml.append(f'"{table}" }o--|| "{foreign_table}"')

# Right
plantuml.append(f'"{table}" }}o--|| "{foreign_table}"')
```

### 4. **CORS Issues**
❌ **Problem**: PlantUML server blocks requests
✅ **Solution**: Use `img` tag with `src` attribute (not fetch/XHR)

### 5. **Large Diagrams**
❌ **Problem**: Diagram too large, server times out
✅ **Solution**: Split into multiple diagrams or use local PlantUML server

## Performance Optimization

### 1. Caching
```javascript
const diagramCache = new Map();

function renderDiagramCached(plantumlCode) {
    const cacheKey = btoa(plantumlCode);

    if (diagramCache.has(cacheKey)) {
        return diagramCache.get(cacheKey);
    }

    const encoded = encodePlantUML(plantumlCode);
    diagramCache.set(cacheKey, encoded);
    return encoded;
}
```

### 2. Lazy Loading
```javascript
// Only load pako.js when needed
async function loadPako() {
    if (window.pako) return;

    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js';
        script.onload = resolve;
        script.onerror = reject;
        document.head.appendChild(script);
    });
}
```

## Testing Your Implementation

### Test PlantUML Code Online
Visit: https://www.plantuml.com/plantuml/uml/

Paste your PlantUML code to verify syntax before implementing.

### Simple Test Diagram
```plantuml
@startuml
Bob -> Alice : hello
Alice -> Bob : hi
@enduml
```

This should render a simple sequence diagram. If this works, your encoding is correct.

## Alternative: Server-Side Rendering

If you prefer server-side rendering:

### Using Python `plantuml` Package

```bash
pip install plantuml
```

```python
import plantuml

def render_diagram_server_side(plantuml_code):
    """Render PlantUML on server and return SVG"""
    plantuml_server = plantuml.PlantUML(
        url='https://www.plantuml.com/plantuml/'
    )

    # Render to SVG
    svg_content = plantuml_server.processes(plantuml_code)
    return svg_content
```

## Resources

### Official Documentation
- PlantUML Website: https://plantuml.com/
- PlantUML Language Reference: https://plantuml.com/guide
- Class Diagram Guide: https://plantuml.com/class-diagram
- Entity Diagram Guide: https://plantuml.com/ie-diagram

### Tools
- Online Editor: https://www.plantuml.com/plantuml/uml/
- VS Code Extension: "PlantUML" by jebbs
- pako.js Library: https://github.com/nodeca/pako

### Docker PlantUML Server
```bash
docker run -d -p 8080:8080 plantuml/plantuml-server:jetty
```

Then use: `http://localhost:8080/svg/` instead of public server

## Conclusion

This implementation provides a robust, client-side PlantUML rendering solution that:
- ✅ Requires no backend PlantUML installation
- ✅ Works in all modern browsers
- ✅ Properly handles compression and encoding
- ✅ Supports all PlantUML diagram types
- ✅ Can be easily adapted to any web framework

For production applications with privacy concerns or high traffic, consider self-hosting a PlantUML server using Docker.

---

**Implementation Reference**: Database Structure Page (`templates/db_structure.html`)
**Created**: 2025-10-13
**Last Updated**: 2025-10-13
