---
title: "CKEditor Conversion System"
manual: "RTE CKEditor Image"
version: "main"
permalink: "https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-conversions@main"
source: "CKEditor/Conversions.rst"
rendered: "2026-10-01T01:27:51+00:00"
---

# CKEditor Conversion System {#ckeditor-conversions}

Complete guide to the upcast/downcast conversion system for transforming between HTML and model representations.

## Conversion Architecture {#conversion-architecture}

### Three-Layer System {#three-layer-system}

```php
┌──────────────────────────────────────┐
│ Data Layer (Database/API)           │
│ HTML with data-* attributes          │
│ <img data-htmlarea-file-uid="123"/>  │
└────────────┬─────────────────────────┘
             │
             │ Upcast (Load)
             │
┌────────────▼─────────────────────────┐
│ Model Layer (Abstract)               │
│ typo3image element with attributes   │
│ { name: 'typo3image', fileUid: 123 } │
└────────────┬─────────────────────────┘
             │
             │ Downcast (Render)
             │
┌────────────▼─────────────────────────┐
│ View Layer (Editor Display)          │
│ Visual HTML in contenteditable       │
│ <img src="..." class="..." />        │
└──────────────────────────────────────┘

```

## Upcast Conversions {#upcast-conversions}

### Purpose {#purpose}

**Upcast**: Transforms HTML (from database/API) into model elements when loading content into editor

**When Used**:

-   Initial content load into CKEditor
-   Paste from clipboard
-   Insert HTML programmatically

### Image Element Upcast {#image-element-upcast}

```javascript
editor.conversion.for('upcast').elementToElement({
    view: {
        name: 'img',
        attributes: ['data-htmlarea-file-uid', 'src']
    },
    model: (viewElement, { writer }) => {
        return writer.createElement('typo3image', {
            fileUid: viewElement.getAttribute('data-htmlarea-file-uid'),
            fileTable: viewElement.getAttribute('data-htmlarea-file-table') || 'sys_file',
            src: viewElement.getAttribute('src'),
            width: viewElement.getAttribute('width') || '',
            height: viewElement.getAttribute('height') || '',
            class: viewElement.getAttribute('class') || '',
            alt: viewElement.getAttribute('alt') || '',
            altOverride: viewElement.getAttribute('data-alt-override') || false,
            title: viewElement.getAttribute('title') || '',
            titleOverride: viewElement.getAttribute('data-title-override') || false,
            enableZoom: viewElement.getAttribute('data-htmlarea-zoom') || false,
        });
    }
});
```

### Configuration Breakdown {#configuration-breakdown}

#### View Matcher {#view-matcher}

```javascript
view: {
    name: 'img',
    attributes: ['data-htmlarea-file-uid', 'src']
}
```

**Purpose**: Defines which HTML elements should be converted

**Matching Logic**:

-   **Element name**: Must be `<img>`
-   **Required attributes**: Must have both `data-htmlarea-file-uid` AND `src`

**Examples**:

Matched (will be upcasted):

```html
<img data-htmlarea-file-uid="123" src="/fileadmin/image.jpg" />
```

Not matched (regular img passthrough):

```html
<img src="/fileadmin/image.jpg" />  <!-- Missing data-htmlarea-file-uid -->
<span data-htmlarea-file-uid="123"></span>  <!-- Wrong element -->
```

#### Model Creator Function {#model-creator-function}

```javascript
model: (viewElement, { writer }) => {
    return writer.createElement('typo3image', {
        // attributes...
    });
}
```

**Parameters**:

-   `viewElement`: The matched `<img>` element from HTML
-   `writer`: Model writer for creating elements

**Return**: New model element with extracted attributes

#### Attribute Extraction {#attribute-extraction}

```javascript
fileUid: viewElement.getAttribute('data-htmlarea-file-uid'),
```

**Pattern**: Extract HTML attribute → map to model attribute

**Mappings**:

| HTML Attribute | Model Attribute | Transformation |
| --- | --- | --- |
| data-htmlarea-file-uid | fileUid | Direct copy |
| data-htmlarea-file-table | fileTable | Default: 'sys_file' |
| src | src | Direct copy |
| width | width | Default: empty string |
| height | height | Default: empty string |
| class | class | Default: empty string |
| alt | alt | Default: empty string |
| data-alt-override | altOverride | Default: false |
| title | title | Default: empty string |
| data-title-override | titleOverride | Default: false |
| data-htmlarea-zoom | enableZoom | Default: false |

### Upcast Example Flow {#upcast-example-flow}

**Input HTML**:

```html
<img
    src="/fileadmin/image.jpg"
    data-htmlarea-file-uid="123"
    data-htmlarea-file-table="sys_file"
    width="800"
    height="600"
    alt="Product photo"
    title="Click to enlarge"
    class="img-fluid"
    data-htmlarea-zoom="true"
/>
```

**Upcast Process**:

1.  CKEditor parser encounters `<img>` element
1.  Checks if has `data-htmlarea-file-uid` and `src`
1.  Calls model creator function
1.  Extracts all attributes
1.  Creates model element

**Result Model Element**:

```javascript
{
    name: 'typo3image',
    attributes: {
        fileUid: 123,
        fileTable: 'sys_file',
        src: '/fileadmin/image.jpg',
        width: '800',
        height: '600',
        alt: 'Product photo',
        altOverride: false,
        title: 'Click to enlarge',
        titleOverride: false,
        class: 'img-fluid',
        enableZoom: true
    }
}
```

## Downcast Conversions {#downcast-conversions}

### Purpose {#purpose-1}

**Downcast**: Transforms model elements into HTML for editor display and data saving

**Two Pipelines**:

1.  **Editing Downcast**: Render in contenteditable (editor view)
1.  **Data Downcast**: Serialize for database storage

### Image Element Downcast {#image-element-downcast}

```javascript
editor.conversion.for('downcast').elementToElement({
    model: {
        name: 'typo3image',
        attributes: ['fileUid', 'fileTable', 'src']
    },
    view: (modelElement, { writer }) => {
        const attributes = {
            'src': modelElement.getAttribute('src'),
            'data-htmlarea-file-uid': modelElement.getAttribute('fileUid'),
            'data-htmlarea-file-table': modelElement.getAttribute('fileTable'),
            'width': modelElement.getAttribute('width'),
            'height': modelElement.getAttribute('height'),
            'class': modelElement.getAttribute('class') || '',
            'title': modelElement.getAttribute('title') || '',
            'alt': modelElement.getAttribute('alt') || '',
        };

        if (modelElement.getAttribute('titleOverride')) {
            attributes['data-title-override'] = true;
        }
        if (modelElement.getAttribute('altOverride')) {
            attributes['data-alt-override'] = true;
        }
        if (modelElement.getAttribute('enableZoom')) {
            attributes['data-htmlarea-zoom'] = true;
        }

        return writer.createEmptyElement('img', attributes);
    },
});
```

### Configuration Breakdown {#configuration-breakdown-1}

#### Model Matcher {#model-matcher}

```javascript
model: {
    name: 'typo3image',
    attributes: ['fileUid', 'fileTable', 'src']
}
```

**Purpose**: Defines which model elements trigger this conversion

**Matching**:

-   Element name is `typo3image`
-   Has `fileUid`, `fileTable`, `src` attributes (required for meaningful output)

#### View Creator Function {#view-creator-function}

```javascript
view: (modelElement, { writer }) => {
    return writer.createEmptyElement('img', attributes);
}
```

**Parameters**:

-   `modelElement`: The typo3image model element
-   `writer`: View writer for creating elements

**Return**: New view element (`<img>`)

#### Attribute Mapping {#attribute-mapping}

```javascript
const attributes = {
    'src': modelElement.getAttribute('src'),
    'data-htmlarea-file-uid': modelElement.getAttribute('fileUid'),
    // ...
};
```

**Pattern**: Read model attribute → map to HTML attribute

**Reverse Mappings**:

| Model Attribute | HTML Attribute | Transformation |
| --- | --- | --- |
| src | src | Direct copy |
| fileUid | data-htmlarea-file-uid | Direct copy |
| fileTable | data-htmlarea-file-table | Direct copy |
| width | width | Direct copy |
| height | height | Direct copy |
| class | class | Default: empty string |
| alt | alt | Default: empty string |
| title | title | Default: empty string |
| altOverride | data-alt-override | Only if true |
| titleOverride | data-title-override | Only if true |
| enableZoom | data-htmlarea-zoom | Only if true |

#### Conditional Attributes {#conditional-attributes}

```javascript
if (modelElement.getAttribute('titleOverride')) {
    attributes['data-title-override'] = true;
}
```

**Pattern**: Only add boolean attributes when true

**Why**: Cleaner HTML output, avoid unnecessary attributes

**Result**:

```html
<!-- When titleOverride = true -->
<img ... data-title-override="true" />

<!-- When titleOverride = false or absent -->
<img ... />  <!-- No data-title-override attribute -->
```

### Downcast Example Flow {#downcast-example-flow}

**Input Model Element**:

```javascript
{
    name: 'typo3image',
    attributes: {
        fileUid: 123,
        fileTable: 'sys_file',
        src: '/fileadmin/image.jpg',
        width: '800',
        height: '600',
        alt: 'Product photo',
        altOverride: true,
        class: 'img-fluid',
        enableZoom: true
    }
}
```

**Downcast Process**:

1.  CKEditor needs to render model element
1.  Finds typo3image → elementToElement converter
1.  Calls view creator function
1.  Maps all attributes
1.  Adds conditional attributes
1.  Creates view element

**Result HTML**:

```html
<img
    src="/fileadmin/image.jpg"
    data-htmlarea-file-uid="123"
    data-htmlarea-file-table="sys_file"
    width="800"
    height="600"
    alt="Product photo"
    data-alt-override="true"
    class="img-fluid"
    data-htmlarea-zoom="true"
/>
```

## Attribute Converters {#attribute-converters}

### Class Attribute Converter {#class-attribute-converter}

```javascript
editor.conversion.for('downcast').attributeToAttribute({
    model: { name: 'typo3image', key: 'class' },
    view: 'class'
});
```

**Purpose**: Immediately sync class attribute changes to view

> [!NOTE]
> The elementToElement converter only runs on element creation. This converter handles attribute updates.

**Behavior**:

```javascript
// User changes class via style system
editor.model.change(writer => {
    writer.setAttribute('class', 'float-left mr-3', modelElement);
});

// Immediately reflected in view
<img class="float-left mr-3" ... />
```

### Custom Attribute Converters {#custom-attribute-converters}

You can add converters for any attribute:

```javascript
// Width changes immediately visible
editor.conversion.for('downcast').attributeToAttribute({
    model: { name: 'typo3image', key: 'width' },
    view: 'width'
});

// Alt changes immediately visible
editor.conversion.for('downcast').attributeToAttribute({
    model: { name: 'typo3image', key: 'alt' },
    view: 'alt'
});
```

## Data Pipeline {#data-pipeline}

### Complete Load → Edit → Save Flow {#complete-load-edit-save-flow}

```php
1. Load from Database
   ─────────────────────►
   HTML String
   <img data-htmlarea-file-uid="123" src="..." />

2. Upcast (HTML → Model)
   ─────────────────────►
   Model Element
   typo3image { fileUid: 123, src: "..." }

3. Edit in Editor
   ─────────────────────►
   Model Changes
   width: "800" → "1200"
   class: "" → "float-left"

4. Downcast (Model → View)
   ─────────────────────►
   View Updates
   <img width="1200" class="float-left" ... />

5. Save to Database
   ─────────────────────►
   Data Downcast → HTML String
   <img data-htmlarea-file-uid="123" width="1200" class="float-left" ... />

6. Backend Processing
   ─────────────────────►
   RteImagesDbHook processes HTML
   Magic image processing, URL updates

```

## Paste Handling {#paste-handling}

### Paste from External Source {#paste-from-external-source}

When pasting HTML from external sources (websites, Word, etc.):

```php
1. Browser Paste Event
   ─────────────────────►
   External HTML
   <img src="https://example.com/image.jpg" />

2. Upcast Attempted
   ─────────────────────►
   Check: data-htmlarea-file-uid present? ❌
   Result: Upcast skipped, treated as regular <img>

3. Fallback Handling
   ─────────────────────►
   CKEditor default image handling
   May need custom paste processor for external images

```

### Paste from Same Editor {#paste-from-same-editor}

```php
1. Copy typo3image
   ─────────────────────►
   Clipboard contains model element

2. Paste
   ─────────────────────►
   Direct model copy (no conversion needed)

3. Result
   ─────────────────────►
   Duplicate typo3image with same attributes

```

## Custom Conversion Patterns {#custom-conversion-patterns}

### Pattern 1: Transformation During Upcast {#pattern-1-transformation-during-upcast}

```javascript
editor.conversion.for('upcast').elementToElement({
    view: {
        name: 'img',
        attributes: ['data-htmlarea-file-uid']
    },
    model: (viewElement, { writer }) => {
        // Transform srcset to src
        const src = viewElement.getAttribute('src') ||
                    viewElement.getAttribute('srcset')?.split(',')[0];

        // Parse dimensions from style
        const style = viewElement.getAttribute('style') || '';
        const widthMatch = style.match(/width:\s*(\d+)px/);
        const heightMatch = style.match(/height:\s*(\d+)px/);

        return writer.createElement('typo3image', {
            src: src,
            width: widthMatch ? widthMatch[1] : '',
            height: heightMatch ? heightMatch[1] : '',
            // ... other attributes
        });
    }
});
```

### Pattern 2: Conditional Downcast {#pattern-2-conditional-downcast}

```javascript
editor.conversion.for('downcast').elementToElement({
    model: 'typo3image',
    view: (modelElement, { writer }) => {
        // Different output based on context
        const width = parseInt(modelElement.getAttribute('width'), 10);

        // Large images get responsive class
        if (width > 1200) {
            attributes['class'] = (attributes['class'] || '') + ' img-responsive';
        }

        return writer.createEmptyElement('img', attributes);
    }
});
```

### Pattern 3: Multi-Element Conversion {#pattern-3-multi-element-conversion}

```javascript
// Convert linked image to nested structure
editor.conversion.for('downcast').elementToStructure({
    model: 'typo3image',
    view: (modelElement, { writer }) => {
        const linkHref = modelElement.getAttribute('linkHref');

        if (linkHref) {
            // Create nested structure: <a><img/></a>
            const img = writer.createEmptyElement('img', imgAttributes);
            const link = writer.createContainerElement('a', { href: linkHref });
            writer.insert(writer.createPositionAt(link, 0), img);
            return link;
        } else {
            // Just image
            return writer.createEmptyElement('img', imgAttributes);
        }
    }
});
```

## Debugging Conversions {#debugging-conversions}

### Logging Upcast {#logging-upcast}

```javascript
editor.conversion.for('upcast').elementToElement({
    view: { name: 'img', attributes: ['data-htmlarea-file-uid'] },
    model: (viewElement, { writer }) => {
        console.log('Upcasting image:', {
            src: viewElement.getAttribute('src'),
            fileUid: viewElement.getAttribute('data-htmlarea-file-uid'),
            allAttributes: Array.from(viewElement.getAttributes())
        });

        return writer.createElement('typo3image', {
            // ... attributes
        });
    }
});
```

### Logging Downcast {#logging-downcast}

```javascript
editor.conversion.for('downcast').elementToElement({
    model: 'typo3image',
    view: (modelElement, { writer }) => {
        console.log('Downcasting typo3image:', {
            fileUid: modelElement.getAttribute('fileUid'),
            src: modelElement.getAttribute('src'),
            allAttributes: Array.from(modelElement.getAttributes())
        });

        return writer.createEmptyElement('img', attributes);
    }
});
```

### Inspecting Conversion Results {#inspecting-conversion-results}

```javascript
// After loading content
editor.model.change(() => {
    const root = editor.model.document.getRoot();
    for (const item of root.getChildren()) {
        if (item.name === 'typo3image') {
            console.log('Found typo3image:', {
                fileUid: item.getAttribute('fileUid'),
                src: item.getAttribute('src')
            });
        }
    }
});
```

## Common Issues {#common-issues}

### Issue: Images Not Converting on Load {#issue-images-not-converting-on-load}

**Symptoms**:

-   HTML loaded but no typo3image elements in model
-   Images appear as plain text or broken

**Causes**:

1.  Missing `data-htmlarea-file-uid` attribute
1.  Upcast converter not registered
1.  View matcher too restrictive

**Solutions**:

Verify HTML has required attributes:

```html
<!-- Will convert -->
<img data-htmlarea-file-uid="123" src="..." />

<!-- Won't convert (missing required attribute) -->
<img src="..." />
```

Check converter registration:

```javascript
// Verify in browser console
console.log(editor.conversion);
// Should show upcast/downcast converters
```

### Issue: Attributes Lost During Conversion {#issue-attributes-lost-during-conversion}

**Symptoms**:

-   Attributes present in HTML/model
-   Missing in view/output

**Causes**:

1.  Attribute not in schema `allowAttributes` list
1.  Attribute not mapped in conversion
1.  Conditional logic skipping attribute

**Solutions**:

Verify schema allows attribute:

```javascript
allowAttributes: [
    'src', 'fileUid', /* add missing attribute here */
]
```

Add to conversion:

```javascript
// In upcast
myCustomAttribute: viewElement.getAttribute('data-custom'),

// In downcast
'data-custom': modelElement.getAttribute('myCustomAttribute'),
```

### Issue: View Not Updating When Model Changes {#issue-view-not-updating-when-model-changes}

**Symptoms**:

-   Model attribute updated
-   View doesn't reflect change
-   Need to reload to see changes

**Cause**: Missing attribute converter for immediate sync

**Solution**:

Add attribute converter:

```javascript
editor.conversion.for('downcast').attributeToAttribute({
    model: { name: 'typo3image', key: 'myAttribute' },
    view: 'data-my-attribute'
});
```

## Performance Optimization {#performance-optimization}

### Batch Conversions {#batch-conversions}

```javascript
// Inefficient: Convert one at a time
images.forEach(img => {
    editor.model.change(writer => {
        writer.setAttribute('class', 'processed', img);
    });
});

// Efficient: Single model change batch
editor.model.change(writer => {
    images.forEach(img => {
        writer.setAttribute('class', 'processed', img);
    });
});
```

### Lazy Attribute Reading {#lazy-attribute-reading}

```javascript
// Inefficient: Read all attributes upfront
const allAttrs = {
    src: viewElement.getAttribute('src'),
    width: viewElement.getAttribute('width'),
    height: viewElement.getAttribute('height'),
    // ... 20 more attributes
};

// Efficient: Read only needed attributes
const src = viewElement.getAttribute('src');
const fileUid = viewElement.getAttribute('data-htmlarea-file-uid');
```

## Related Documentation {#related-documentation}

-   [CKEditor Plugin Development](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-plugin-development-guide@main)
-   [CKEditor Model Element Reference](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-model-element@main)
-   [CKEditor Style Integration](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:ckeditor-style-integration@main)
-   [System Architecture](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:architecture-overview@main)
-   [Data Handling API](https://docs.typo3.org/permalink/netresearch/rte-ckeditor-image:api-datahandling@main)
