RTE CKEditor Image 

Extension key

rte_ckeditor_image

Package name

netresearch/rte-ckeditor-image

Version

13.9

Language

en

Author

Netresearch DTT GmbH

License

This document is published under the Creative Commons BY 4.0 license.

Rendered

Wed, 29 Apr 2026 06:21:56 +0000

Image support in CKEditor for the TYPO3 ecosystem - by Netresearch.


📘 Introduction 

The RTE CKEditor Image extension provides comprehensive image handling capabilities for TYPO3's CKEditor Rich Text Editor with full FAL integration.

⚡ Quick Start 

Get up and running quickly with installation instructions and basic configuration examples.

⚠️ Core Removal Notice 

Important: TYPO3 intentionally removed RTE image handling in v10. Understand the design decision before using this extension.

⚙️ Configuration 

Learn how to configure custom image styles, processing options, and frontend rendering setup.

🏗️ Architecture 

Understand the extension's architecture, design patterns, and how components interact.

🔧 Developer API 

Explore the PHP and JavaScript APIs for extending and customizing the extension.

🐛 Troubleshooting 

Find solutions to common issues and learn debugging techniques.

🤝 Contributing 

Help improve this extension through code contributions, documentation, or translations.

License 

This extension is licensed under AGPL-3.0-or-later.

Introduction 

The RTE CKEditor Image extension provides comprehensive image handling capabilities for |typo3|'s CKEditor Rich Text Editor. This extension enables editors to insert, configure, and style images directly within the CKEditor interface, with full integration into |typo3|'s File Abstraction Layer (FAL).

Key Features 

CKEditor 5 Integration 

Native CKEditor 5 plugin with full toolbar integration and TYPO3 file browser support.

TYPO3 FAL Support 

Full File Abstraction Layer integration with file references, metadata, and browser.

Image Processing 

Magic images, cropping, scaling with configurable quality multipliers (1x-6x).

Custom Styles 

Define custom image styles (borders, shadows, alignment) via YAML configuration.

Responsive Images 

Automatic srcset generation and picture element support for responsive layouts.

Performance 

Native lazy loading, intersection observer fallback, and optimized rendering.

Event Architecture 

PSR-14 event dispatching for custom image processing and rendering hooks.

Security 

Protocol blocking, XSS prevention, file validation, and FAL-based access control.

Quality Multipliers 

New in version 13.1.5

Retina (2x), Ultra (3x), and Print (6x) quality settings for high-DPI displays.

noScale Mode 

New in version 13.1.5

Skip image processing entirely for pre-optimized images (SVG, WebP, optimized PNG).

Service Architecture 

New in version 13.1.5

Modern Parser → Resolver → Renderer pipeline with dependency injection.

Fluid Templates 

New in version 13.1.5

Customizable output via template overrides for complete rendering control.

Linked Images 

New in version 13.5.0

Full link support with TYPO3 link browser integration, target options, and URL parameters.

Figure & Caption Support 

Native <figure>/<figcaption> output. Add captions directly in the image dialog -- no custom content elements needed.

Caption Width Constraint 

New in version 13.6.0

Figcaptions automatically constrained to image width via max-width on <figure>.

Image Validation 

New in version 13.5.0

CLI command and upgrade wizard to detect and fix broken image references and nested links.

TYPO3 v14 & PHP 8.5 

New in version 13.2.0

Full compatibility with TYPO3 v14 and tested with PHP 8.5.

Visual Preview 

Rich Text Editor with image support in TYPO3 backend

The RTE with image support enabled, showing an inserted image with styling options

RTE CKEditor Image extension demo

Image insertion and configuration workflow in CKEditor with TYPO3 file browser integration

Version Information 

Supported TYPO3

13.4.21+ LTS, 14.3+ LTS

License

AGPL-3.0-or-later

Repository

github.com/netresearch/t3x-rte_ckeditor_image

Maintainer

Netresearch DTT GmbH

Requirements 

System Requirements 

  • TYPO3: 13.4.21+ LTS or 14.3+ LTS
  • PHP: 8.2 or later
  • Extensions: cms-rte-ckeditor (included in TYPO3 core)

Quick Start 

Installation 

Install via Composer:

composer require netresearch/rte-ckeditor-image
Copied!
Extension Manager showing RTE CKEditor Image extension installed

The extension appears in the TYPO3 Extension Manager after installation

Changed in version 13.5.0

The RTE preset and frontend TypoScript are now provided via Site Set only. You must enable the Site Set in your site configuration.

Step 1: Enable Site Set (Required)

Add the extension to your site dependencies:

config/sites/<site>/config.yaml
dependencies:
  - netresearch/rte-ckeditor-image
Copied!

This enables:

  • Backend RTE: Registers the rteWithImages preset with insertimage button
  • Frontend rendering: Includes TypoScript for image processing

Step 2: Clear Caches

vendor/bin/typo3 cache:flush
Copied!
CKEditor toolbar with image insert button highlighted

The insertimage button in the CKEditor toolbar opens the TYPO3 file browser for image selection

Custom Configuration (Optional) 

If you need to customize the RTE configuration or create your own preset, see the RTE Setup Guide for detailed instructions.

The extension provides a default preset that you can extend or override as needed.

TYPO3 Core Removal & Design Decision 

What TYPO3 Removed 

In TYPO3 v10.0 (Breaking #88500), the core team removed the RTE image handling functionality:

Removed Components 

  • RTE processing mode ("ts_images")
  • SoftReference Index for inline images
  • Magic Image processing (automatic scaling, cropping via TSConfig)
  • Image storage handling (RTE_imageStorageDir)
  • CLI cleanup command (cleanup:rteimages)
  • Public API methods (ImportExport->getRTEoriginalFilename(), RteHtmlParser->TS_images_rte())

Later Deprecation 

In TYPO3 v12.4 (Deprecation #99237), the MagicImageService class was deprecated with no direct migration path.

Why TYPO3 Removed This 

The TYPO3 core team removed this functionality for several architectural reasons:

  1. Obsolete Technology

    CKEditor replaced RTEHtmlArea in TYPO3 v8, making the native RTE image handling unused and obsolete.

  2. Incomplete Implementation

    The changelog explicitly states the functionality was "very incomplete" compared to modern alternatives.

  3. Architectural Philosophy

    TYPO3 promotes structured content over inline mixed content. Storing images as relations (FAL references) provides better:

    • Content reuse across multiple elements
    • Metadata management (alt text, copyright, descriptions)
    • Image variant generation (responsive images, WebP conversion)
    • Migration and content import/export
    • Multi-language handling
    • Permission and access control
    • Asset management and organization

TYPO3's Official Recommendation 

The breaking change documentation recommends:

Primary Approach: Structured Content 

Move images from inline RTE fields to proper relational fields:

// TCA Configuration Example
'columns' => [
    'bodytext' => [
        'config' => [
            'type' => 'text',
            'enableRichtext' => true,
        ],
    ],
    'images' => [
        'config' => [
            'type' => 'file',
            'allowed' => 'common-image-types',
            'maxitems' => 10,
        ],
    ],
],
Copied!

Benefits of Structured Content:

  • ✅ Better content reuse
  • ✅ Proper metadata management
  • ✅ Responsive image generation
  • ✅ Clean separation of concerns
  • ✅ Modern TYPO3 architecture
  • ✅ Better editor experience

Fallback Approach: Extensions 

For projects that need inline image functionality, TYPO3 recommends extensions like rte_ckeditor_image or creating custom extension implementations.

Why This Extension Exists 

Despite TYPO3's architectural direction, this extension exists because:

Real-World Requirements 

  1. Legacy Content Migration

    Many TYPO3 installations have years of content with inline images. Migrating to structured content requires significant time and resources.

  2. Editorial Workflows

    Some editorial teams are trained on inline image workflows and prefer WYSIWYG image placement directly in text.

  3. Content Nature

    Certain content types (news articles, blog posts, documentation) naturally contain inline images that are contextually bound to surrounding text.

  4. Migration Bridge

    Provides a transition path while planning migration to structured content.

What This Extension Provides 

  • Backward compatibility with RTEHtmlArea image workflows
  • Magic Image processing (automatic scaling via TSConfig)
  • TYPO3 FAL integration (native file browser)
  • Modern CKEditor 5 implementation
  • Image attributes (width, height, alt, title, quality)
  • Custom styles via CKEditor style system
  • Event-driven architecture for extensibility

Decision Guide: Should You Use This Extension? 

Use This Extension When 

You have legacy content with extensive inline images that cannot be migrated immediately

Editorial workflow requires inline image placement with WYSIWYG editing

Content is tightly coupled to surrounding text (inline diagrams, screenshots, examples)

Migration timeline is long and you need a working solution now

Small to medium projects where structured content overhead isn't justified

Follow TYPO3 Guidelines Instead When 

Starting a new project - Build with structured content from the beginning

Images are reusable - Same images appear across multiple content elements

Need advanced features - Responsive images, WebP conversion, image variants

Multi-language sites - Image metadata needs proper translation workflows

Large editorial teams - Structured content provides better governance

Long-term maintainability - Align with TYPO3's architectural direction

Hybrid Approach 

You can use both approaches in the same TYPO3 installation:

  • Structured content for main images, galleries, and reusable assets
  • Inline images (this extension) for contextual images in rich text

Example TCA configuration:

'columns' => [
    'header_image' => [
        // Structured: Main article image
        'config' => [
            'type' => 'file',
            'allowed' => 'common-image-types',
            'maxitems' => 1,
        ],
    ],
    'bodytext' => [
        // Inline: Contextual images within text
        'config' => [
            'type' => 'text',
            'enableRichtext' => true,
            // rte_ckeditor_image provides inline functionality
        ],
    ],
    'gallery' => [
        // Structured: Image gallery
        'config' => [
            'type' => 'file',
            'allowed' => 'common-image-types',
            'maxitems' => 20,
        ],
    ],
],
Copied!

Future Migration Path 

If you use this extension now but plan to migrate to structured content later:

Planning Migration 

  1. Audit content - Identify all RTE fields with inline images
  2. Create TCA fields - Add proper FAL reference fields
  3. Write migration script - Extract inline images to relations
  4. Update templates - Adjust Fluid templates for structured content
  5. Train editors - Update editorial workflows and documentation

Migration Tools 

TYPO3 provides tools for content migration:

  • Data Handler API for programmatic content updates
  • TypoScript processors for rendering
  • CLI commands for batch processing

This extension can coexist during the migration period.

Best Practices If Using This Extension 

  1. Document the Decision

    Add notes to your project documentation explaining why inline images are used and what the long-term plan is.

  2. Set Editor Guidelines

    Define when editors should use inline images vs. structured image fields.

  3. Configure Processing

    Use magic image configuration (TSConfig) to control automatic scaling:

    RTE.default.buttons.image.options.magic {
        maxWidth = 1920
        maxHeight = 9999
    }
    Copied!
  4. Monitor TYPO3 Updates

    Stay informed about TYPO3's direction regarding RTE and CKEditor.

  5. Plan Migration

    If project lifespan is long, plan eventual migration to structured content.

Conclusion 

This extension serves as a pragmatic bridge between TYPO3's architectural direction (structured content) and real-world editorial needs (inline images).

Key Takeaways:

  • TYPO3 intentionally removed RTE image handling for good architectural reasons
  • Structured content is the recommended modern approach
  • This extension provides backward compatibility when needed
  • Consider your project's specific requirements, timeline, and resources
  • A hybrid approach (both structured and inline) is valid
  • Plan for eventual migration if project lifespan is long

Questions to ask:

  1. Do we have time/budget to migrate existing content?
  2. Does our editorial team need inline image placement?
  3. Are our images contextually bound to text or reusable assets?
  4. What is our project's expected lifespan?
  5. Can we align with TYPO3's architectural direction?

The "right" choice depends on your specific context. There is no universal answer.

Additional Resources 

TYPO3 Core Documentation:

This Extension:

Support & Contributing 

Get Help 

Contribute Code 

Help Translate 

You can help translate this extension into your language through TYPO3's Crowdin platform:

Translation Platform: https://crowdin.com/project/typo3-extension-rte_ckeditor_image

How to contribute translations 

  1. Create a Crowdin account (free for open source contributors)
  2. Join the TYPO3 translation team for your language
  3. Translate strings directly in the Crowdin web interface
  4. Review translations from other contributors
  5. Suggest improvements to existing translations

Why translate 

  • Make TYPO3 more accessible to speakers of your language
  • Help the global TYPO3 community
  • No programming knowledge required
  • Translations are automatically integrated via pull requests

Translation notes 

  • Some terms like "Retina", "Ultra", "Standard" are multilingual - keep as-is or transliterate if more natural in your language
  • Context notes are provided for technical terms to help with accurate translation
  • Your contributions are reviewed by language coordinators before integration

Need help 

Credits 

Development & Maintenance 

Community Contributors 

See GitHub Contributors

Additional Resources 

Integration & Configuration 

Complete configuration reference and integration guide for the RTE CKEditor Image extension.

Changed in version 13.1.5

Added quality multipliers, noScale mode, SVG support, and new service architecture. See Quality Multipliers for quality settings.

Configuration Quick Reference 

For Custom RTE Presets 

These examples show how to create custom configurations that override the automatic defaults. If you just installed the extension and it's working, you don't need these.

Minimum Custom Toolbar 

# Only needed if customizing the default toolbar
editor:
  config:
    toolbar:
      items:
        - insertimage
Copied!

Custom Toolbar with Specific Buttons 

# Example: Custom preset with limited toolbar
editor:
  config:
    toolbar:
      items:
        - bold
        - italic
        - insertimage
Copied!

Configuration Patterns 

By Use Case 

By Component 

Configuration Topics 

🛠️ RTE Setup 

RTE configuration, presets, and toolbar setup

⚙️ TSConfig 

Page TSConfig settings, permissions, and file mounts

🖼️ Frontend Rendering 

TypoScript configuration and frontend rendering setup

🔧 Advanced Configuration 

Custom styles, performance optimization, and best practices

🛡️ Security 

Security features, file validation, and XSS protection

RTE Setup 

Complete guide for configuring the RTE (Rich Text Editor) with CKEditor image support.

Automatic Configuration (Default) 

The extension automatically provides for the backend:

  • Preset: rteWithImages registered and enabled globally
  • Toolbar: insertimage button included in default toolbar
  • Configuration: Configuration/RTE/Default.yaml with full toolbar

Custom RTE Configuration 

Creating Custom Presets 

If you need to customize the toolbar or RTE behavior beyond the defaults, create a custom preset:

EXT:my_ext/Configuration/RTE/Custom.yaml
imports:
  # Import default RTE config
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Default.yaml" }
  # Import image plugin configuration
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    toolbar:
      items:
        - heading
        - '|'
        - insertimage
        - link
        - '|'
        - bold
        - italic
Copied!

Register Custom Preset 

EXT:my_ext/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['custom']
    = 'EXT:my_ext/Configuration/RTE/Custom.yaml';
Copied!

Enable Custom Preset 

Page TSConfig
RTE.default.preset = custom
Copied!

Advanced RTE Configuration 

Custom Allowed Extensions 

editor.externalPlugins.typo3image.allowedExtensions

editor.externalPlugins.typo3image.allowedExtensions
type

string

Default

Value from $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']

Comma-separated list of allowed image file extensions for the RTE image plugin.

Restricts which file types can be selected through the image browser.

Example:

editor:
  externalPlugins:
    typo3image:
      route: "rteckeditorimage_wizard_select_image"
      allowedExtensions: "jpg,jpeg,png,gif,webp"
Copied!

If not specified, falls back to the global TYPO3 configuration at $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']

Multiple RTE Presets 

Different configurations for different content types:

EXT:my_ext/Configuration/RTE/Simple.yaml
imports:
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Minimal.yaml" }
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    removePlugins: null
    toolbar:
      items:
        - insertimage
Copied!
Different presets for different fields
# Different presets for different fields
RTE.default.preset = default
RTE.config.tt_content.bodytext.preset = full
RTE.config.tt_content.header.preset = simple
Copied!

Page TSConfig Setup 

Configuration of page TSConfig settings for image handling, upload folders, and permissions.

Page TSConfig 

Page TSConfig module showing RTE configuration

Page TSConfig configuration in the TYPO3 backend

Magic Image Configuration 

Configure maximum image dimensions for automatic image processing:

RTE.default.buttons.image.options.magic.maxWidth

RTE.default.buttons.image.options.magic.maxWidth
type

integer

Default

300

Maximum width in pixels for images inserted through the RTE.

Images larger than this value will be automatically resized during processing.

RTE.default.buttons.image.options.magic.maxHeight

RTE.default.buttons.image.options.magic.maxHeight
type

integer

Default

1000

Maximum height in pixels for images inserted through the RTE.

Images taller than this value will be automatically resized during processing.

Example:

RTE.default.buttons.image.options.magic {
    maxWidth = 1920
    maxHeight = 9999
}
Copied!

Processing Modes 

RTE.default.proc.overruleMode := addToList(default)
Copied!

Upload Folder Configuration 

RTE.default.buttons.image.options.defaultUploadFolder

RTE.default.buttons.image.options.defaultUploadFolder
type

string

Default

(empty)

Default upload folder for images inserted through the RTE.

Format: <storage_uid>:<folder_path>

Example: 1:rte_uploads/ uses storage 1 and uploads to rte_uploads/ directory.

RTE.default.buttons.image.options.createUploadFolderIfNeeded

RTE.default.buttons.image.options.createUploadFolderIfNeeded
type

boolean

Default

false

Automatically creates the upload folder if it doesn't exist.

Recommended to set to 1 (true) to avoid upload errors.

Example:

RTE.default.buttons.image.options {
    defaultUploadFolder = 1:rte_uploads/
    createUploadFolderIfNeeded = 1
}
Copied!

Content Element Configuration 

Enable for Specific Content Types 

# Only enable for tt_content bodytext
RTE.config.tt_content.bodytext {
    preset = default
    buttons.image.options.magic {
        maxWidth = 1200
        maxHeight = 800
    }
}
Copied!

Disable for Specific Fields 

# Disable RTE entirely for specific field
RTE.config.tt_content.header.disabled = 1
Copied!

Backend User Permissions 

File Mounts 

Ensure backend users have appropriate file mounts:

User TSConfig
options.defaultUploadFolder = 1:user_uploads/rte/
Copied!

Access Restrictions 

User TSConfig
# Allow only specific file extensions
options.file_list.validFileExtensions = jpg,jpeg,png,gif,webp
Copied!

Multi-Language Configuration 

Language-Specific Presets 

[siteLanguage("locale") == "de_DE"]
    RTE.default.preset = german
[END]

[siteLanguage("locale") == "en_US"]
    RTE.default.preset = english
[END]
Copied!

Troubleshooting Configuration 

Debug RTE Configuration 

Enable RTE debugging:

Page TSConfig
RTE.default.showButtons = *
RTE.default.hideButtons =
Copied!

Verify Configuration Loading 

Check active RTE configuration in backend:

  1. Edit content element
  2. Open browser console
  3. Check CKEDITOR.config object

Configuration Priority 

Configuration precedence (highest to lowest):

  1. Field-specific config: RTE.config.tt_content.bodytext
  2. Type-specific config: RTE.config.tt_content
  3. Default config: RTE.default
  4. Extension defaults

Frontend Rendering 

TypoScript configuration for frontend image rendering, CSS classes, lazy loading, and lightbox integration.

TypoScript Configuration 

Frontend Rendering Setup 

The extension provides default configuration. You can customize it:

Changed in version 13.1.5

The legacy ImageRenderingController and ImageLinkRenderingController were replaced with unified ImageRenderingAdapter using the new service architecture. See Services API for details.

Frontend rendering configuration
lib.parseFunc_RTE {
    tags.img = TEXT
    tags.img {
        current = 1
        preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
    }

    tags.a = TEXT
    tags.a {
        current = 1
        preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderInlineLink
    }

    nonTypoTagStdWrap.HTMLparser.tags.img.fixAttrib {
        # Remove internal data attributes from frontend
        allparams.unset = 1
        data-htmlarea-file-uid.unset = 1
        data-htmlarea-file-table.unset = 1
        # Keep zoom attributes for popup/lightbox rendering (ImageRenderingAdapter.php)
        # data-htmlarea-zoom.unset = 1
        # data-htmlarea-clickenlarge.unset = 1
        data-title-override.unset = 1
        data-alt-override.unset = 1
    }
}

lib.parseFunc_RTE.nonTypoTagStdWrap.encapsLines.encapsTagList := addToList(img)
Copied!

Default CSS Class 

Add default class to all RTE images:

Default CSS class configuration
lib.parseFunc_RTE {
    nonTypoTagStdWrap.HTMLparser.tags.img.fixAttrib.class {
        default = img-fluid responsive-image
    }
}
Copied!

Lazyload Configuration 

Enable native browser lazy loading:

Template constants for lazy loading
styles.content.image.lazyLoading = lazy
# Options: lazy, eager, auto
Copied!

Manual TypoScript Inclusion 

Changed in version 13.4.0

TypoScript is no longer automatically loaded. Manual inclusion is required. See Frontend Rendering for inclusion options.

The extension requires manual TypoScript inclusion, giving you full control over load order and the ability to override settings in your site package.

Include via Static Template:

  1. Go to WEB > Template module
  2. Select your root page
  3. In Includes tab, add: CKEditor Image Support

Or import directly in your site package:

Direct import in site package TypoScript
@import 'EXT:rte_ckeditor_image/Configuration/TypoScript/ImageRendering/setup.typoscript'

# Now you can override settings:
lib.contentElement.settings.media.popup {
    directImageLink = 1
    linkParams.ATagParams.dataWrap = class="lightbox"
}
Copied!

TypoScript Reference 

Complete TypoScript Configuration Options 

Image Rendering 

Image tag processing
lib.parseFunc_RTE {
    tags.img = TEXT
    tags.img {
        current = 1
        preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
    }
}
Copied!

HTML Parser Configuration 

HTMLparser attribute cleanup
lib.parseFunc_RTE.nonTypoTagStdWrap.HTMLparser.tags.img {
    fixAttrib {
        # Remove internal data attributes
        data-htmlarea-file-uid.unset = 1
        data-htmlarea-file-table.unset = 1
        # Keep zoom attributes for popup/lightbox rendering (ImageRenderingAdapter.php)
        # data-htmlarea-zoom.unset = 1
        # data-htmlarea-clickenlarge.unset = 1
        data-title-override.unset = 1
        data-alt-override.unset = 1
    }
}
Copied!

Default CSS Classes 

Default and allowed CSS classes
lib.parseFunc_RTE.nonTypoTagStdWrap.HTMLparser.tags.img.fixAttrib.class {
    default = img-fluid
    list = img-fluid,img-thumbnail,rounded
}
Copied!

Lazy Loading 

Native browser lazy loading
# Template Constants
styles.content.image.lazyLoading = lazy
# Options: lazy, eager, auto
Copied!

Image Processing 

Image dimension processing
lib.parseFunc_RTE.nonTypoTagStdWrap.HTMLparser.tags.img {
    width =
    height =
    # Allows TYPO3 to process dimensions
}
Copied!

Encapsulation Configuration 

Image encapsulation in paragraphs
lib.parseFunc_RTE.nonTypoTagStdWrap.encapsLines {
    encapsTagList := addToList(img)
    remapTag.img = p
}
Copied!

Backend Preview Styling 

New in version 13.5

The extension automatically registers an image-aware preview renderer for all CTypes with RTE-enabled bodytext (e.g. text, textmedia, textpic). This ensures images are visible in the Page module preview.

By default, preview images render at their original size. To limit image dimensions in the backend Page module, add a custom backend stylesheet:

EXT:your_sitepackage/Resources/Public/Css/backend.css
/* Limit RTE image preview size in Page module */
.t3-page-ce-body img {
    max-width: 200px;
    max-height: 150px;
}
Copied!

Register the stylesheet in your site package:

EXT:your_sitepackage/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['BE']['stylesheets']['your_sitepackage']
    = 'EXT:your_sitepackage/Resources/Public/Css/backend.css';
Copied!

Advanced Configuration 

Advanced configuration options including custom styles, performance optimization, extension settings, and best practices.

CKEditor Style Configuration 

Adding Image Styles 

Define custom styles for images:

EXT:my_ext/Configuration/RTE/Default.yaml
editor:
  config:
    style:
      definitions:
        - name: 'Image Left'
          element: 'img'
          classes: ['float-left', 'mr-3']
        - name: 'Image Right'
          element: 'img'
          classes: ['float-right', 'ml-3']
        - name: 'Image Center'
          element: 'img'
          classes: ['d-block', 'mx-auto']
        - name: 'Full Width'
          element: 'img'
          classes: ['w-100']
Copied!

Style Groups 

Organize styles in groups:

Style group definitions
editor:
  config:
    style:
      definitions:
        # ... style definitions ...

      # Group styles in dropdown
      groupDefinitions:
        - name: 'Image Alignment'
          styles: ['Image Left', 'Image Right', 'Image Center']
        - name: 'Image Size'
          styles: ['Full Width', 'Half Width']
Copied!

Image Dialog Fields 

The image properties dialog provides the following fields when an editor double-clicks an image or inserts a new one:

Field Description
Width / Height Display dimensions in pixels. Aspect ratio is locked automatically.
Quality (Scaling) Dropdown: No Scaling, Standard (1.0x), Retina (2.0x), Ultra (3.0x), Print (6.0x).
Title Advisory title attribute. Checkbox toggles override of FAL metadata default.
Alt Text Alternative text attribute. Checkbox toggles override of FAL metadata default.
Caption Figcaption text. When set, the image is wrapped in a <figure>/<figcaption> element.
Click Behavior Radio buttons: None, Enlarge (zoom/lightbox), or Link (custom URL via link browser). Sub-fields for CSS class, link URL, target, and title appear based on selection.

Extension Configuration 

Configure extension behavior in Admin Tools > Settings > Extension Configuration or settings.php:

fetchExternalImages

fetchExternalImages
Type
boolean
Default
true
Path
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image']['fetchExternalImages']

Controls whether external image URLs are automatically fetched and uploaded to the backend user's upload folder.

When enabled, pasting external image URLs into the editor will trigger automatic download and upload to FAL.

Options:

  • true: External image URLs are fetched and uploaded to BE user's uploads folder.
  • false: External URLs remain as external links (not recommended for security).

Example:

settings.php or LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'] = [
    'fetchExternalImages' => true,
];
Copied!

Quality and Image Processing 

New in version 13.1.5

Quality multiplier support, noScale mode, and SVG handling.

Quality Multipliers 

The extension supports quality multipliers for high-DPI displays and print output:

Quality selector dropdown in image properties dialog

Quality multiplier dropdown in the image properties dialog

Quality Multiplier Use Case
No Scaling N/A Skip processing, use original file unchanged.
Low 0.9x Performance optimization.
Standard 1.0x Default web display.
Retina 2.0x High-DPI displays (Retina).
Ultra 3.0x Extra sharp display.
Print 6.0x Print-quality output.

Editors can select quality from a dropdown in the image dialog. The selection persists with the image and affects frontend processing dimensions.

Image properties dialog showing all configuration options including Click Behavior dropdown

Image properties dialog with dimensions, quality, title, alt text, and click behavior options

TSConfig for quality-based processing:

Maximum dimensions for image processing
RTE.default.buttons.image.options.magic {
    maxWidth = 1920
    maxHeight = 1080
}
Copied!

When quality multipliers are applied, the actual processing dimensions are calculated as: display dimensions × quality multiplier, capped by maxWidth/maxHeight.

noScale Mode 

Skip TYPO3 image processing entirely and use original files:

  • Manual toggle: Editors can enable No Scaling in the image dialog.
  • SVG auto-detection: SVG files automatically use noScale mode.
  • Original file delivery: The original file URL is used instead of processed variants.

This is useful for:

  • Vector graphics (SVG) that should not be rasterized.
  • Images already optimized for web delivery.
  • Situations where exact original quality is required.

SVG Support 

The extension handles SVG files with special processing:

  • Dimension extraction: Reads dimensions from viewBox or width/ height attributes.
  • No rasterization: SVGs are never processed through ImageMagick/GraphicsMagick.
  • Automatic noScale: SVG files automatically skip image processing.

Performance Optimization 

Image Processing Configuration 

LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX'] = [
    'processor' => 'ImageMagick',
    'processor_path' => '/usr/bin/',
    'processor_enabled' => true,
    'processor_effects' => true,

    // Image quality
    'jpg_quality' => 85,

    // Maximum dimensions
    'imagefile_ext' => 'gif,jpg,jpeg,png,webp',
];
Copied!

Processed File Cache 

TYPO3 caches processed images in _processed_/ folder. Clear if needed:

Clear TYPO3 caches
# TYPO3 CLI
./vendor/bin/typo3 cache:flush --group=pages
Copied!

Performance Best Practices 

  1. Configure appropriate image processing quality ( jpg_quality: 85).
  2. Enable TYPO3 page caching for content with images.
  3. Use WebP format where supported.
  4. Implement lazy loading for images below the fold.
  5. Set reasonable maximum dimensions in TSConfig.
  6. Consider using CDN for image delivery in production.

Example Configurations 

Minimal Setup 

Minimal RTE with just image support
imports:
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Minimal.yaml" }
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    removePlugins: null
    toolbar:
      items: [insertimage]
Copied!

Best Practices 

Configuration Best Practices 

  1. Start with minimal configuration and add features incrementally.
  2. Test in staging environment before deploying to production.
  3. Use separate RTE presets for different content types.
  4. Enable caching for processed images.
  5. Set appropriate maxWidth/maxHeight to prevent oversized images.
  6. Configure lazy loading for better performance.
  7. Use meaningful style names that reflect intent, not appearance.
  8. Document custom configurations for team members.

Security Considerations 

  • Restrict allowed file extensions to safe image formats.
  • Configure appropriate file mounts for backend users.
  • Review and limit upload folder permissions.
  • Validate image dimensions and file sizes.
  • Keep extension and TYPO3 core up to date.

Maintenance 

  • Regularly clear processed image cache.
  • Monitor storage usage in upload folders.
  • Review and clean unused images periodically.
  • Keep documentation of custom configurations.
  • Test after TYPO3 core or extension updates.

Using with Third-Party Extensions 

Automatic Support for ALL Extensions (v13.x+)

This extension automatically configures RTE image support for all tables with RTE-enabled text fields, including:

  • TYPO3 core tables ( tt_content, sys_template).
  • Third-party extensions ( tx_news_domain_model_news, etc.).
  • Custom extension tables.

No manual configuration is required. The extension uses a PSR-14 event listener that automatically adds the rtehtmlarea_images soft reference to all RTE fields during TCA compilation.

Extension Configuration 

You can customize the automatic behavior via Extension Configuration:

Admin Tools > Settings > Extension Configuration > rte_ckeditor_image

enableAutomaticRteSoftref

enableAutomaticRteSoftref
Type
boolean
Default
1 (enabled)

Primary toggle to enable or disable automatic RTE softref processing.

When enabled, the extension automatically adds rtehtmlarea_images soft reference to all RTE-enabled text fields across all tables.

excludedTables

excludedTables
Type
string (comma-separated)
Default
(empty)

Comma-separated list of table names to exclude from automatic processing.

Example: tx_form_formframework,sys_template

Use this if specific tables should not have automatic softref processing.

includedTablesOnly

includedTablesOnly
Type
string (comma-separated)
Default
(empty)

Allowlist mode: If set, ONLY these tables will be processed.

Example: tt_content,tx_news_domain_model_news,tx_myext_article

This overrides the excludedTables setting. Leave empty to process all tables (recommended).

enableAutomaticPreviewRenderer

enableAutomaticPreviewRenderer
Type
boolean
Default
1 (enabled)

New in version 13.5.0

Toggle automatic registration of RteImagePreviewRenderer for all record types with RTE-enabled bodytext fields.

When enabled, the extension registers a custom preview renderer that preserves <img> tags in the page module preview. Without this, TYPO3's default StandardContentPreviewRenderer strips images via strip_tags().

The preview renderer also detects broken image references and displays a warning callout above the content preview (see Image Reference Validation).

This setting respects excludedTables and includedTablesOnly.

Configuration Examples 

Exclude Specific Tables:

settings.php or LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'] = [
    'enableAutomaticRteSoftref' => true,
    'excludedTables' => 'tx_form_formframework,sys_template',
];
Copied!

Allowlist Mode (Only Specific Tables):

settings.php or LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'] = [
    'enableAutomaticRteSoftref' => true,
    'includedTablesOnly' => 'tt_content,tx_news_domain_model_news',
];
Copied!

Disable Automatic Processing:

settings.php or LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'] = [
    'enableAutomaticRteSoftref' => false,
];
Copied!

Disable Preview Renderer Only:

settings.php or LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'] = [
    'enableAutomaticRteSoftref' => true,
    'enableAutomaticPreviewRenderer' => false,
];
Copied!

Troubleshooting Third-Party Extension Issues 

Images Disappear When Saving 

Symptom: Images inserted in RTE fields disappear after saving the record.

Cause: Automatic softref processing may be disabled, or the table is excluded.

Solution:

  1. Verify automatic processing is enabled:

    Check extension configuration
    // Check extension configuration
    $config = $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'];
    // enableAutomaticRteSoftref should be true (default)
    Copied!
  2. Check if the table is excluded:

    Check table exclusion settings
    // Check excludedTables and includedTablesOnly settings
    debug($config['excludedTables']);
    debug($config['includedTablesOnly']);
    Copied!
  3. Verify soft reference is configured in TCA:

    Verify TCA softref configuration
    // In TYPO3 backend console or debug output
    debug($GLOBALS['TCA']['your_table']['columns']['your_field']['config']['softref']);
    // Should output: "rtehtmlarea_images" or include it in comma-separated list
    Copied!
  4. Check if data-htmlarea-file-uid attribute is preserved:

    Check database content
    -- Check database content
    SELECT bodytext FROM tx_news_domain_model_news WHERE uid = 123;
    -- Should contain: data-htmlarea-file-uid="456"
    Copied!
  5. Clear all caches and retry:

    Clear all caches
    ./vendor/bin/typo3 cache:flush
    Copied!

Automatic Processing Not Working 

Symptom: RTE images are not tracked automatically in custom tables.

Cause: Event listener not registered, caches not cleared, or configuration issue.

Solution:

  1. Verify the event listener is registered:

    Check RteSoftrefEnforcer class
    # Check if RteSoftrefEnforcer class exists
    ls Classes/Listener/TCA/RteSoftrefEnforcer.php
    Copied!
  2. Verify Services.yaml configuration:

    Check listener registration
    # Check listener registration
    grep -A 5 "RteSoftrefEnforcer" Configuration/Services.yaml
    Copied!
  3. Clear all caches:

    Clear all caches
    ./vendor/bin/typo3 cache:flush
    Copied!
  4. Check if the field is RTE-enabled:

    Verify RTE field configuration
    // Field must have type='text' AND enableRichtext=true
    debug($GLOBALS['TCA']['your_table']['columns']['your_field']['config']);
    Copied!

data-htmlarea-file-uid Attribute Missing 

Symptom: Images render but the data-htmlarea-file-uid attribute is missing from saved content.

Cause: Soft reference parser not registered or not being invoked.

Solution:

  1. Verify soft reference parser is registered:

    Check softreference.parser registration
    # Check Services.yaml for softreference.parser
    grep -A 3 "softreference.parser" Configuration/Services.yaml
    Copied!
  2. Verify soft reference is in TCA (see "Images Disappear When Saving" above).
  3. Clear all caches:

    Clear all caches
    ./vendor/bin/typo3 cache:flush
    Copied!

Images Not Processed in Frontend 

Symptom: Images appear as <img> tags with data-htmlarea-file-uid in frontend HTML.

Cause: TypoScript configuration missing or incorrect.

Solution: Ensure TypoScript static template is included:

Include static template
# Include static template in your root template
# Template > Info/Modify > Edit whole template record > Includes
# Select: "CKEditor Image Support" for "Include static (from extensions)"
Copied!

Security 

New in version 13.1.5

Comprehensive security measures including protocol blocking, file validation, and XSS prevention.

Security features and best practices for the RTE CKEditor Image extension.

Security architecture 

The extension implements security at multiple layers:

1. Input Validation2. XSS Prevention3. Output RenderingImageResolverServiceProtocol blockingFile visibility checkFAL validationImageRenderingDtohtmlspecialchars encodingReadonly propertiesImmutable after creationFluid TemplatesPre-validated DTOsAuto-escaping enabledvalidated dataimmutable data
Security layers architecture

Protocol blocking 

The ImageResolverService blocks dangerous URL protocols:

  • javascript: - Prevents script execution via URLs.
  • file: - Prevents local file system access.
  • data:text/html - Prevents HTML injection via data URIs.
  • vbscript: - Prevents VBScript execution (legacy IE).

Safe protocols allowed:

  • http:// and https:// - Standard web URLs.
  • / - Relative paths.
  • t3:// - TYPO3 link handler URLs.

File visibility validation 

Before rendering, the extension validates:

  1. File exists: FAL file reference must be valid.
  2. File accessible: File must not be hidden or restricted.
  3. Storage accessible: File storage must be publicly accessible.

If validation fails, the original unprocessed content is returned.

XSS prevention 

All user-controlled content is sanitized:

Caption text 

Caption sanitization
// Caption is sanitized with htmlspecialchars()
$caption = htmlspecialchars($rawCaption, ENT_QUOTES | ENT_HTML5, 'UTF-8');
Copied!

Alt and title attributes 

Alt and title text are sanitized before inclusion in HTML output.

CSS classes 

CSS class names are validated and encoded to prevent attribute injection.

Immutable DTOs 

The ImageRenderingDto and LinkDto are declared as readonly:

Readonly DTO declaration
final readonly class ImageRenderingDto
{
    // Properties cannot be modified after construction
}
Copied!

This ensures:

  • Data integrity: Validated data cannot be corrupted.
  • Audit trail: Security validation happens once, at creation.
  • Thread safety: No race conditions on property access.

SVG security 

Recommendations:

  1. Sanitize before upload: Use server-side SVG sanitization libraries.
  2. Restrict uploads: Consider limiting SVG uploads to trusted users.
  3. Content Security Policy: Implement CSP headers to mitigate XSS risks.

Best practices 

File upload restrictions 

Configure allowed file extensions in TYPO3:

config/system/settings.php
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] = 'gif,jpg,jpeg,png,webp';
Copied!

Restrict in RTE configuration:

EXT:my_extension/Configuration/RTE/Custom.yaml
editor:
  externalPlugins:
    typo3image:
      allowedExtensions: "jpg,jpeg,png,gif,webp"
Copied!

Backend user permissions 

  • Configure appropriate file mounts for backend users.
  • Restrict upload folder access to necessary directories.
  • Use TYPO3 backend user groups for granular control.

Content Security Policy 

Implement CSP headers for additional protection:

.htaccess CSP configuration
Header set Content-Security-Policy "default-src 'self'; img-src 'self' data: https:;"
Copied!

External image fetching 

The fetchExternalImages option downloads external images to FAL:

config/system/settings.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'] = [
    'fetchExternalImages' => true,  // Recommended: download to local storage
];
Copied!

This prevents:

  • Hotlinking to external resources.
  • Privacy leaks via external image loading.
  • Broken images when external sources change.

Regular updates 

Keep the extension and TYPO3 core updated to receive security patches:

Update extension via Composer
composer update netresearch/rte-ckeditor-image
Copied!

Security reporting 

Report security vulnerabilities to:

  • TYPO3 Security Team: security@typo3.org
  • Extension maintainer: Via GitHub issues (for non-critical issues)

Examples & Use Cases 

Practical, ready-to-use examples for common implementation scenarios with rte_ckeditor_image.

Table of Contents

Examples by Topic 

🚀 Basic Integration 

Minimal setup guide for getting basic image functionality working quickly. Perfect for new installations and quick starts.

🔗 Linked Images 

Configure click behavior, link browser integration, and URL parameters. Complete guide for creating linked and clickable images.

🎨 Image Styles 

Configure custom image styles with Bootstrap classes, CSS groups, and style dropdowns. Includes both framework-based and custom CSS approaches.

📱 Responsive Images 

Implement responsive images with automatic srcset generation and multiple breakpoints. Complete PHP implementation with result examples.

⭐ Advanced Features 

Add lightbox functionality with PhotoSwipe and implement lazy loading for performance. Includes both native browser lazy loading and Intersection Observer fallbacks.

🔌 Custom Extensions 

Extend the image plugin with custom dialog fields, external image handling, multi-language support, and automatic backend processing hooks.

✅ Testing 

Functional and unit test examples for ensuring quality and preventing regressions. Includes controller tests and database hook tests.

🎭 Template Overrides 

Customize image rendering output by overriding Fluid templates. Includes Bootstrap integration, PhotoSwipe lightbox, and lazy loading examples.

Basic Integration 

Quick start guide demonstrating the installation and basic customization options.

Installation 

Objective: Get image functionality working in backend and frontend

Step 1: Install Extension 

composer require netresearch/rte-ckeditor-image:^13.0
Copied!

The extension automatically configures the backend RTE:

  • ✅ Registers the rteWithImages preset
  • ✅ Configures toolbar with insertimage button for all sites

Step 2: Include TypoScript 

For frontend rendering, include the TypoScript:

Option A: Static Template (Recommended)

  1. Go to WEB > Template module
  2. Select your root page, edit the template
  3. In Includes tab, add: CKEditor Image Support

Option B: Direct Import

@import 'EXT:rte_ckeditor_image/Configuration/TypoScript/ImageRendering/setup.typoscript'
Copied!

Verification 

  1. Backend: Log into TYPO3 backend → Edit any content element → RTE should show image button
  2. Frontend: Insert image in RTE → Save → View frontend → Image renders correctly
Image insertion demo

The insertimage button is automatically available after installation

Custom Configuration (Optional) 

If you need to customize the RTE configuration, you can create your own preset:

Custom Preset 

EXT:my_site/Configuration/RTE/Custom.yaml
imports:
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Default.yaml" }
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    toolbar:
      items:
        - heading
        - '|'
        - bold
        - italic
        - '|'
        - insertimage
        - link
Copied!

Register Custom Preset 

EXT:my_site/ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['RTE']['Presets']['custom']
    = 'EXT:my_site/Configuration/RTE/Custom.yaml';
Copied!

Enable Custom Preset 

Configuration/page.tsconfig
RTE.default.preset = custom
Copied!

Linked Images 

New in version 13.5.0

Complete link support with TYPO3 link browser integration, additional parameters, and proper URL handling.

This guide covers how to create and configure linked images in CKEditor, including the link browser integration, click behavior options, and URL parameters.

Click Behavior Options 

The image dialog provides three click behavior options:

Click Behavior options showing None, Enlarge, and Link radio buttons

The Click Behavior section with three options: None, Enlarge, and Link

None

None

Image is not clickable. No link is applied.

Enlarge

Enlarge

Opens the full-size image in a lightbox/popup. Uses the enableZoom attribute and TYPO3's built-in popup handling.

See Advanced Features for lightbox integration.

URL Parameter Handling 

Additional parameters are intelligently appended to URLs:

Basic Examples 

# URL without query string
/page + &L=1 → /page?L=1

# URL with existing query string
/page?foo=bar + &L=1 → /page?foo=bar&L=1

# URL with fragment (preserved at end)
/page#section + &L=1 → /page?L=1#section

# URL with both query and fragment
/page?foo=bar#section + &L=1 → /page?foo=bar&L=1#section
Copied!

PHP Implementation 

The LinkDto::getUrlWithParams() method handles all edge cases:

use Netresearch\RteCKEditorImage\Domain\Model\LinkDto;

$link = new LinkDto(
    url: 'https://example.com/page?existing=param#section',
    target: '_blank',
    class: 'external',
    params: '&utm_source=ckeditor&L=1',
    isPopup: false,
    jsConfig: null
);

// Returns: https://example.com/page?existing=param&utm_source=ckeditor&L=1#section
$fullUrl = $link->getUrlWithParams();
Copied!

Frontend Rendering 

Linked images are rendered with the configured attributes. Internal links retain their original rel (or none):

Internal link with target="_blank" (no rel injected)
<a href="/page?L=1#section"
   target="_blank"
   title="Click to view details"
   class="image-link external">
    <img src="/fileadmin/_processed_/image.jpg"
         alt="Product image"
         width="800"
         height="600" />
</a>
Copied!

External links opening a new browsing context receive rel="noreferrer" automatically — mirroring TYPO3 typolink security semantics. See External link security (rel="noreferrer").

External link with target="_blank" (rel="noreferrer" injected)
<a href="https://example.com"
   target="_blank"
   rel="noreferrer">
    <img src="/fileadmin/_processed_/image.jpg"
         alt="Product image"
         width="800"
         height="600" />
</a>
Copied!

Fluid Template Access 

In custom Fluid templates, access link properties via the link object:

<f:if condition="{image.link}">
    <a href="{image.link.urlWithParams}"
       target="{image.link.target}"
       title="{image.link.title}"
       class="{image.link.class}">
        <f:render partial="Image" arguments="{image: image}" />
    </a>
</f:if>
Copied!

Clearing Stale Attributes 

When selecting a new link from the link browser, all previous link attributes are cleared automatically. This prevents stale values from being retained:

// Before: Image linked to /old-page with target="_blank" and class="old-class"
// User selects new link: /new-page with no target or class

// After: Only the new URL is set, old attributes are cleared
// linkHref: '/new-page'
// linkTarget: null (cleared)
// linkClass: null (cleared)
// linkTitle: null (cleared)
// linkParams: null (cleared)
Copied!

This behavior ensures editors always see the actual link configuration without inherited values from previous links.

Translations 

All link-related UI labels are translatable. The following keys are available in locallang_be.xlf:

<!-- Link field labels -->
<trans-unit id="labels.ckeditor.linkUrl">
<trans-unit id="labels.ckeditor.linkTarget">
<trans-unit id="labels.ckeditor.linkTitle">
<trans-unit id="labels.ckeditor.linkCssClass">
<trans-unit id="labels.ckeditor.linkParams">
<trans-unit id="labels.ckeditor.linkParamsPlaceholder">

<!-- Target options -->
<trans-unit id="labels.ckeditor.linkTargetDefault">
<trans-unit id="labels.ckeditor.linkTargetBlank">
<trans-unit id="labels.ckeditor.linkTargetTop">
<trans-unit id="labels.ckeditor.linkTargetSelf">
<trans-unit id="labels.ckeditor.linkTargetParent">

<!-- Click behavior labels -->
<trans-unit id="labels.ckeditor.clickBehavior">
<trans-unit id="labels.ckeditor.clickBehaviorNone">
<trans-unit id="labels.ckeditor.clickBehaviorEnlarge">
<trans-unit id="labels.ckeditor.clickBehaviorLink">
Copied!

Translations are provided for 31 languages. See Resources/Private/Language/ for the complete list.

Troubleshooting 

Parameters not appended correctly 

Symptom: URL shows /page?foo=bar?L=1 (double question mark).

Cause: Parameters are prefixed with ? instead of &.

Solution: Always use & prefix for additional parameters. The getUrlWithParams() method handles normalization automatically.

Image styles 

This extension provides two ways to apply styles to images in the editor. Understanding the difference is important for choosing the right approach.

Overview: Two styling approaches 

Native CKEditor style dropdown (advanced) 

For custom styles beyond basic alignment, you can use the native CKEditor style dropdown. This requires additional configuration.

Requirements 

  1. Add style to your toolbar configuration.
  2. Define style definitions for img elements.
  3. Ensure StyleUtils and GeneralHtmlSupport plugins are loaded (automatic).

Configuration example 

EXT:my_site/Configuration/RTE/MyPreset.yaml
imports:
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Default.yaml" }

editor:
  config:
    # Add 'style' to the toolbar
    toolbar:
      items:
        - style           # <-- Required for style dropdown
        - heading
        - '|'
        - insertimage
        - link
        - '|'
        - bold
        - italic

    # Define styles for images
    style:
      definitions:
        - name: 'Float Left (Bootstrap)'
          element: 'img'
          classes: ['float-start', 'me-3', 'mb-3']
        - name: 'Float Right (Bootstrap)'
          element: 'img'
          classes: ['float-end', 'ms-3', 'mb-3']
        - name: 'Centered'
          element: 'img'
          classes: ['d-block', 'mx-auto']
        - name: 'Rounded'
          element: 'img'
          classes: ['rounded']
        - name: 'Thumbnail'
          element: 'img'
          classes: ['img-thumbnail']
Copied!

How to use 

  1. Insert an image.
  2. Select the image (click on it).
  3. Open the Style dropdown in the main toolbar.
  4. Select a style from the dropdown.

Style groups 

Organize styles into groups for better UX:

Grouped style definitions
editor:
  config:
    style:
      definitions:
        - name: 'Float Left'
          element: 'img'
          classes: ['float-start', 'me-3']
        - name: 'Float Right'
          element: 'img'
          classes: ['float-end', 'ms-3']
        - name: 'Thumbnail'
          element: 'img'
          classes: ['img-thumbnail']
        - name: 'Rounded'
          element: 'img'
          classes: ['rounded']

      groupDefinitions:
        - name: 'Image Alignment'
          styles: ['Float Left', 'Float Right']
        - name: 'Image Style'
          styles: ['Thumbnail', 'Rounded']
Copied!

Troubleshooting 

Style dropdown not appearing 

Symptoms: No "Styles" dropdown visible in the toolbar.

Solution: Add style to your toolbar configuration:

editor:
  config:
    toolbar:
      items:
        - style    # <-- Add this
        - heading
        - insertimage
        # ...
Copied!

Styles disabled when image selected 

Symptoms: Style dropdown shows options but they're grayed out for images.

Cause: Missing dependencies or incorrect style definitions.

Solution: Verify your style definitions target img elements:

style:
  definitions:
    - name: 'My Image Style'
      element: 'img'      # <-- Must be 'img'
      classes: ['my-class']
Copied!

Balloon toolbar not showing 

Symptoms: No toolbar appears when clicking an image.

Solution: Ensure you're using the rteWithImages preset or have configured typo3image.toolbar in your RTE configuration.

Check your preset includes the image plugin
imports:
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Default.yaml" }
Copied!

Choosing the right approach 

Feature Balloon Toolbar (Built-in) Style Dropdown (Native)
Configuration None required YAML configuration needed
Styles available 5 style options (3 alignment + 2 display) Custom (you define them)
Location Balloon above image Main toolbar dropdown
Best for Basic alignment Custom Bootstrap/CSS classes
CSS included Yes (image-alignment.css) No (provide your own)

Recommendation: Start with the built-in balloon toolbar. Only configure the native style dropdown if you need custom CSS classes beyond the basic alignments.

Responsive Images 

Examples for implementing responsive images with srcset and automatic generation.

Automatic srcset Generation 

Objective: Generate responsive images with srcset

TypoScript Setup 

EXT:my_site/Configuration/TypoScript/setup.typoscript
lib.parseFunc_RTE {
    tags.img = TEXT
    tags.img {
        current = 1
        preUserFunc = MyVendor\MySite\UserFunc\ResponsiveImageRenderer->render
    }
}
Copied!

PHP Implementation 

EXT:my_site/Classes/UserFunc/ResponsiveImageRenderer.php
namespace MyVendor\MySite\UserFunc;

use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

class ResponsiveImageRenderer
{
    public function render(
        string $content,
        array $conf,
        ContentObjectRenderer $cObj
    ): string {
        // Parse img tag
        if (!preg_match('/<img([^>]*)>/i', $content, $imgMatch)) {
            return $content;
        }

        // Extract data-htmlarea-file-uid
        if (!preg_match('/data-htmlarea-file-uid="(\d+)"/', $imgMatch[1], $uidMatch)) {
            return $content;
        }

        $fileUid = (int)$uidMatch[1];

        // Get FAL file
        $fileRepository = GeneralUtility::makeInstance(FileRepository::class);
        try {
            $file = $fileRepository->findByUid($fileUid);
        } catch (\Exception $e) {
            return $content;
        }

        // Generate responsive variants
        $breakpoints = [
            'xs' => 480,
            'sm' => 768,
            'md' => 992,
            'lg' => 1200,
            'xl' => 1920
        ];

        $srcsetParts = [];
        foreach ($breakpoints as $name => $width) {
            $processedFile = $file->process(
                ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
                ['width' => $width]
            );
            $srcsetParts[] = $processedFile->getPublicUrl() . ' ' . $width . 'w';
        }

        $srcset = implode(', ', $srcsetParts);
        $sizes = '(max-width: 768px) 100vw, (max-width: 992px) 50vw, 33vw';

        // Replace img tag with srcset version
        $newImg = str_replace(
            '<img',
            '<img srcset="' . htmlspecialchars($srcset) . '" sizes="' . $sizes . '"',
            $imgMatch[0]
        );

        return str_replace($imgMatch[0], $newImg, $content);
    }
}
Copied!

Result HTML 

<img
    src="/fileadmin/image.jpg"
    srcset="/fileadmin/_processed_/image_480.jpg 480w,
            /fileadmin/_processed_/image_768.jpg 768w,
            /fileadmin/_processed_/image_992.jpg 992w,
            /fileadmin/_processed_/image_1200.jpg 1200w,
            /fileadmin/_processed_/image_1920.jpg 1920w"
    sizes="(max-width: 768px) 100vw, (max-width: 992px) 50vw, 33vw"
    alt="Image description"
/>
Copied!

Result: Automatic responsive images ✅

Advanced Features 

Examples for implementing lightbox functionality and lazy loading for performance optimization.

Lazy Loading 

Native Lazy Loading 

Objective: Improve page load performance with native lazy loading

TypoScript Setup 

lib.parseFunc_RTE {
    nonTypoTagStdWrap.HTMLparser.tags.img {
        fixAttrib {
            loading {
                set = lazy
            }
            # Remove internal attributes
            data-htmlarea-file-uid.unset = 1
            data-htmlarea-file-table.unset = 1
            # Keep zoom attributes for popup/lightbox rendering
            # data-htmlarea-zoom.unset = 1
        }
    }
}
Copied!

Result HTML 

<img src="..." loading="lazy" alt="..." />
Copied!

Intersection Observer Fallback 

For older browsers:

TypoScript 

page.includeJSFooterlibs.lazyload = EXT:my_site/Resources/Public/JavaScript/lazyload.js

lib.parseFunc_RTE {
    nonTypoTagStdWrap.HTMLparser.tags.img {
        fixAttrib {
            class {
                list = lazyload
            }
            data-src {
                # Copy src to data-src
                stdWrap.field = src
            }
            src {
                # Set placeholder
                set = data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3C/svg%3E
            }
        }
    }
}
Copied!

JavaScript 

EXT:my_site/Resources/Public/JavaScript/lazyload.js
document.addEventListener('DOMContentLoaded', function() {
    const imageObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.classList.remove('lazyload');
                imageObserver.unobserve(img);
            }
        });
    });

    document.querySelectorAll('img.lazyload').forEach(img => {
        imageObserver.observe(img);
    });
});
Copied!

Result: Progressive image loading ✅

Custom Extensions 

Advanced examples for extending the image plugin with custom fields, external image handling, multi-language support, and backend processing.

Custom Image Dialog 

Extended Image Properties 

Objective: Add custom fields to image dialog

CKEditor Plugin Extension 

EXT:my_site/Resources/Public/JavaScript/Plugins/extended-typo3image.js
import { Plugin } from '@ckeditor/ckeditor5-core';

export default class ExtendedTypo3Image extends Plugin {
    static get pluginName() {
        return 'ExtendedTypo3Image';
    }

    init() {
        const editor = this.editor;

        // Extend schema with custom attributes
        editor.model.schema.extend('typo3image', {
            allowAttributes: ['customCaption', 'customCopyright']
        });

        // Add upcast conversion
        editor.conversion.for('upcast').attributeToAttribute({
            view: 'data-custom-caption',
            model: 'customCaption'
        });

        // Add downcast conversion
        editor.conversion.for('downcast').attributeToAttribute({
            model: 'customCaption',
            view: 'data-custom-caption'
        });

        // Modify image dialog
        editor.on('typo3image:dialog', (evt, { dialog, modelElement }) => {
            // Add custom fields to dialog using native DOM
            const group1 = document.createElement('div');
            group1.className = 'form-group';
            const label1 = document.createElement('label');
            label1.textContent = 'Custom Caption';
            group1.appendChild(label1);
            const captionInput = document.createElement('input');
            captionInput.type = 'text';
            captionInput.className = 'form-control';
            captionInput.name = 'customCaption';
            captionInput.value = modelElement.getAttribute('customCaption') || '';
            group1.appendChild(captionInput);

            const group2 = document.createElement('div');
            group2.className = 'form-group';
            const label2 = document.createElement('label');
            label2.textContent = 'Copyright';
            group2.appendChild(label2);
            const copyrightInput = document.createElement('input');
            copyrightInput.type = 'text';
            copyrightInput.className = 'form-control';
            copyrightInput.name = 'customCopyright';
            copyrightInput.value = modelElement.getAttribute('customCopyright') || '';
            group2.appendChild(copyrightInput);

            dialog.el.appendChild(group1);
            dialog.el.appendChild(group2);

            // Override dialog.get() to include custom fields
            const originalGet = dialog.get;
            dialog.get = function() {
                const attrs = originalGet.call(this);
                attrs.customCaption = captionInput.value;
                attrs.customCopyright = copyrightInput.value;
                return attrs;
            };
        });
    }
}
Copied!

Register Plugin 

Configuration/RTE/Extended.yaml
editor:
  config:
    importModules:
      - '@my-vendor/my-site/extended-typo3image.js'
Copied!

Result: Custom image metadata fields ✅

External Image Handling 

Fetch and Upload External Images 

Extension Configuration 

settings.php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image'] = [
    'fetchExternalImages' => true,
];
Copied!

Custom Upload Folder 

User TSConfig
options.defaultUploadFolder = 1:user_upload/rte_images/
Copied!

Custom Fetch Handler 

EXT:my_site/Classes/Hooks/CustomImageFetchHook.php
namespace MyVendor\MySite\Hooks;

use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;

class CustomImageFetchHook
{
    public function postProcessExternalImage(
        string $externalUrl,
        File $uploadedFile,
        Folder $targetFolder
    ): void {
        // Add custom metadata
        $uploadedFile->updateProperties([
            'title' => 'Imported from ' . parse_url($externalUrl, PHP_URL_HOST),
            'description' => 'Automatically fetched external image',
        ]);

        // Trigger image optimization
        $this->optimizeImage($uploadedFile);
    }

    protected function optimizeImage(File $file): void
    {
        // Custom optimization logic
        // e.g., compress, resize, convert format
    }
}
Copied!

Register Hook 

ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['rte_ckeditor_image']['postProcessExternalImage'][]
    = \MyVendor\MySite\Hooks\CustomImageFetchHook::class . '->postProcessExternalImage';
Copied!

Result: Automatic external image import ✅

Multi-Language Setup 

Language-Specific Image Variants 

Page TSConfig 

[siteLanguage("languageId") == 0]
    # Default language (English)
    RTE.default.preset = default
[END]

[siteLanguage("languageId") == 1]
    # German
    RTE.default.preset = german
    RTE.default.buttons.image.options.defaultUploadFolder = 1:user_upload/de/
[END]

[siteLanguage("languageId") == 2]
    # French
    RTE.default.preset = french
    RTE.default.buttons.image.options.defaultUploadFolder = 1:user_upload/fr/
[END]
Copied!

RTE Configuration 

Configuration/RTE/German.yaml
imports:
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    language: de
    style:
      definitions:
        - name: 'Bild Links'
          element: 'img'
          classes: ['float-left']
        - name: 'Bild Rechts'
          element: 'img'
          classes: ['float-right']
Copied!

Result: Language-specific configurations ✅

Custom Backend Processing 

Automatic Image Optimization 

Custom Hook 

EXT:my_site/Classes/Hooks/ImageOptimizationHook.php
namespace MyVendor\MySite\Hooks;

use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ImageOptimizationHook
{
    public function processDatamap_afterDatabaseOperations(
        string $status,
        string $table,
        string $id,
        array $fieldArray,
        DataHandler $dataHandler
    ): void {
        if ($table !== 'tt_content') {
            return;
        }

        foreach ($fieldArray as $field => $value) {
            if (!$this->isRteField($table, $field)) {
                continue;
            }

            // Find images in RTE content
            preg_match_all('/data-htmlarea-file-uid="(\d+)"/', $value, $matches);

            foreach ($matches[1] as $fileUid) {
                $this->optimizeImage((int)$fileUid);
            }
        }
    }

    protected function isRteField(string $table, string $field): bool
    {
        $tcaConfig = $GLOBALS['TCA'][$table]['columns'][$field]['config'] ?? [];
        return ($tcaConfig['enableRichtext'] ?? false) === true;
    }

    protected function optimizeImage(int $fileUid): void
    {
        $fileRepository = GeneralUtility::makeInstance(FileRepository::class);

        try {
            $file = $fileRepository->findByUid($fileUid);

            // Generate optimized variants
            $file->process(
                \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
                ['width' => 1920, 'height' => 1080]
            );

            // Generate WebP variant
            $file->process(
                \TYPO3\CMS\Core\Resource\ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
                ['width' => 1920, 'height' => 1080, 'fileExtension' => 'webp']
            );
        } catch (\Exception $e) {
            // Log error
        }
    }
}
Copied!

Register Hook 

ext_localconf.php
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
    = \MyVendor\MySite\Hooks\ImageOptimizationHook::class;
Copied!

Result: Automatic optimization on save ✅

Testing Examples 

Examples for writing functional and unit tests for the RTE CKEditor Image extension.

Functional Test 

Controller Test Example 

Tests/Functional/Controller/SelectImageControllerTest.php
namespace Netresearch\RteCKEditorImage\Tests\Functional\Controller;

use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;

class SelectImageControllerTest extends FunctionalTestCase
{
    protected $testExtensionsToLoad = [
        'typo3conf/ext/rte_ckeditor_image'
    ];

    /**
     * @test
     */
    public function infoActionReturnsJsonForValidFile(): void
    {
        // Import test data
        $this->importDataSet(__DIR__ . '/Fixtures/sys_file.xml');

        // Create request
        $request = $this->createRequest('/typo3/rte/wizard/selectimage')
            ->withQueryParams([
                'action' => 'info',
                'fileId' => 1,
                'table' => 'sys_file'
            ]);

        // Execute
        $response = $this->executeFrontendRequest($request);

        // Assert
        self::assertEquals(200, $response->getStatusCode());

        $json = json_decode((string)$response->getBody(), true);
        self::assertArrayHasKey('uid', $json);
        self::assertArrayHasKey('url', $json);
        self::assertEquals(1, $json['uid']);
    }
}
Copied!

Unit Test 

Database Hook Test Example 

Tests/Unit/Database/RteImagesDbHookTest.php
namespace Netresearch\RteCKEditorImage\Tests\Unit\Database;

use Netresearch\RteCKEditorImage\Database\RteImagesDbHook;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;

class RteImagesDbHookTest extends UnitTestCase
{
    /**
     * @test
     */
    public function processDatamapAddsImageAttributes(): void
    {
        $hook = new RteImagesDbHook(/* dependencies */);

        $fieldArray = ['bodytext' => '<img src="/fileadmin/image.jpg" />'];
        $hook->processDatamap_postProcessFieldArray('new', 'tt_content', '1', $fieldArray, $dataHandler);

        self::assertStringContainsString('alt=', $fieldArray['bodytext']);
    }
}
Copied!

Run Tests 

Execute Test Suites 

# Functional tests
./vendor/bin/phpunit -c Build/phpunit-functional.xml

# Unit tests
./vendor/bin/phpunit -c Build/phpunit-unit.xml
Copied!

Result: Automated testing suite ✅

Template Overrides 

New in version 13.1.5

The new Fluid-based rendering architecture allows complete customization of image output via template overrides.

Override the default Fluid templates to customize image rendering output for your site's design requirements.

Overview 

The extension provides six Fluid templates for different rendering contexts:

Template directory structure
Resources/Private/Templates/
├── Image/
│   ├── Standalone.html      # Basic image without wrapper
│   ├── WithCaption.html     # Image with <figure>/<figcaption>
│   ├── Link.html            # Image wrapped in <a> tag
│   ├── LinkWithCaption.html # Linked image with caption
│   ├── Popup.html           # Image with lightbox/popup link
│   └── PopupWithCaption.html # Popup image with caption
└── Partials/Image/
    ├── Tag.html             # <img> element partial
    ├── TagInFigure.html     # <img> without class (for figures)
    ├── Link.html            # <a> wrapper partial
    └── Figure.html          # <figure> wrapper partial
Copied!

Template selection 

The ImageRenderingService automatically selects the appropriate template:

Condition Template
No link, no caption Standalone.html
No link, has caption WithCaption.html
Has link, no popup, no caption Link.html
Has link, no popup, has caption LinkWithCaption.html
Has popup, no caption Popup.html
Has popup, has caption PopupWithCaption.html

Inline vs block rendering 

Images are rendered differently depending on whether they appear as block elements (<figure>) or inline elements (<img> within text flow). Understanding this distinction is important when overriding templates:

Block images (<figure>):
Processed by renderFigure(). The template handles everything including the link wrapper. Uses Link.html, LinkWithCaption.html, Popup.html, or PopupWithCaption.html.
Inline images (<img> in text flow):
Processed by renderImageAttributes() for the <img> element and renderInlineLink() for the <a> wrapper. The template only renders the <img> tag — always Standalone.html. The link wrapper is handled separately by the tags.a handler in parseFunc_RTE.
Popup images:
Both inline and block use Popup.html or PopupWithCaption.html because popup attributes (lightbox data, click handler) require special template handling.
Context Link Caption Handler Template
Block No No renderFigure() Standalone.html
Block No Yes renderFigure() WithCaption.html
Block Yes No renderFigure() Link.html
Block Yes Yes renderFigure() LinkWithCaption.html
Block Popup No renderFigure() Popup.html
Block Popup Yes renderFigure() PopupWithCaption.html
Inline No renderImageAttributes() Standalone.html
Inline Yes renderImageAttributes() + renderInlineLink() Standalone.html
Inline Popup renderImageAttributes() Popup.html

Setting up overrides 

Step 1: Create template directory 

In your site package, create the override directory:

Create template override directory
mkdir -p packages/my_sitepackage/Resources/Private/Templates/Image/
Copied!

Step 2: Configure TypoScript 

Add the template path to your TypoScript setup:

EXT:my_sitepackage/Configuration/TypoScript/setup.typoscript
lib.parseFunc_RTE.tags.img {
    # Add your templates with higher priority (higher number = higher priority)
    settings.templateRootPaths {
        10 = EXT:my_sitepackage/Resources/Private/Templates/
    }
    settings.partialRootPaths {
        10 = EXT:my_sitepackage/Resources/Private/Partials/
    }
    settings.layoutRootPaths {
        10 = EXT:my_sitepackage/Resources/Private/Layouts/
    }
}
Copied!

Step 3: Create override templates 

Copy and modify only the templates you need to customize.

Available DTO properties 

All templates receive the image variable containing an ImageRenderingDto:

Available template variables
<!-- Core properties -->
{image.src}                    <!-- Processed image URL -->
{image.width}                  <!-- Display width in pixels -->
{image.height}                 <!-- Display height in pixels -->
{image.alt}                    <!-- Alternative text -->
{image.title}                  <!-- Title attribute -->
{image.caption}                <!-- Caption text (XSS-sanitized) -->
{image.isMagicImage}           <!-- Whether TYPO3 processing applied -->

<!-- HTML attributes -->
{image.htmlAttributes.class}   <!-- CSS classes -->
{image.htmlAttributes.style}   <!-- Inline styles -->
{image.htmlAttributes.loading} <!-- lazy/eager -->

<!-- Link properties (when linked) -->
{image.link.url}               <!-- Link URL -->
{image.link.target}            <!-- Link target (_blank, etc.) -->
{image.link.class}             <!-- Link CSS classes -->
{image.link.isPopup}           <!-- Whether popup/lightbox -->
Copied!

See ImageRenderingDto for complete property documentation.

Example overrides 

Bootstrap 5 responsive image 

Override Standalone.html for Bootstrap 5 responsive images:

EXT:my_sitepackage/Resources/Private/Templates/Image/Standalone.html
<img src="{image.src}"
     alt="{image.alt}"
     width="{image.width}"
     height="{image.height}"
     class="img-fluid {image.htmlAttributes.class}"
     {f:if(condition: image.title, then: 'title="{image.title}"')}
     {f:if(condition: image.htmlAttributes.style, then: 'style="{image.htmlAttributes.style}"')}
     loading="lazy"
     decoding="async" />
Copied!

Figure with custom styling 

Override WithCaption.html for custom figure styling:

EXT:my_sitepackage/Resources/Private/Templates/Image/WithCaption.html
<figure class="content-image{f:if(condition: image.htmlAttributes.class, then: ' {image.htmlAttributes.class}')}">
    <img src="{image.src}"
         alt="{image.alt}"
         width="{image.width}"
         height="{image.height}"
         class="content-image__img"
         {f:if(condition: image.title, then: 'title="{image.title}"')}
         loading="lazy"
         decoding="async" />
    <figcaption class="content-image__caption">
        {image.caption}
    </figcaption>
</figure>
Copied!

PhotoSwipe lightbox integration 

Override Popup.html for PhotoSwipe v5 integration:

EXT:my_sitepackage/Resources/Private/Templates/Image/Popup.html
<a href="{image.link.url}"
   class="pswp-gallery__item {image.link.class}"
   data-pswp-width="{image.width}"
   data-pswp-height="{image.height}"
   {f:if(condition: image.link.target, then: 'target="{image.link.target}"')}>
    <img src="{image.src}"
         alt="{image.alt}"
         width="{image.width}"
         height="{image.height}"
         {f:if(condition: image.title, then: 'title="{image.title}"')}
         {f:if(condition: image.htmlAttributes.class, then: 'class="{image.htmlAttributes.class}"')}
         loading="lazy"
         decoding="async" />
</a>
Copied!

Lazy loading with placeholder 

Override Standalone.html for progressive image loading:

Lazy loading with SVG placeholder
<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 {image.width} {image.height}'%3E%3C/svg%3E"
     data-src="{image.src}"
     alt="{image.alt}"
     width="{image.width}"
     height="{image.height}"
     class="lazyload {image.htmlAttributes.class}"
     {f:if(condition: image.title, then: 'title="{image.title}"')}
     {f:if(condition: image.htmlAttributes.style, then: 'style="{image.htmlAttributes.style}"')}
     decoding="async" />
Copied!

Best practices 

  1. Only override what you need: Copy only templates requiring changes.
  2. Preserve accessibility: Always include alt attribute and maintain semantic HTML.
  3. Keep security intact: The DTO properties are pre-sanitized. Do not apply additional encoding that could double-escape content.
  4. Test all contexts: Verify overrides work with captions, links, and popups.
  5. Use native lazy loading: Prefer loading="lazy" over JavaScript solutions.
  6. CSS classes move to figure: When images have captions, CSS classes defined on the <img> element are applied to the <figure> wrapper instead. This ensures valid HTML5 semantics. If you need classes specifically on the <img> within a figure, create a custom WithCaption.html template override.
  7. Decoding attribute: The default templates include decoding="async" on all images to improve rendering performance by allowing the browser to decode images off the main thread. This is a modern best practice that does not affect visual output.
  8. Whitespace is stripped: The rendering service removes whitespace between HTML tags to prevent parseFunc_RTE from creating <p>&nbsp;</p> artifacts. Templates can use readable multi-line formatting; it will be normalized. However, deliberate spacing between inline elements will be removed.

Debugging templates 

Enable Fluid debugging to inspect available variables:

Debug all template variables
<f:debug>{_all}</f:debug>
Copied!

Or in TypoScript:

Enable debug mode via TypoScript
lib.parseFunc_RTE.settings.debug = 1
Copied!

Troubleshooting & Support 

Solutions to common issues, debugging techniques, and support resources.

Quick Fixes 

Most Common Issues 

1. Style Dropdown Disabled (v13.0.0+) 

// Ensure these dependencies are present:
static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];
}
Copied!

2. Images Not Appearing in Frontend 

  • Check TypoScript setup
  • Verify file permissions
  • Clear all caches

3. File Browser Not Opening 

  • Check backend user permissions
  • Verify TSConfig
  • Check file mount configuration

Debugging Techniques 

Enable Debug Mode 

$GLOBALS['TYPO3_CONF_VARS']['SYS']['devIPmask'] = '*';
$GLOBALS['TYPO3_CONF_VARS']['SYS']['displayErrors'] = 1;
Copied!

Browser Console 

  • Check for JavaScript errors
  • Monitor network requests
  • Inspect CKEditor plugin loading

TYPO3 Logs 

  • Check var/log/typo3_*.log
  • Review deprecation log
  • Monitor PHP error log

Database Queries 

  • Enable SQL debug mode
  • Check soft references
  • Verify file relations

Getting Help 

Self-Help Resources 

  1. Check this troubleshooting guide
  2. Review Integration & Configuration
  3. Consult Examples & Use Cases
  4. Search GitHub Issues

Community Support 

GitHub Discussions
github.com/netresearch/t3x-rte_ckeditor_image/discussions
TYPO3 Slack
#ext-rte_ckeditor_image channel
TYPO3 Forum
typo3.org/community/meet

Reporting Bugs 

Report bugs: github.com/netresearch/t3x-rte_ckeditor_image/issues

Include:

  • TYPO3 version
  • PHP version
  • Extension version
  • Steps to reproduce
  • Error messages
  • Browser console output

Troubleshooting Topics 

📦 Installation Issues 

Extension installation problems, dependency conflicts, cache issues, and permission problems

✏️ Editor Issues 

Image button problems, style dropdown issues, file browser problems, CKEditor errors, and known limitations

🖥️ Frontend Issues 

Image display problems, broken links, dimension issues, and rendering problems

⚡ Performance Issues 

Editor performance, frontend performance, image processing optimization, and database performance

🔍 Image Reference Validation 

Detect and fix stale image references, processed URLs, orphaned file UIDs, and broken src attributes

Installation Issues 

Solutions for problems encountered during extension installation, configuration, and setup.

Image Insert Button Not Appearing 

Cause 1: Site Set Dependency Not Added 

The extension requires a Site Set dependency in your site configuration. Without it, the RTE preset is not loaded and the button will not appear.

Solution: Add the dependency to your site configuration:

config/sites/<your-site>/config.yaml
base: 'https://example.com/'
rootPageId: 1
dependencies:
  - netresearch/rte-ckeditor-image
Copied!

Cause 2: Bootstrap Package or Theme Extension Overriding the Preset 

Extensions like bootstrap_package set their own RTE.default.preset via Site Sets. If their preset loads after ours, it overrides the rteWithImages preset.

Solution: List netresearch/rte-ckeditor-image after the theme package in your site dependencies:

config/sites/<your-site>/config.yaml
dependencies:
  - bootstrap-package/full
  - netresearch/rte-ckeditor-image  # Must come AFTER theme packages
Copied!

This ensures our preset loads last and overrides the theme's RTE preset.

Cause 3: Custom YAML Preset Missing insertimage 

If you use a custom RTE preset YAML file, the insertimage toolbar item must be listed explicitly.

Solution: Ensure your custom preset includes the plugin import and toolbar item:

EXT:my_sitepackage/Configuration/RTE/Custom.yaml
imports:
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Default.yaml" }
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    toolbar:
      items:
        - heading
        - '|'
        - bold
        - italic
        - '|'
        - insertimage  # Required for the image button
        - link
Copied!

Cause 4: Cache Not Cleared After Installation 

TYPO3 caches RTE configuration aggressively. After installing the extension or changing site configuration, caches must be flushed.

Solution:

vendor/bin/typo3 cache:flush
Copied!

Also clear your browser cache and do a hard reload (Ctrl+Shift+R).

Verification:

After applying the fix, check that the RTE preset is active:

  1. Go to Site Management > Page TSconfig
  2. Search for RTE.default.preset
  3. It should show rteWithImages

If it shows a different preset (e.g., bootstrap, default), the Site Set dependency is not loaded or is being overridden.


Extension Installation Problems 

Issue: Extension Not Working After TYPO3 13 Upgrade 

Symptoms:

  • Extension installed but not functional
  • Errors about missing classes

Solution: Ensure correct version compatibility:

{
  "require": {
    "typo3/cms-core": "^13.4",
    "netresearch/rte-ckeditor-image": "^13.0"
  }
}
Copied!
composer update
./vendor/bin/typo3 cache:flush
./vendor/bin/typo3 extension:setup
Copied!

Dependency Conflicts 

Issue: Style Drop-Down Dependency Error 

Symptoms:

  • Styles disabled when image selected
  • Style changes not applied to images

Cause: Missing GeneralHtmlSupport dependency (fixed in v13.0.0+)

Solution: Ensure you're using extension version 13.0.0 or higher:

composer require netresearch/rte-ckeditor-image:^13.0
Copied!

The plugin now requires:

static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];  // Both mandatory
}
Copied!

Issue: JavaScript Dependency Errors 

Symptoms:

  • Browser console shows "GeneralHtmlSupport is not defined"
  • Editor doesn't load properly

Cause: Extension version < 13.0.0

Solution: Update to latest version:

composer update netresearch/rte-ckeditor-image
Copied!

Permission Problems 

Issue: File Browser Empty or Not Loading 

Symptoms:

  • Modal opens but shows no files
  • File browser stuck loading

Causes:

  1. No file mount configured for backend user
  2. Missing file permissions
  3. Empty fileadmin directory

Solution:

# User TSConfig
options.defaultUploadFolder = 1:fileadmin/user_upload/
Copied!

Verify backend user has file mount in: BackendUser ManagementBackend UsersFile Mounts


Issue: Processed Images Directory Not Writable 

Symptoms:

  • Original large images displayed
  • No _processed_/ directory created
  • Slow page load due to large images

Solution: Check directory permissions:

# Ensure _processed_/ is writable
chmod 775 fileadmin/_processed_/

# Verify ownership
chown www-data:www-data fileadmin/_processed_/
Copied!

Static Template Configuration 

Issue: Static Template Not Included 

Symptoms:

  • Images visible in backend RTE
  • Images missing in frontend output

Solution:

  1. Include Static Template:

    • Go to TemplateInfo/Modify
    • Edit whole template record
    • Include CKEditor Image Support before Fluid Styled Content
  2. Verify TypoScript:
lib.parseFunc_RTE {
    tags.img = TEXT
    tags.img {
        current = 1
        preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
    }
}
Copied!

Issue: Click-to-Enlarge Not Working with sys_template Records (TYPO3 v13) 

New in version 13.0.0

TYPO3 v13 introduced site sets as a modern alternative to sys_template records. When sys_template records exist, site sets are bypassed, which affects extensions that rely on site set dependencies.

Symptoms:

  • Images display correctly in frontend
  • Click-to-enlarge functionality doesn't work
  • Data attributes still visible in HTML output (data-htmlarea-zoom, data-htmlarea-file-uid)
  • Image processing hooks not executed

Cause:

In TYPO3 v13, sys_template records prevent site sets from loading. Legacy installations like the Introduction Package use sys_template records instead of modern site sets. When a sys_template exists on a page, TYPO3 ignores site set dependencies, so the extension's TypoScript configuration is never loaded.

Detection:

Check if your site uses sys_template records:

SELECT uid, pid, title, include_static_file
FROM sys_template
WHERE deleted=0 AND hidden=0;
Copied!

If records exist and data-htmlarea-* attributes appear in frontend HTML, the extension's TypoScript is not being loaded.

Solution 1: Manual TypoScript Include (Quick Fix)

Add TypoScript directly to the sys_template record:

  1. Go to WEB > Template module
  2. Select page with sys_template record
  3. Click Edit the whole template record
  4. In Setup field, add:
# Include RTE CKEditor Image TypoScript
<INCLUDE_TYPOSCRIPT: source="FILE:EXT:rte_ckeditor_image/Configuration/TypoScript/ImageRendering/setup.typoscript">
Copied!
  1. Save template
  2. Clear all caches:
./vendor/bin/typo3 cache:flush
Copied!

Solution 2: Migrate to Site Sets (Recommended for TYPO3 v13)

Modern TYPO3 v13 approach:

  1. Remove sys_template records from pages (or set them to deleted/hidden)
  2. Enable site set dependencies in config/sites/<site>/config.yaml:
base: 'https://example.com/'
rootPageId: 1
dependencies:
  - typo3/fluid-styled-content
  - netresearch/rte-ckeditor-image
Copied!
  1. Clear caches:
./vendor/bin/typo3 cache:flush
Copied!
  1. Verify in frontend - data attributes should be removed and click-to-enlarge should work

Why This Happens:

  • TypoScript must be manually included via static template or import
  • sys_template records control TypoScript for their page tree
  • Bootstrap Package in sys_template may clear lib.parseFunc_RTE hooks
  • Load order matters - include the extension's TypoScript after theme packages but before Fluid Styled Content

Verification:

After applying fix, check frontend HTML:

<!-- Before (BROKEN): -->
<img src="..." data-htmlarea-zoom="true" data-htmlarea-file-uid="2" />

<!-- After (WORKING): -->
<a href="/index.php?eID=tx_cms_showpic&file=2&...">
    <img src="..." />
</a>
Copied!

Issue: Insert Image Button Missing with Bootstrap Package or Other Site Sets 

New in version 13.1.5

Site Set dependency ordering ensures proper override behavior.

Symptoms:

  • Extension is installed and active
  • "Insert image" button missing from RTE toolbar
  • Page TSConfig shows RTE.default.preset = bootstrap (or another third-party preset)

Cause:

Third-party extensions like bootstrap_package use Site Sets to configure the RTE. In TYPO3 v13, Site Sets have higher priority than extension page.tsconfig files.

The loading order is:

  1. Extension Configuration/page.tsconfig (our RTE.default.preset = rteWithImages)
  2. Site Set configurations (bootstrap's RTE.default.preset = bootstrap overrides ours)

When your site uses a Site Set dependency like bootstrap-package/full, it loads after our extension's page.tsconfig and overrides our RTE preset.

Detection:

Check the active RTE preset in Page TSConfig module:

  1. Go to Site ManagementPage TSconfig
  2. Search for RTE.default.preset
  3. If it shows bootstrap or another preset (not rteWithImages), you have this issue

Or check your site configuration:

config/sites/<site>/config.yaml
dependencies:
  - bootstrap-package/full  # This overrides our RTE preset!
Copied!

Solution: Add Site Set Dependency

Add netresearch/rte-ckeditor-image to your site dependencies after the overriding package so our preset loads last:

config/sites/<site>/config.yaml
base: 'https://example.com/'
rootPageId: 1
dependencies:
  - bootstrap-package/full
  - netresearch/rte-ckeditor-image  # Must come AFTER bootstrap-package
Copied!

Clear caches after updating:

./vendor/bin/typo3 cache:flush
Copied!

Why This Works:

Our Site Set (netresearch/rte-ckeditor-image) declares optionalDependencies on bootstrap-package, so when both are listed, ours loads after bootstrap and overrides their RTE preset with rteWithImages.

EXT:rte_ckeditor_image/Configuration/Sets/RteCKEditorImage/config.yaml
name: netresearch/rte-ckeditor-image
label: 'CKEditor Image Support'
optionalDependencies:
  - bootstrap-package/content-elements
  - bootstrap-package/full
Copied!

Affected Packages:

This issue affects any extension that sets RTE.default.preset via Site Sets:

  • bootstrap_package (sets RTE.default.preset = bootstrap)
  • Other theme packages with custom RTE configurations
  • Any extension using Site Sets for RTE configuration

Verification:

After adding the dependency:

  1. Clear all caches
  2. Go to any content element with an RTE field
  3. Verify the Insert image button appears in the toolbar
  4. Check Page TSConfig shows RTE.default.preset = rteWithImages

Image Processing Configuration 

Issue: ImageMagick/GraphicsMagick Not Configured 

Symptoms:

  • Original large images displayed instead of processed versions
  • Image processing test fails

Solution: Verify image processing configuration:

// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX'] = [
    'processor' => 'ImageMagick',  // or 'GraphicsMagick'
    'processor_path' => '/usr/bin/',
    'processor_enabled' => true,
];
Copied!

Test Image Processing:

./vendor/bin/typo3 backend:test:imageprocessing
Copied!

Debugging Installation 

Check Extension Installation 

# Verify extension is installed
composer show netresearch/rte-ckeditor-image

# Check TYPO3 extension list
./vendor/bin/typo3 extension:list
Copied!

Verify Configuration Loading 

# Page TSConfig - Enable RTE debugging
RTE.default.showButtons = *
RTE.default.hideButtons =
Copied!

Check Browser Console 

  1. Open browser DevTools (F12)
  2. Go to Console tab
  3. Look for errors related to:

    • Plugin loading
    • Configuration issues
    • Missing dependencies

Monitor Network Requests 

  1. Open browser DevTools
  2. Go to Network tab
  3. Check for failed requests to:

    • /rte/wizard/selectimage
    • Backend image info API

Database Issues 

Issue: Large Database Size 

Symptoms:

  • Database growing rapidly
  • sys_refindex table very large

Cause: Excessive soft reference entries

Solution: Rebuild reference index:

./vendor/bin/typo3 referenceindex:update
Copied!

Check References:

-- Find images in RTE content
SELECT uid, bodytext
FROM tt_content
WHERE bodytext LIKE '%data-htmlarea-file-uid%';
Copied!

Getting Help 

If issues persist after troubleshooting:

  1. Check GitHub Issues: https://github.com/netresearch/t3x-rte_ckeditor_image/issues
  2. Review Changelog: Look for breaking changes in CHANGELOG.md
  3. TYPO3 Slack: Join #typo3-cms
  4. Stack Overflow: Tag questions with typo3 and ckeditor

Editor Issues 

Solutions for problems encountered in the TYPO3 backend editor and CKEditor functionality.

Image Button Problems 

Issue: Image Button Not Visible in Toolbar 

Symptoms:

  • Insert image button missing from CKEditor toolbar
  • RTE loads but no image functionality

Causes:

  1. Plugin not properly imported in RTE configuration
  2. removePlugins includes image plugin
  3. Toolbar configuration missing insertimage item

Solution:

# Configuration/RTE/Default.yaml
imports:
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    removePlugins: null  # Critical: Don't remove image plugin
    toolbar:
      items:
        - insertimage  # Add to toolbar
Copied!

Style Dropdown Problems 

Issue: Style Drop-Down Not Working with Images 

Symptoms:

  • Styles disabled when image selected
  • Style changes not applied to images

Cause: Missing GeneralHtmlSupport dependency (fixed in v13.0.0+)

Solution: Ensure you're using extension version 13.0.0 or higher:

composer require netresearch/rte-ckeditor-image:^13.0
Copied!

The plugin now requires:

static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];  // Both mandatory
}
Copied!

Issue: Custom Image Styles Lost After Upgrade 

Symptoms:

  • Custom styles no longer available
  • Style drop-down empty

Cause: RTE configuration changed

Solution: Re-apply custom styles in RTE configuration:

editor:
  config:
    style:
      definitions:
        - name: 'Your Custom Style'
          element: 'img'
          classes: ['your-class']
Copied!

File Browser Issues 

Issue: File Browser Empty or Not Loading 

Symptoms:

  • Modal opens but shows no files
  • File browser stuck loading

Causes:

  1. No file mount configured for backend user
  2. Missing file permissions
  3. Empty fileadmin directory

Solution:

# User TSConfig
options.defaultUploadFolder = 1:fileadmin/user_upload/
Copied!

Verify backend user has file mount in: BackendUser ManagementBackend UsersFile Mounts


Issue: "File Not Found" After Selection 

Symptoms:

  • Image selected but error occurs
  • Empty image inserted

Causes:

  1. File reference invalid
  2. Storage not accessible
  3. File deleted from filesystem

Solution:

  1. Verify file exists in fileadmin/
  2. Check file permissions (readable by web server)
  3. Clear file abstraction layer cache:
./vendor/bin/typo3 cache:flush --group=system
Copied!

Image Dimension Problems 

Issue: Magic Image Maximum Dimensions Not Working 

Symptoms:

  • Images not respecting configured maxWidth/maxHeight
  • Large images not being resized

Cause: TSConfig settings in custom template extension not loaded (TYPO3 bug #87068)

Solution: Add settings to root page config instead:

# In root page TSConfig (not template extension)
RTE.default.buttons.image.options.magic {
    maxWidth = 1920
    maxHeight = 9999
}
Copied!

JavaScript/CKEditor Errors 

Issue: JavaScript Console Errors 

Symptoms:

  • Browser console shows errors
  • Editor doesn't load properly

Common Errors 

1. "GeneralHtmlSupport is not defined" 

Cause: Extension version < 13.0.0

Solution: Update to latest version:

composer update netresearch/rte-ckeditor-image
Copied!
2. "Cannot read property 'typo3image' of undefined" 

Cause: Plugin configuration not loaded

Solution: Verify Configuration/RTE/Plugin.yaml imported:

imports:
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }
Copied!

Issue: Double-Click on Image Does Nothing 

Symptoms:

  • Double-clicking image doesn't open dialog
  • Edit functionality not working

Causes:

  1. DoubleClickObserver not registered
  2. JavaScript error blocking execution
  3. Image not recognized as typo3image

Solution:

  1. Check browser console for JavaScript errors
  2. Verify image has data-htmlarea-file-uid attribute
  3. Clear browser cache and reload
  4. Check CKEditor version compatibility (requires CKEditor 5)

Editor Loading Problems 

Issue: Editor Not Initializing 

Symptoms:

  • CKEditor doesn't load
  • Textarea remains plain text field

Causes:

  1. JavaScript errors preventing initialization
  2. RTE configuration not loaded
  3. Browser cache issues

Solution:

  1. Check browser console for errors
  2. Verify RTE preset is enabled:
# Configuration/RTE/Default.yaml
imports:
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Default.yaml" }
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }
Copied!
  1. Clear browser and TYPO3 caches:
./vendor/bin/typo3 cache:flush
Copied!
  1. Force reload in browser (Ctrl+Shift+R)

Plugin Configuration Issues 

Issue: Plugin Not Recognized 

Symptoms:

  • Image plugin functionality missing
  • Console error about undefined plugin

Cause: Plugin configuration not properly loaded

Solution: Ensure proper import order:

# Configuration/RTE/Default.yaml
imports:
  # Base CKEditor configuration first
  - { resource: "EXT:rte_ckeditor/Configuration/RTE/Default.yaml" }
  # Then image plugin
  - { resource: "EXT:rte_ckeditor_image/Configuration/RTE/Plugin.yaml" }

editor:
  config:
    # Ensure plugin is not removed
    removePlugins: null
    toolbar:
      items:
        - insertimage
Copied!

Debugging Editor Issues 

Enable RTE Debugging 

# Page TSConfig
RTE.default.showButtons = *
RTE.default.hideButtons =
Copied!

Check Loaded Configuration 

Browser console:

// Check if plugin loaded
console.log(CKEDITOR.instances);

// Inspect editor config
const editor = Object.values(CKEDITOR.instances)[0];
console.log(editor.config);
Copied!

Monitor Network Requests 

  1. Open browser DevTools
  2. Go to Network tab
  3. Trigger image selection
  4. Check for failed requests to:

    • /rte/wizard/selectimage
    • Backend image info API

Inspect DOM Elements 

Check if images have required attributes:

// In browser console
document.querySelectorAll('img[data-htmlarea-file-uid]');
Copied!

Resolved Issues 

UTF-8 characters in figcaptions 

Changed in version 13.6.0

Symptoms:

  • German umlauts (ä, ö, ü, ß) corrupted in figcaptions on the frontend
  • Accented characters (é, ç, ñ) replaced with garbled text
  • Only figcaptions affected, image alt/title attributes rendered correctly

Cause: PHP's DOMDocument::loadHTML() defaults to ISO-8859-1 encoding, which corrupts multi-byte UTF-8 characters during HTML parsing.

Status: Fixed. All DOMDocument::loadHTML() calls now prefix the HTML with <?xml encoding="UTF-8"> before parsing. No action required.

Inline linked images with unresolved t3:// URLs 

Changed in version 13.6.0

Symptoms:

  • Inline linked images show t3://page?uid=123 as literal text in the frontend href instead of the resolved URL
  • Links work in the backend but not on the frontend

Cause: The externalBlocks.a configuration was dead code — the <a> tag was never listed in the externalBlocks comma list, so the per-tag configuration was silently ignored by TYPO3's parseFunc.

Status: Fixed. Link resolution now uses tags.a with a dedicated renderInlineLink() method in the ImageRenderingAdapter. No action required.


Known Limitations 

Figcaption line breaks 

Symptoms:

  • Pressing Shift+Enter in figcaption does not insert a line break
  • Adding <br> in source mode is removed after saving
  • Caption text always appears on a single line

Cause: CKEditor 5's figcaption content model only supports inline text — it does not allow <br> tags or block-level elements. The editor strips unsupported elements during content serialization. This is a CKEditor 5 limitation, not a bug in this extension.

Workaround: Captions wrap naturally based on the figure container width. Since version 13.6.0, the <figure> element is automatically constrained to the image width via max-width, so captions wrap within the image boundary without additional CSS. See Image styles for details.


Getting Help 

If issues persist after troubleshooting:

  1. Check GitHub Issues: https://github.com/netresearch/t3x-rte_ckeditor_image/issues
  2. Review Changelog: Look for breaking changes in CHANGELOG.md
  3. TYPO3 Slack: Join #typo3-cms
  4. Stack Overflow: Tag questions with typo3 and ckeditor

Frontend Issues 

Solutions for problems with image display and rendering in the frontend.

Image Display Problems 

Issue: Images Not Appearing in Frontend 

Symptoms:

  • Images visible in backend RTE
  • Images missing in frontend output

Causes:

  1. TypoScript not included (most common)
  2. Cached content
  3. Custom TypoScript overriding extension configuration

Solution:

  1. Include TypoScript (required):

    The extension requires manual TypoScript inclusion for frontend rendering:

    Option A: Static Template

    1. Go to WEB > Template module
    2. Select your root page, edit the template
    3. In Includes tab, add: CKEditor Image Support

    Option B: Direct Import

    @import 'EXT:rte_ckeditor_image/Configuration/TypoScript/ImageRendering/setup.typoscript'
    Copied!
  2. Clear Caches:

    ./vendor/bin/typo3 cache:flush
    Copied!
  3. Check for TypoScript conflicts:

    If you have custom lib.parseFunc_RTE configuration, ensure it doesn't override the image rendering hooks.


Issue: Processed Images Not Generated 

Symptoms:

  • Original large images displayed
  • No _processed_/ directory created
  • Slow page load due to large images

Causes:

  1. Image processing disabled
  2. ImageMagick/GraphicsMagick not configured
  3. File permissions issue

Solution:

  1. Verify Image Processing Configuration:
// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX'] = [
    'processor' => 'ImageMagick',  // or 'GraphicsMagick'
    'processor_path' => '/usr/bin/',
    'processor_enabled' => true,
];
Copied!
  1. Test Image Processing:
# TYPO3 CLI
./vendor/bin/typo3 backend:test:imageprocessing
Copied!
  1. Check Directory Permissions:
# Ensure _processed_/ is writable
chmod 775 fileadmin/_processed_/
Copied!
  1. Check Processed Files:
ls -la fileadmin/_processed_/
Copied!

Dimension Problems 

Issue: Images Display at Wrong Size 

Symptoms:

  • Images too large or too small
  • Dimensions not respected
  • Responsive behavior broken

Causes:

  1. CSS conflicts
  2. Missing width/height attributes
  3. Responsive image configuration issues

Solution:

  1. Check Generated HTML:
<!-- Should include width and height -->
<img src="..." width="800" height="600" />
Copied!
  1. Verify CSS:
/* Ensure images are responsive */
.rte img {
    max-width: 100%;
    height: auto;
}
Copied!
  1. Check TypoScript Configuration:
lib.parseFunc_RTE.tags.img {
    current = 1
    preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
}
Copied!

Issue: Image Dimensions Not Preserved 

Symptoms:

  • Aspect ratio distorted
  • Images stretched or squashed

Cause: Missing or incorrect dimension attributes

Solution:

Ensure both width and height are rendered:

lib.parseFunc_RTE.tags.img {
    current = 1
    preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
    stdWrap {
        wrap = <div class="rte-image">|</div>
    }
}
Copied!

Style and Class Problems 

Issue: CSS Classes Not Applied 

Symptoms:

  • Custom classes missing from output
  • Styles not visible in frontend
  • Classes work in backend but not frontend

Cause: HTMLparser configuration stripping classes

Solution:

lib.parseFunc_RTE.nonTypoTagStdWrap.HTMLparser {
    keepNonMatchedTags = 1
    tags.img {
        allowedAttribs = class,src,alt,title,width,height
        fixAttrib.class.list = your-allowed-classes
    }
}
Copied!

Issue: Data Attributes Visible in Frontend 

Symptoms:

  • data-htmlarea-file-uid visible in HTML
  • Internal attributes exposed

Cause: HTMLparser configuration missing

Solution:

lib.parseFunc_RTE.nonTypoTagStdWrap.HTMLparser.tags.img.fixAttrib {
    data-htmlarea-file-uid.unset = 1
    data-htmlarea-file-table.unset = 1
    # Keep zoom attributes for popup/lightbox rendering
    # data-htmlarea-zoom.unset = 1
    data-title-override.unset = 1
    data-alt-override.unset = 1
}
Copied!

Responsive Image Issues 

Issue: Images Not Responsive 

Symptoms:

  • Images overflow container on mobile
  • Fixed width prevents scaling
  • No srcset generated

Cause: Missing responsive configuration

Solution:

/* Basic responsive images */
.rte img {
    max-width: 100%;
    height: auto;
    display: block;
}
Copied!

For advanced responsive images with srcset, configure image processing:

lib.parseFunc_RTE.tags.img {
    current = 1
    preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
}
Copied!

Caching Issues 

Issue: Old Images Still Displayed 

Symptoms:

  • Updated images not showing
  • Old version cached
  • Changes visible in backend but not frontend

Solution:

  1. Clear TYPO3 Caches:
./vendor/bin/typo3 cache:flush
Copied!
  1. Clear Browser Cache:

    • Hard reload: Ctrl+Shift+R (Windows/Linux)
    • Hard reload: Cmd+Shift+R (Mac)
  2. Clear Processed Images:
# Remove all processed images
rm -rf fileadmin/_processed_/*
Copied!
  1. Verify Cache Configuration:
config {
    sendCacheHeaders = 1
    cache_period = 86400
}
Copied!

Debugging Frontend Issues 

Check Generated HTML 

View page source and inspect image markup:

<!-- Expected output -->
<img src="fileadmin/_processed_/.../image.jpg"
     alt="Description"
     width="800"
     height="600"
     class="your-class" />
Copied!

Verify TypoScript Processing 

# Enable TypoScript debugging
config {
    debug = 1
    admPanel = 1
}
Copied!

Check Browser Network Tab 

  1. Open DevTools (F12)
  2. Go to Network tab
  3. Filter by images
  4. Check for:

    • 404 errors
    • Slow loading
    • Wrong paths

Inspect CSS 

/* Check for conflicts */
.rte img {
    /* Ensure no display:none or visibility:hidden */
}
Copied!

Monitor Console Errors 

Look for JavaScript errors that might affect image rendering:

// Common issues
- Failed to load resource
- CORS errors
- JavaScript blocking rendering
Copied!

Getting Help 

If issues persist after troubleshooting:

  1. Check GitHub Issues: https://github.com/netresearch/t3x-rte_ckeditor_image/issues
  2. Review Changelog: Look for breaking changes in CHANGELOG.md
  3. TYPO3 Slack: Join #typo3-cms
  4. Stack Overflow: Tag questions with typo3 and ckeditor

Performance Issues 

Solutions for performance problems, slow loading, and optimization strategies.

Editor Performance 

Issue: Slow Editor Loading 

Symptoms:

  • CKEditor takes long time to initialize
  • Image browser slow to open

Causes:

  1. Large number of files in file browser
  2. Unoptimized image processing settings
  3. Network latency
  4. Browser resource constraints

Solutions:

  1. Optimize Image Processing:
// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality'] = 85;
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_effects'] = false;  // If not needed
Copied!
  1. Reduce Maximum Dimensions:
RTE.default.buttons.image.options.magic {
    maxWidth = 1200  # Instead of 1920
    maxHeight = 800  # Instead of 9999
}
Copied!
  1. Limit File Browser Results:

Configure file mounts to show only relevant directories:

# User TSConfig
options.folderTree.uploadFieldsInLinkBrowser = 0
options.pageTree.showPageIdWithTitle = 0
Copied!

Issue: Image Selection Dialog Slow 

Symptoms:

  • Modal takes long time to open
  • Thumbnails load slowly
  • Browser becomes unresponsive

Causes:

  1. Too many files in directory
  2. Large unprocessed images
  3. Missing thumbnail cache

Solutions:

  1. Organize Files into Subdirectories:

    • Group images by category
    • Use year/month folder structure
    • Keep directories under 100 files
  2. Pre-generate Thumbnails:
# Generate missing thumbnails
./vendor/bin/typo3 cleanup:missingfiles
./vendor/bin/typo3 cleanup:previewlinks
Copied!
  1. Optimize File Storage:
# Limit file selection to specific folders
options.defaultUploadFolder = 1:fileadmin/user_upload/
Copied!

Frontend Performance 

Issue: Slow Page Load Due to Images 

Symptoms:

  • Pages load slowly
  • Large images not processed
  • High bandwidth usage

Causes:

  1. Original large images served instead of processed versions
  2. No image compression
  3. Missing lazy loading
  4. Too many images on page

Solutions:

  1. Enable Image Processing:
// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX'] = [
    'processor_enabled' => true,
    'processor' => 'ImageMagick',
    'jpg_quality' => 85,
];
Copied!
  1. Configure Reasonable Maximum Dimensions:
RTE.default.buttons.image.options.magic {
    maxWidth = 1920
    maxHeight = 1080
}
Copied!
  1. Implement Lazy Loading:
lib.parseFunc_RTE.tags.img {
    current = 1
    preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
    stdWrap.replacement {
        10 {
            search = <img
            replace = <img loading="lazy"
        }
    }
}
Copied!
  1. Enable Browser Caching:
# .htaccess
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg "access plus 1 year"
    ExpiresByType image/png "access plus 1 year"
    ExpiresByType image/webp "access plus 1 year"
</IfModule>
Copied!

Issue: Excessive Bandwidth Usage 

Symptoms:

  • High server bandwidth consumption
  • Slow site on mobile devices
  • Large page sizes

Solutions:

  1. Use WebP Format:
// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowUpscaling'] = false;
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_stripColorProfileByDefault'] = true;
Copied!
  1. Implement Progressive JPEG:
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_interlace'] = true;
Copied!
  1. Compress Images:
$GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality'] = 80;
Copied!

Image Processing Performance 

Issue: Image Processing Timeouts 

Symptoms:

  • 500 errors when uploading images
  • Processing hangs
  • Timeouts in backend

Causes:

  1. Insufficient PHP memory
  2. Low execution time limits
  3. Slow image processor
  4. Very large source images

Solutions:

  1. Increase PHP Limits:
// php.ini or LocalConfiguration.php
memory_limit = 512M
max_execution_time = 300
upload_max_filesize = 20M
post_max_size = 20M
Copied!
  1. Optimize Image Processor:
// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_path_lzw'] = '';
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_colorspace'] = 'RGB';
Copied!
  1. Limit Upload Size:
# Page TSConfig
RTE.default.buttons.image.options.magic {
    maxFileSize = 5000  # 5MB in KB
}
Copied!

Issue: Processed Images Directory Growing 

Symptoms:

  • Large _processed_/ directory
  • Disk space issues
  • Duplicate processed images

Causes:

  1. Old processed images not cleaned up
  2. Multiple versions of same image
  3. Unused processed files accumulating

Solutions:

  1. Clean Old Processed Files:
# Remove processed files older than 30 days
find fileadmin/_processed_ -type f -mtime +30 -delete
Copied!
  1. Implement Automated Cleanup:
// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_allowTemporaryMasksAsPng'] = false;
Copied!
  1. Schedule Cleanup Task:

Create a scheduler task to regularly clean old processed images:

# Cron job example (runs weekly)
0 2 * * 0 find /path/to/fileadmin/_processed_ -type f -mtime +30 -delete
Copied!

Database Performance 

Issue: Large Database Size 

Symptoms:

  • Database growing rapidly
  • sys_refindex table very large
  • Slow queries

Causes:

  1. Excessive soft reference entries
  2. Orphaned records
  3. Unoptimized indexes

Solutions:

  1. Rebuild Reference Index:
./vendor/bin/typo3 referenceindex:update
Copied!
  1. Clean Up Orphaned Records:
-- Find images in deleted content
SELECT uid, bodytext
FROM tt_content
WHERE deleted = 1 AND bodytext LIKE '%data-htmlarea-file-uid%';
Copied!
  1. Optimize Tables:
OPTIMIZE TABLE sys_refindex;
OPTIMIZE TABLE tt_content;
Copied!

Issue: Slow Reference Index Updates 

Symptoms:

  • Reference index update takes long time
  • High CPU usage during updates

Solutions:

  1. Batch Update References:
# Update in smaller batches
./vendor/bin/typo3 referenceindex:update --check
Copied!
  1. Schedule Off-Peak Updates:

Run reference index updates during low-traffic periods:

# Cron job (runs at 3 AM)
0 3 * * * /path/to/vendor/bin/typo3 referenceindex:update
Copied!

Cache Performance 

Issue: Excessive Cache Invalidation 

Symptoms:

  • Caches cleared too frequently
  • Slow page regeneration
  • High server load

Causes:

  1. Aggressive cache clearing
  2. Content changes trigger full cache clear
  3. Misconfigured cache tags

Solutions:

  1. Optimize Cache Configuration:
// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['pages'] = [
    'backend' => \TYPO3\CMS\Core\Cache\Backend\RedisBackend::class,
    'options' => [
        'hostname' => 'localhost',
        'database' => 0,
        'port' => 6379,
    ],
];
Copied!
  1. Use Cache Tags Effectively:
config {
    cache_period = 86400
    sendCacheHeaders = 1
}
Copied!
  1. Implement Selective Cache Clearing:
# Clear only specific cache groups
./vendor/bin/typo3 cache:flush --group=pages
Copied!

Network Performance 

Issue: Slow Image Loading from CDN 

Symptoms:

  • Images load slowly despite CDN
  • Inconsistent loading times
  • Failed CDN requests

Solutions:

  1. Configure Proper CDN:
config {
    absRefPrefix = /
    baseURL = https://cdn.yourdomain.com/
}
Copied!
  1. Use HTTP/2:

Ensure your server supports HTTP/2 for multiplexing

  1. Implement Preconnect:
<link rel="preconnect" href="https://cdn.yourdomain.com">
<link rel="dns-prefetch" href="https://cdn.yourdomain.com">
Copied!

Monitoring and Profiling 

Performance Monitoring 

  1. Enable TYPO3 Admin Panel:
config.admPanel = 1
Copied!
  1. Monitor Image Processing:
# Check processing time
./vendor/bin/typo3 backend:test:imageprocessing
Copied!
  1. Track Page Load Times:
Use browser DevTools Network tab to monitor:
  • Image load times
  • TTFB (Time to First Byte)
  • Total page load time

Database Query Profiling 

-- Find slow queries
SHOW PROCESSLIST;

-- Analyze reference index queries
EXPLAIN SELECT * FROM sys_refindex WHERE tablename='tt_content';
Copied!

Image Processing Profiling 

# Time image processing
time ./vendor/bin/typo3 backend:test:imageprocessing

# Monitor processed files
watch -n 5 "ls -lh fileadmin/_processed_/ | wc -l"
Copied!

Optimization Best Practices 

Image Optimization Checklist 

Configure reasonable maximum dimensions:

RTE.default.buttons.image.options.magic {
    maxWidth = 1920
    maxHeight = 1080
}
Copied!

Enable image processing:

$GLOBALS['TYPO3_CONF_VARS']['GFX']['processor_enabled'] = true;
Copied!

Set appropriate JPEG quality:

$GLOBALS['TYPO3_CONF_VARS']['GFX']['jpg_quality'] = 85;
Copied!

Implement lazy loading:

<img loading="lazy" src="..." />
Copied!

Use browser caching:

ExpiresActive On
ExpiresByType image/jpeg "access plus 1 year"
Copied!

Regular cleanup of processed files:

find fileadmin/_processed_ -type f -mtime +30 -delete
Copied!

Monitor disk space and database size

Optimize images before upload (recommend to editors)

Server Optimization Checklist 

Adequate PHP memory: 512M minimum

Fast image processor: ImageMagick or GraphicsMagick

Redis/Memcached for caching

HTTP/2 support enabled

CDN for static assets

Regular database optimization

Scheduled cleanup tasks


Getting Help 

If issues persist after troubleshooting:

  1. Check GitHub Issues: https://github.com/netresearch/t3x-rte_ckeditor_image/issues
  2. Review Changelog: Look for performance improvements in CHANGELOG.md
  3. TYPO3 Slack: Join #typo3-cms
  4. Stack Overflow: Tag questions with typo3 and performance

Image Reference Validation 

The extension ships a validator that detects and fixes stale or broken image references in RTE content fields. It is available both as a CLI command and as an Upgrade Wizard in the TYPO3 Install Tool.

Overview 

Over time, image references stored inside RTE bodytext fields can become stale. Common causes include TYPO3 major upgrades, bulk file operations in the Filelist module, and manual database edits. The validator scans every <img> tag that carries a data-htmlarea-file-uid attribute, resolves the corresponding FAL file, and compares the src attribute against the file's current public URL.

Six categories of issues are detected:

Type Description Auto-fixable
processed_image_src src points to a _processed_/ URL. Processed files are regenerated on demand and their paths change between TYPO3 versions, so storing them as src will break after an upgrade. Yes
src_mismatch src does not match the FAL file's current public URL. This happens when a file is moved or renamed in the Filelist module while existing RTE content still references the old path. Yes
broken_src src is empty or missing, but a valid data-htmlarea-file-uid is present. The correct URL can be resolved from FAL. Yes
orphaned_file_uid data-htmlarea-file-uid references a FAL file that no longer exists in sys_file. The stale data-htmlarea-file-uid attribute is removed, but no src correction is possible because the file is gone. Yes (attribute removed)
missing_file_uid The <img> tag has no data-htmlarea-file-uid attribute at all. Without a file UID there is no way to determine which FAL file the image should reference, so this issue requires manual intervention. No
nested_link_wrapper The <img> tag is wrapped in two or more nested <a> tags (e.g., <a><a><img></a></a>). This typically occurs after upgrading from older extension versions where the tags.a and externalBlocks.a handlers both wrapped the same image. The inner duplicate <a> wrapper is removed, preserving the outer link and its attributes. Yes

For fixable issues the validator replaces the src attribute with the file's current getPublicUrl() value. The orphaned_file_uid type is treated as fixable in the scan (it is counted and reported), but no src update is applied because the underlying file no longer exists.

Prerequisites 

The validator relies on the TYPO3 reference index (sys_refindex) to discover which RTE fields contain image references. On a fresh installation or after large imports, the reference index may be empty or out of date. Always update it before running the validator:

bin/typo3 referenceindex:update
Copied!

If the validator reports "Scanned records: 0" despite images existing in RTE content, this is almost certainly the cause.

CLI Command 

The rte_ckeditor_image:validate command scans RTE content fields and reports (or fixes) broken image references.

Dry-run (default) 

Run the command without any flags to perform a read-only scan:

bin/typo3 rte_ckeditor_image:validate
Copied!

The output shows a summary of scanned records and images, followed by a table listing every issue found, including the current src, the expected src, and whether the issue is auto-fixable.

CLI command output showing RTE image reference validation results

Example output of bin/typo3 rte_ckeditor_image:validate in dry-run mode.

Apply fixes 

Add the --fix flag to write corrected src attributes back to the database:

bin/typo3 rte_ckeditor_image:validate --fix
Copied!

Skipped origins 

New in version 13.9.0

Not every <img src> value can be repaired by the validator. By default the command classifies each src and skips four out-of-scope categories:

Category Example Why skipped
external https://cdn.example.com/foo.jpg Off-site URL — not part of this TYPO3's FAL.
data data:image/png;base64,... Inline-encoded image, no FAL reference exists.
legacy typo3conf/ext/some_ext/Resources/... Pre-FAL extension path, file lives outside FAL.
securedl /securedl/sk=.../foo.jpg URL signed by EXT:naw_securedl / similar — the on-disk path differs from the signed URL.

Skips are surfaced in the CLI summary as a per-origin breakdown so they stay visible (added to the existing "Scanned records / Scanned images / Issues found" definition list):

Skipped (out of scope)
16 total (12 external, 3 data, 1 legacy)
Copied!

Use --include to opt one or more categories back in if your environment needs them validated:

# Re-include external URLs (the validator will then flag them as
# mismatches against FAL) — useful when migrating off a CDN.
bin/typo3 rte_ckeditor_image:validate --include=external

# Re-include multiple categories at once
bin/typo3 rte_ckeditor_image:validate --include=external,legacy

# Disable all skipping (validate every <img src> regardless of origin)
bin/typo3 rte_ckeditor_image:validate --include=all
Copied!

Limit to a specific table 

Use the --table (short: -t) option to restrict the scan to a single table:

bin/typo3 rte_ckeditor_image:validate --table=tt_content
Copied!

This is useful on large installations where you want to process one table at a time or only care about a particular table.

Combining options 

Options can be combined freely:

# Fix issues in tt_content only
bin/typo3 rte_ckeditor_image:validate --fix --table=tt_content
Copied!

Exit codes 

Code Meaning
0 No issues found, or all fixable issues were repaired successfully.
1 Issues were found (dry-run mode), or no fixable issues exist while unfixable issues remain.

Upgrade Wizard 

The same validation logic is exposed as a TYPO3 Upgrade Wizard named Validate RTE image references.

To run it:

  1. Open Admin Tools > Upgrade > Upgrade Wizard.
  2. Locate Validate RTE image references in the list of available wizards.
  3. Click Execute.

The wizard scans all RTE fields, and if fixable issues are found it automatically applies corrections. It implements RepeatableInterface, so it can be executed multiple times safely.

TYPO3 Upgrade Wizard showing the Validate RTE Image References wizard

The Upgrade Wizard panel with detected issues and the Execute button.

Page Module Preview Warning 

New in version 13.5.0

In addition to the CLI command and upgrade wizard, the extension now detects broken image references directly in the TYPO3 page module preview. When a content element contains images with validation issues, a yellow warning callout is shown above the content preview:

┌─────────────────────────────────────────────┐
│ ⚠ Image reference issues detected           │
│ 2 orphaned file reference(s),               │
│ 1 outdated src path(s).                     │
│ Run the upgrade wizard                      │
│ rteImageReferenceValidation or CLI command   │
│ bin/typo3 rte_ckeditor_image:validate --fix  │
│ to repair.                                   │
└─────────────────────────────────────────────┘
Copied!

This warning appears automatically for all CTypes that use the RteImagePreviewRenderer (see RtePreviewRendererRegistrar). The detection happens during page module rendering and requires no additional configuration.

The same five issue types detected by the CLI command are shown in the warning: orphaned_file_uid, src_mismatch, processed_image_src, missing_file_uid, and broken_src.

When to Use 

Run the validator in the following situations:

After a TYPO3 major upgrade 

Especially when upgrading from TYPO3 v10, v11, or v12 to v13+. Older versions of TYPO3 and of this extension sometimes stored _processed_/ URLs in bodytext instead of the original file path. These processed paths break after an upgrade because processed files are regenerated with different names.

After bulk file operations 

When files are moved or renamed in the Filelist module, the extension updates references in RTE content automatically (via the UpdateImageReferences listener). However, if files were moved by other means (direct filesystem operations, TYPO3 CLI, third-party tools), references may become stale.

As a periodic maintenance check 

Run the dry-run scan periodically to detect drift before it causes broken images in the frontend. The scan is read-only and safe to run at any time.


Architecture & Design 

System architecture, component design, and technical implementation details for the RTE CKEditor Image extension.

Overview 

This section explains the architectural decisions, design patterns, and component interactions in the RTE CKEditor Image extension.

Architecture Topics 

🏛️ System Architecture 

Three-layer architecture, core components, technology stack, and security/performance considerations

🎯 Design Patterns 

Key design patterns, integration points, data flow, and extension points for developers

📋 Architecture Decision Records 

Key architectural decisions, their context, rationale, and consequences for the extension design and implementation

System Architecture 

System architecture overview for the RTE CKEditor Image extension, covering the three-layer architecture, core components, and technology stack.

Overview 

This document explains the architectural structure and core components of the RTE CKEditor Image extension. For design patterns and integration details, see Design Patterns & Integration.

Three-Layer Architecture 

  1. CKEditor Plugin Layer (JavaScript)

    • Custom typo3image plugin
    • Model element definition
    • UI components and commands
    • Upcast/downcast conversions
  2. TYPO3 Backend Layer (PHP)

    • Controllers for image selection and rendering
    • Database hooks for content processing
    • FAL integration
    • Event listeners
  3. Frontend Rendering Layer (PHP/HTML)

    • TypoScript configuration
    • Image processing and optimization
    • HTML generation

System Design 

The rte_ckeditor_image extension follows TYPO3's modern extension architecture with CKEditor 5 integration, providing seamless FAL (File Abstraction Layer) image management within rich text editors.

High-Level Architecture 

TYPO3 BackendContent StorageFrontend RenderingCKEditorPluginImageControllerFALStorageJavaScriptDialogBackendRouteRTE Content withdata-htmlarea-* attributesTypoScriptHooksImageRenderingRenderedHTML
High-level system architecture

Core Components 

Backend Layer 

1. Controllers (Classes/Controller/) 

  • SelectImageController: Backend image selection wizard for FAL integration
  • ImageRenderingAdapter: TypoScript adapter bridging preUserFunc to modern service architecture

2. Event Listeners (Classes/Listener/) 

  • RteSoftrefEnforcer: Auto-adds rtehtmlarea_images softref to all RTE fields
  • RtePreviewRendererRegistrar: Auto-registers image-aware preview renderer for all CTypes
  • UpdateImageReferences: Syncs src attributes when FAL files are moved or renamed

3. Database Hooks (Classes/Database/) 

  • RteImagesDbHook: TCEmain data processing for image references

4. Data Handling (Classes/DataHandling/SoftReference/) 

  • RteImageSoftReferenceParser: Tracks soft references for link management

Frontend Layer (CKEditor Plugin) 

JavaScript Module (Resources/Public/JavaScript/Plugins/typo3image.js) 

  • Typo3Image Plugin: CKEditor 5 plugin class
  • Custom Model: typo3image element with rich attributes
  • UI Components: Image dialog, selection modal
  • Style Integration: StyleUtils and GeneralHtmlSupport integration
  • Conversion System: Upcast (HTML → Model) and Downcast (Model → HTML)

Configuration Layer 

YAML Configuration 

  • Services.yaml: Dependency injection container configuration
  • Plugin.yaml: RTE plugin registration

TypoScript 

  • setup.typoscript: Frontend rendering configuration
  • page.tsconfig: Backend RTE configuration

Backend Routes 

  • Routes.php: Backend route definitions for image selection

Technology Stack 

  • PHP: 8.2+ with strict types
  • TYPO3: 13.4 LTS / 14.3 LTS (Core, Backend, Frontend, Extbase, RTE CKEditor)
  • JavaScript: ES6 modules
  • CKEditor: 5.x provided by TYPO3 core with direct imports from @ckeditor/* namespace
  • Dependency Injection: Symfony service container
  • Standards: PSR-12, PER-CS2.0

Security Considerations 

  • File access through FAL security layer
  • Backend routes require authentication
  • Input validation on all user data
  • XSS prevention through proper encoding
  • Data attribute sanitization on frontend

Performance Considerations 

  • Processed images cached by TYPO3
  • Lazy loading support for frontend
  • Minimal JavaScript footprint
  • Efficient database queries with soft references

Design Patterns & Integration 

Design patterns, integration points, data flow, and extension mechanisms for the RTE CKEditor Image extension.

Overview 

This document explains the design patterns, integration approaches, and data flow used in the RTE CKEditor Image extension. For system architecture and components, see System Architecture.

Key Design Patterns 

The extension employs several proven design patterns for maintainability and extensibility:

  • MVC Pattern - Controllers, models, and views separation
  • Event-Driven - PSR-14 events for extensibility
  • Plugin Architecture - Modular CKEditor plugin
  • Soft References - TYPO3 reference tracking
  • Command Pattern - CKEditor commands for actions

Dependency Injection 

All PHP classes use Symfony's dependency injection:

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false
Copied!

This approach provides:

  • Loose coupling between components
  • Easier testing through dependency substitution
  • Clear component dependencies
  • Automatic service wiring

Event-Driven Architecture 

TYPO3 event system for loose coupling:

  • AfterPrepareConfigurationForEditorEvent - RTE configuration
  • TCEmain hooks for data processing

Benefits:

  • Components can be extended without modification
  • Third-party extensions can hook into processing
  • Maintainable separation of concerns
  • Clear extension points for customization

MVC Pattern 

Controllers handle requests, models represent data, views render output:

  • Controllers: Process backend requests and coordinate actions
  • FAL Models: Represent files and their metadata
  • TypoScript Views: Render frontend HTML output

This separation ensures:

  • Clear responsibility boundaries
  • Independent testing of each layer
  • Flexible view rendering strategies
  • Reusable business logic

Plugin Pattern 

CKEditor 5 plugin system:

  • Custom typo3image model element
  • Editor commands and UI components
  • Conversion system for data transformation

Implementation details:

  • Plugin registration via Plugin.yaml
  • Custom schema definitions for model elements
  • Bidirectional conversion (upcast/downcast)
  • Integration with CKEditor's command system

Integration Points 

TYPO3 Core Integration 

  1. RTE CKEditor: Extends TYPO3's CKEditor integration

    • Registers custom plugin through YAML configuration
    • Extends default RTE configuration
    • Adds TYPO3-specific functionality to editor
  2. FAL: Uses File Abstraction Layer for file management

    • Leverages FAL for unified file handling
    • Respects file permissions and access rights
    • Supports all FAL storage drivers
    • Maintains file reference integrity
  3. TCEmain: Hooks into data processing pipeline

    • Processes image references during save operations
    • Updates soft references automatically
    • Validates data integrity
    • Triggers reference index updates
  4. Soft References: Tracks file references for integrity

    • Custom soft reference parser for RTE images
    • Enables reference tracking across content
    • Supports reference index operations
    • Prevents orphaned file records

CKEditor Integration 

  1. Plugin Registration: Via JavaScriptModules.php and Plugin.yaml

    • Module registration in PHP
    • Plugin configuration in YAML
    • Integration with TYPO3's asset management
    • Proper loading order and dependencies
  2. Custom Model: typo3image element with TYPO3-specific attributes

    • Schema definition for element structure
    • Support for data-htmlarea-* attributes
    • Custom properties for FAL integration
    • Validation rules for data integrity
  3. Style System: Integration with CKEditor's style drop-down

    • Custom style definitions
    • Integration with GeneralHtmlSupport
    • TYPO3-specific class handling
    • Style persistence in content
  4. Conversion: Bidirectional HTML ↔ Model conversion

    • Upcast: HTML to model during editor initialization
    • Downcast: Model to HTML during save operations
    • Attribute mapping and transformation
    • Special handling for TYPO3 data attributes

Data Flow 

Image Selection Flow 

User clicks insert imageCKEditor plugin opens modalBackend route loads file browserUser selects imageJavaScript receives file UIDBackend API returns image infoDialog opens with image propertiesUser confirms settingstypo3image model element createdContent saved to database
Image selection workflow

Detailed steps:

  1. User Interaction: Editor toolbar button clicked
  2. Modal Opening: CKEditor executes custom command
  3. Browser Loading: AJAX call to backend route
  4. File Selection: User navigates FAL structure
  5. Data Retrieval: File UID sent to backend API
  6. Properties Dialog: JavaScript populates form with file data
  7. Confirmation: User sets dimensions, alignment, etc.
  8. Model Creation: CKEditor creates typo3image element
  9. Persistence: Content saved with data-htmlarea-* attributes

Frontend Rendering Flow 

RTE content loaded from databaselib.parseFunc_RTE processes contentImageRenderingAdapter hook invokedFAL file loaded from UIDMagic image processing appliedProcessed image URL generatedHTML with processed URL renderedInternal data-* attributes removed
Frontend rendering workflow

Detailed steps:

  1. Content Retrieval: Database query loads RTE field
  2. TypoScript Processing: lib.parseFunc_RTE activated
  3. Hook Execution: Custom rendering hook triggered
  4. File Loading: FAL resolves file UID to file object
  5. Image Processing: Magic image generation (resize, crop, etc.)
  6. URL Generation: Processed image URL created
  7. HTML Rendering: Final img tag generated
  8. Attribute Cleanup: Internal data-* attributes stripped

Extension Points 

Developers can extend the extension through:

  1. Event listeners (PSR-14 events)

    • AfterPrepareConfigurationForEditorEvent: Customize RTE configuration
    • Custom events can be added for additional hooks
    • Event priority allows fine-grained control
    • Standard TYPO3 event dispatcher patterns
  2. TypoScript configuration

    • Override rendering settings
    • Custom image processing instructions
    • Template modifications
    • Additional CSS classes or attributes
  3. XClasses (not recommended)

    • Last resort for core modifications
    • Potential compatibility issues
    • Better alternatives usually exist
    • Should only be used when no other option available
  4. Custom processing hooks

    • TCEmain hooks for data manipulation
    • Content element rendering hooks
    • Custom transformations during save/load
    • Validation and sanitization extensions
  5. Additional CKEditor plugins

    • Complementary functionality
    • Integration with typo3image plugin
    • Custom commands and UI components
    • Extended model attributes

Example Event Listener 

use TYPO3\CMS\RteCKEditor\Form\Element\Event\AfterPrepareConfigurationForEditorEvent;

class CustomRteConfigurationListener
{
    public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
    {
        $config = $event->getConfiguration();

        // Modify configuration as needed
        $config['typo3image']['customSetting'] = 'value';

        $event->setConfiguration($config);
    }
}
Copied!

Example TypoScript Extension 

lib.parseFunc_RTE {
    tags {
        img {
            width = 1920
            height = 1080

            // Custom processing
            params = class="custom-image-class"
        }
    }
}
Copied!

ADR-001: Image Scaling Behavior 

Status

Accepted

Date

2025-10-27

Authors

Development Team

Context

RTE CKEditor Image Extension for TYPO3

Context and Problem Statement 

The RTE CKEditor Image extension needs to provide flexible image processing options that balance quality, performance, and file size. Users need clear control over when images should be processed versus when original files should be used directly.

The system must handle various scenarios:

  • Different display quality requirements (web, retina displays, print)
  • Performance optimization (avoid unnecessary processing)
  • File size considerations (prevent serving oversized originals)
  • SVG handling (vector graphics don't need raster processing)

Decision Drivers 

  • User Control: Clear options for when to process vs use originals
  • Performance: Avoid unnecessary image processing operations
  • Quality: Provide appropriate quality for different use cases
  • File Size: Prevent serving excessively large files to browsers
  • Browser Compatibility: Ensure proper rendering across devices

Considered Options 

Option 1: Always Process Images 

  • Pros: Consistent behavior, predictable output
  • Cons: Unnecessary processing, performance overhead, potential quality loss

Option 2: Never Process Images (Always Use Originals) 

  • Pros: Maximum quality, no processing overhead
  • Cons: Large file sizes, no optimization for different displays

Option 3: Intelligent Conditional Processing (Selected) 

  • Pros: Balances quality, performance, and file size
  • Cons: More complex logic, requires configuration

Decision Outcome 

Chosen option: Option 3 - Intelligent Conditional Processing

The system implements a multi-tier approach with explicit user control and automatic optimization.

Image Processing Modes 

1. No Scaling (Skip Processing Entirely) 

Behavior: Use original file without any TYPO3 image processing

When to Use:

  • Newsletters (external email clients)
  • PDF generation
  • When maximum quality is required
  • When original file optimization is already optimal

Backend Attribute: data-noscale="1"

Example Scenario:

Original Image: 1920×1080 px (500 KB)
Display Size:   1920×1080 px
Processing:     NONE - original file used directly
Output URL:     /fileadmin/user_upload/image.jpg
Result:         Exact original file served (500 KB)
Copied!

Processing Info Message (Gray):

Processing Info: Image 1920×1080 px will be displayed at 1920×1080 px = ● Standard Quality (1.0x scaling)
Copied!

2. Low Quality (0.9x Multiplier) 

Behavior: Process with reduced quality for smaller file sizes

When to Use:

  • Thumbnail images
  • Background images where quality is less critical
  • Bandwidth-constrained scenarios

Example Scenario:

Original Image: 1920×1080 px
Display Size:   800×450 px
Multiplier:     0.9x
Calculation:    800×450 × 0.9 = 720×405 px
Processing:     YES - image scaled and compressed
Output URL:     /typo3temp/assets/processed/image_[hash].jpg
Result:         Processed file at 720×405 px
Copied!

Processing Info Message (Red):

Processing Info: Image 1920×1080 px will be resized to 720×405 px and displayed at 800×450 px = ● Low Quality (0.9x scaling)
Copied!

3. Standard Quality (1.0x Multiplier) 

Behavior: Process with exact display dimensions

When to Use:

  • General content images
  • Standard web displays (non-retina)
  • Balanced quality and file size

Example Scenario:

Original Image: 1920×1080 px
Display Size:   800×450 px
Multiplier:     1.0x
Calculation:    800×450 × 1.0 = 800×450 px
Processing:     YES - image scaled to exact display size
Output URL:     /typo3temp/assets/processed/image_[hash].jpg
Result:         Processed file at 800×450 px
Copied!

Processing Info Message (Orange):

Processing Info: Image 1920×1080 px will be resized to 800×450 px and displayed at 800×450 px = ● Standard Quality (1.0x scaling)
Copied!

4. Retina Quality (2.0x Multiplier) 

Behavior: Process with 2x display dimensions for high-DPI screens

When to Use:

  • Retina/HiDPI displays (MacBook, iPhone, modern monitors)
  • High-quality content imagery
  • Professional photography portfolios

Example Scenario:

Original Image: 1920×1500 px
Display Size:   960×750 px
Multiplier:     2.0x
Calculation:    960×750 × 2.0 = 1920×1500 px
Processing:     YES - image scaled for retina displays
Output URL:     /typo3temp/assets/processed/image_[hash].jpg
Result:         Processed file at 1920×1500 px
Copied!

Processing Info Message (Green):

Processing Info: Image 1920×1500 px will be resized to 1920×1500 px and displayed at 960×750 px = ● Retina Quality (2.0x scaling)
Copied!

5. Ultra Quality (3.0x Multiplier) 

Behavior: Process with 3x display dimensions for ultra-high-DPI

When to Use:

  • 4K/5K displays
  • Professional design work
  • Maximum quality requirements

Example Scenario:

Original Image: 5760×3240 px
Display Size:   640×360 px
Multiplier:     3.0x
Calculation:    640×360 × 3.0 = 1920×1080 px
Processing:     YES - image scaled for ultra displays
Output URL:     /typo3temp/assets/processed/image_[hash].jpg
Result:         Processed file at 1920×1080 px
Copied!

Processing Info Message (Cyan):

Processing Info: Image 5760×3240 px will be resized to 1920×1080 px and displayed at 640×360 px = ● Ultra Quality (3.0x scaling)
Copied!

6. Print Quality (6.0x Multiplier) 

Behavior: Process with 6x display dimensions for print output

When to Use:

  • Print-ready materials
  • High-resolution documents
  • Maximum quality print output

Example Scenario:

Original Image: 5760×3240 px
Display Size:   320×180 px
Multiplier:     6.0x
Calculation:    320×180 × 6.0 = 1920×1080 px
Processing:     YES - image scaled for print quality
Output URL:     /typo3temp/assets/processed/image_[hash].jpg
Result:         Processed file at 1920×1080 px
Copied!

Processing Info Message (Blue):

Processing Info: Image 5760×3240 px will be resized to 1920×1080 px and displayed at 320×180 px = ● Print Quality (6.0x scaling)
Copied!

Quality Calculation Logic 

Achievable Quality 

The system automatically determines what quality can actually be achieved based on the original image dimensions:

Quality Formula: actualQuality = min(imageWidth / displayWidth, imageHeight / displayHeight)

Examples:

  • Image 1920×1500, Display 960×750 → Quality = min(1920/960, 1500/750) = min(2.0, 2.0) = 2.0x (Retina)
  • Image 1920×1500, Display 192×150 → Quality = min(1920/192, 1500/150) = min(10.0, 10.0) = 10.0x (Print)
  • Image 1920×1500, Display 1920×1500 → Quality = min(1920/1920, 1500/1500) = min(1.0, 1.0) = 1.0x (Standard)
  • Image 1920×1500, Display 2840×3000 → Quality = min(1920/2840, 1500/3000) = min(0.68, 0.5) = 0.5x (Poor)

Processing Multipliers vs Achievable Quality 

When a processing option (Standard, Retina, Ultra, Print) is selected:

  • Requested Size = Display × Multiplier
  • Processed Size = min(Requested Size, Original Size) — Never upscale!
  • Actual Quality = Processed Size / Display Size

Example: Image 1920×1500, Display 1920×1500, Retina (2.0x)

  • Requested: 1920×1500 × 2.0 = 3840×3000
  • Processed: min(3840×3000, 1920×1500) = 1920×1500
  • Actual Quality: 1920/1920 = 1.0x (Standard, not Retina!)

Automatic Optimization Rules 

Rule 1: SVG Files (Always Skip Processing) 

Behavior: SVG files are NEVER processed regardless of settings

Rationale:

  • SVG is vector format that scales perfectly at any resolution
  • ImageMagick would rasterize SVG, losing vector benefits
  • Browser handles SVG scaling natively

Example:

File:           logo.svg (vector)
Display Size:   400×300 px
Setting:        ANY (ignored for SVG)
Processing:     NONE - original SVG used
Result:         Browser scales SVG natively
Copied!

Processing Info Message (Gray):

Processing Info: Vector image will not be processed (scales perfectly at any resolution).
Copied!

Rule 2: Dimensions Match Exactly (Skip Processing) 

Behavior: When display dimensions exactly match original, skip processing

Rationale:

  • No resize needed = no quality benefit from processing
  • Avoid unnecessary processing overhead
  • Preserve original file quality

Example:

Original Image: 1920×1080 px
Display Size:   1920×1080 px
Setting:        Standard (1.0x)
Processing:     NONE - dimensions match exactly
Result:         Original file used
Copied!

Processing Info Message (varies by scaling option):

No Scaling: Image 1920×1080 px will be used unchanged (no processing)
Standard (1.0x): Image 1920×1080 px will be displayed at 1920×1080 px = ● Standard Quality (1.0x scaling)
Retina (2.0x): Image 1920×1080 px will be displayed at 1920×1080 px = ● Standard Quality (1.0x scaling) [cannot achieve 2.0x]
Copied!

Note: When requested quality cannot be achieved (original image too small), the message shows the actual achievable quality.

Note: This rule is overridden by file size threshold (see Rule 4).

Rule 3: Display Exceeds Image Size (Skip Processing + Warning) 

Behavior: When display size is larger than original image, skip processing and warn

Rationale:

  • Upscaling degrades quality
  • Better to use original at natural size
  • User should be aware of quality limitation

Example:

Original Image: 800×600 px (small original)
Display Size:   1920×1080 px (larger than original)
Setting:        Retina (2.0x)
Processing:     NONE - cannot upscale quality
Result:         Original 800×600 px used, stretched by browser
Warning:        Quality degradation expected
Copied!

Processing Info Message (Red warning):

Processing Info: Image 800×600 px will be displayed at 1920×1080 px = ● Poor Quality (0.4x scaling)
Copied!

Rule 4: File Size Threshold (Force Processing) 

Behavior: Large files are processed even when dimensions match

Configuration:

lib.parseFunc_RTE.tags.img {
    noScale = 1
    noScale {
        maxFileSizeForAuto = 2000000  # 2MB threshold
    }
}
Copied!

Rationale:

  • Prevent serving multi-megabyte originals
  • Optimize file size through compression
  • Balance quality and bandwidth

Example:

Original Image: 1920×1080 px (5 MB uncompressed TIFF)
Display Size:   1920×1080 px (exact match)
File Size:      5,242,880 bytes (> 2MB threshold)
Processing:     YES - exceeds size threshold
Result:         Processed JPEG at 1920×1080 px (~500 KB)
Copied!

Frontend Rendering (ImageRenderingAdapter) 

Processing Decision Logic 

protected function shouldSkipProcessing(
    File $originalFile,
    array $imageConfiguration,
    bool $noScale,
    int $maxFileSizeForAuto = 0
): bool {
    // RULE 1: SVG files - always skip
    if (strtolower($originalFile->getExtension()) === 'svg') {
        return true;
    }

    // RULE 2: Explicit noScale setting OR data-noscale attribute
    if ($noScale) {
        return true;
    }

    // Get dimensions
    $originalWidth = (int) ($originalFile->getProperty('width') ?? 0);
    $originalHeight = (int) ($originalFile->getProperty('height') ?? 0);
    $requestedWidth = (int) ($imageConfiguration['width'] ?? 0);
    $requestedHeight = (int) ($imageConfiguration['height'] ?? 0);

    // RULE 3: No dimensions requested - use original
    if ($requestedWidth === 0 && $requestedHeight === 0) {
        return true;
    }

    // RULE 4: Dimensions match exactly
    if ($requestedWidth === $originalWidth && $requestedHeight === $originalHeight) {
        // Check file size threshold
        if ($maxFileSizeForAuto > 0) {
            $fileSize = $originalFile->getSize();
            // Exceeds threshold - process to reduce size
            if ($fileSize > $maxFileSizeForAuto) {
                return false;
            }
        }
        // Within threshold or no limit - skip processing
        return true;
    }

    // Different dimensions - processing needed
    return false;
}
Copied!

Configuration Examples 

Global No Processing (All RTE Images) 

TypoScript Setup
# TypoScript Setup
lib.parseFunc_RTE.tags.img.noScale = 1
Copied!

Result: ALL images use originals, no processing

Selective No Processing (Per Image) 

Users set "No Scaling" option in image dialog.

Result: Only images with data-noscale="1" skip processing

File Size Optimized 

TypoScript Setup
lib.parseFunc_RTE.tags.img {
    noScale = 0  # Enable processing
    noScale {
        maxFileSizeForAuto = 2000000  # 2MB
    }
}
Copied!

Result: Images processed only when needed, automatic optimization for large files

User Interface Indicators 

Color Coding 

Quality Color Hex Usage
No Scaling Gray #6c757d No processing
Low Red #dc3545 Reduced quality
Standard Orange #ffc107 Balanced
Retina Green #28a745 High quality
Ultra Cyan #17a2b8 Ultra quality
Print Blue #007bff Print quality

Processing Info States 

  1. No Processing (Gray) - Original file used
  2. Normal Processing (Blue) - Standard resize operation
  3. Exact Match (Green) - No processing needed, dimensions match
  4. Oversized Display (Red) - Warning about quality limitation

Technical Implications 

Backend (SelectImageController) 

  • Validation: Enforce dimension limits (1-10000px) to prevent resource exhaustion
  • Security: Verify file access permissions (IDOR protection)
  • Performance: Use efficient file property access

Frontend (ImageRenderingAdapter) 

  • Caching: Processed images cached in typo3temp/assets/
  • Security: Block non-public files from frontend rendering
  • Performance: Skip processing when possible to reduce server load

JavaScript (typo3image.js) 

  • Real-time Calculation: Show expected output dimensions
  • Visual Feedback: Color-coded quality indicators
  • Validation: Prevent invalid dimension combinations

Consequences 

Positive 

  • Flexibility: Users control when processing occurs
  • Performance: Automatic optimization reduces unnecessary operations
  • Quality: Appropriate processing for different use cases
  • File Size: Prevents serving oversized originals

Negative 

  • Complexity: More logic to maintain and test
  • Learning Curve: Users need to understand when to use each option
  • Edge Cases: Requires careful handling of dimension mismatches

Compliance 

  • TYPO3 Standards: Follows FAL (File Abstraction Layer) patterns
  • Security: Implements access control and resource limits
  • Performance: Optimizes for typical web usage patterns

References 

Revision History 

Date Version Changes
2025-10-27 1.0 Initial ADR documenting scaling behavior

ADR-002: Native CKEditor 5 vs Custom TYPO3 Image Plugin 

Status

Accepted

Date

2025-11-09

Authors

Development Team

Context

RTE CKEditor Image Extension for TYPO3

Context and Problem Statement 

TYPO3 integrates CKEditor 5 as its Rich Text Editor (RTE), and images are a fundamental content element. CKEditor 5 provides comprehensive native image plugins (Image, ImageCaption, ImageToolbar, ImageResize, ImageStyle, LinkImage) with excellent WYSIWYG capabilities including:

  • Inline editable captions
  • Contextual toolbars on image click
  • Visual resize handles
  • Pre-defined image styles (alignment, sizing)
  • Image linking capabilities
  • Text alternative (alt) editing

However, TYPO3 has specific requirements for file handling through its File Abstraction Layer (FAL) that are incompatible with CKEditor 5's native image implementation.

The Question: Should we use CKEditor 5's native image plugins or implement a custom plugin specifically for TYPO3?

Decision Drivers 

TYPO3 Core Requirements 

  • FAL Integration: All files must be managed through TYPO3's File Abstraction Layer
  • Reference Tracking: sys_file_reference database records for all file usage
  • Magic Image Processing: TYPO3's automatic image optimization and variant generation
  • Security: File access permissions and public/non-public file handling
  • Backend Integration: File selector dialog and metadata management

User Experience Requirements 

  • WYSIWYG Editing: Inline caption editing, visual resize, contextual toolbars
  • Accessibility: Alt text, semantic HTML structure
  • Flexibility: Image styles, linking, sizing options
  • Performance: Optimized image delivery

Technical Requirements 

  • Data Persistence: FAL attributes must survive RTE → DB → RTE round-trips
  • Backend Rendering: PHP-based frontend rendering with TypoScript integration
  • Compatibility: Work across TYPO3 versions (v12/v13)

Considered Options 

Option 1: Use CKEditor 5 Native Image Plugins 

Use the official CKEditor 5 image feature set:

  • @ckeditor/ckeditor5-image (base Image plugin)
  • @ckeditor/ckeditor5-image/imagecaption (ImageCaption)
  • @ckeditor/ckeditor5-image/imagetoolbar (ImageToolbar)
  • @ckeditor/ckeditor5-image/imageresize (ImageResize)
  • @ckeditor/ckeditor5-image/imagestyle (ImageStyle)
  • @ckeditor/ckeditor5-link/linkimage (LinkImage)

Pros:

  • ✅ Excellent WYSIWYG experience out-of-the-box
  • ✅ Inline editable captions with proper UX
  • ✅ Contextual balloon toolbar on image selection
  • ✅ Visual resize handles with dimension control
  • ✅ Pre-built image styles (alignment, sizing)
  • ✅ Official support and documentation
  • ✅ Regular updates and bug fixes
  • ✅ Accessibility features built-in
  • ✅ Tested across browsers and devices

Cons:

  • No FAL Support: Uses direct image URLs (src attribute), not FAL file references
  • No Reference Tracking: Cannot create sys_file_reference records
  • No Magic Image Processing: Cannot integrate with TYPO3's image processing pipeline
  • No Backend Integration: Cannot use TYPO3's file selector dialog
  • Data Attribute Loss: Cannot store FAL-specific metadata (data-htmlarea-file-uid, data-htmlarea-file-table)
  • Security Issues: Cannot enforce TYPO3's file access permissions
  • Frontend Rendering Incompatibility: PHP rendering expects FAL attributes, not direct URLs
  • TYPO3 Core Explicitly Disables It: Core configuration removes the plugin

Option 2: Custom TYPO3 Image Plugin (Selected) 

Implement a custom CKEditor 5 plugin (typo3image) that:

  • Uses custom model element (typo3image instead of imageBlock)
  • Stores FAL attributes in the model
  • Integrates with TYPO3's file selector dialog
  • Provides backend rendering through PHP controllers
  • Supports magic image processing

Pros:

  • Full FAL Integration: Stores file UID and table references
  • Reference Tracking: Creates proper sys_file_reference records
  • Magic Image Processing: Full integration with TYPO3's image pipeline
  • Backend Integration: Uses TYPO3's file selector dialog
  • Data Persistence: All FAL attributes preserved through save/load cycles
  • Security: Respects TYPO3's file access permissions
  • Frontend Rendering: PHP controllers handle all rendering logic
  • TypoScript Integration: Full control via TypoScript configuration
  • Feature Control: Can implement exactly what TYPO3 needs

Cons:

  • ❌ Must implement WYSIWYG features manually (captions, toolbar, resize, styles)
  • ❌ Higher development and maintenance effort
  • ❌ Requires deep CKEditor 5 plugin architecture knowledge
  • ❌ Must keep up with CKEditor 5 API changes
  • ❌ More complex than using native plugins

Option 3: Hybrid Approach (Native Plugins + FAL Bridge) 

Use native CKEditor 5 plugins but add a bridge layer to convert to/from FAL.

Pros:

  • ✅ WYSIWYG features from native plugins
  • ✅ Potential FAL integration through conversion

Cons:

  • Model Incompatibility: Native imageBlock model doesn't support FAL attributes
  • Data Loss Risk: Conversion between incompatible models prone to errors
  • Plugin Conflicts: Native plugins expect specific model schema
  • Maintenance Nightmare: Must bridge two incompatible architectures
  • TYPO3 Core Still Disables Native Plugin: Cannot be used without core modifications

Decision Outcome 

Chosen option: Option 2 - Custom TYPO3 Image Plugin

The custom plugin approach is the only viable option for TYPO3 integration.

Evidence and Technical Justification 

1. TYPO3 Core Explicitly Disables Native Image Plugin 

File: typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml

editor:
  config:
    removePlugins:
      - image  # ← Native Image plugin is disabled
Copied!

Source: TYPO3 Core Documentation

"By default, images in CKE are disabled within configuration typo3/sysext/rte_ckeditor/Configuration/RTE/Default.yaml with removePlugins: - image"

This is not accidental — it's a deliberate architectural decision by the TYPO3 core team.

2. FAL Requirements Incompatible with Native Plugins 

Required FAL Data Attributes 

<img
  src="fileadmin/user_upload/image.jpg"
  data-htmlarea-file-uid="123"
  data-htmlarea-file-table="sys_file"
  alt="Example"
  width="800"
  height="600"
/>
Copied!

Why These Matter:

  • data-htmlarea-file-uid: Links to sys_file record for reference tracking
  • data-htmlarea-file-table: Specifies FAL table (always sys_file)
  • These attributes trigger sys_file_reference creation in TYPO3 backend
  • Without them, TYPO3 cannot track where files are used
  • Reference index breaks, file deletion safety checks fail

Native Plugin Model 

// CKEditor 5 native imageBlock model
editor.model.schema.register('imageBlock', {
  allowAttributes: ['src', 'alt', 'srcset', 'sizes']
  // ❌ No support for data-* attributes
  // ❌ No FAL metadata storage
});
Copied!

3. Magic Image Processing Requirements 

Frontend Rendering Flow:

1. RTE saves: <img data-htmlarea-file-uid="123" width="800" height="600" />
2. TYPO3 Backend: Creates sys_file_reference record
3. Frontend Rendering (PHP):
   a. Load file from FAL via UID
   b. Apply TypoScript configuration
   c. Generate processed image variant
   d. Output: <img src="/typo3temp/processed/image_hash.jpg" />
Copied!

TypoScript Hook:

setup.typoscript:12
lib.parseFunc_RTE {
    tags.img = TEXT
    tags.img {
        preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
    }
}
Copied!

Why Native Plugins Can't Support This:

  • Native plugins use direct src URLs, not FAL UIDs
  • ImageRenderingAdapter->renderImageAttributes() expects FAL data attributes
  • No way to look up file in FAL without UID
  • Cannot apply magic image processing without FAL context

4. Backend Integration Requirements 

TYPO3 File Selector Dialog:

typo3image.js:349
externalPlugins: {
  typo3image: {
    route: "rteckeditorimage_wizard_select_image"  // ← TYPO3 backend route
  }
}
Copied!

Flow:

  1. User clicks "Insert Image" button
  2. Custom plugin opens TYPO3's file selector dialog
  3. User selects file from FAL storage
  4. Dialog returns { fileUid: 123, fileTable: 'sys_file', ... }
  5. Plugin creates typo3image model element with FAL attributes

Native Plugin Limitation:

  • Native uploadImage plugin expects file upload or URL input
  • No integration point for TYPO3's file selector
  • Cannot access FAL metadata
  • Cannot select existing files from storage

5. Database Reference Tracking 

sys_file_reference Table:

CREATE TABLE sys_file_reference (
  uid_local int,        -- Points to sys_file.uid
  uid_foreign int,      -- Points to tt_content.uid
  tablenames varchar,   -- 'tt_content'
  fieldname varchar     -- 'bodytext'
);
Copied!

Created by:

Classes/Database/RteImagesDbHook.php:18
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
    = RteImagesDbHook::class;
Copied!

Why This Matters:

  • TYPO3's "Used on" feature shows where images are referenced
  • Prevents deleting images still in use
  • File usage statistics
  • Broken link detection
  • Requires FAL UIDs — native plugins provide only URLs

Implementation: Custom Plugin Architecture 

Model Schema 

typo3image.js:871-894
editor.model.schema.register('typo3image', {
    inheritAllFrom: '$blockObject',
    allowAttributes: [
        'fileUid',          // FAL: sys_file.uid
        'fileTable',        // FAL: table name (sys_file)
        'src',              // Image URL (for editing preview)
        'alt',              // Accessibility
        'title',            // Image title
        'width',            // Display dimensions
        'height',           // Display dimensions
        'enableZoom',       // Click-to-enlarge
        'noScale',          // Skip image processing
        'quality',          // Processing quality multiplier
        'caption',          // Image caption text
        'htmlA',            // Link wrapper attributes
        // ... more FAL-specific attributes
    ]
});
Copied!

Upcast Converter (HTML → Model) 

typo3image.js:1061-1077
editor.conversion.for('upcast').elementToElement({
    view: {
        name: 'img',
        attributes: {
            'data-htmlarea-file-uid': true  // ← FAL attribute required
        }
    },
    model: (viewElement, { writer }) => {
        return writer.createElement('typo3image', {
            fileUid: viewElement.getAttribute('data-htmlarea-file-uid'),
            fileTable: viewElement.getAttribute('data-htmlarea-file-table'),
            src: viewElement.getAttribute('src'),
            // ... extract all FAL attributes
        });
    }
});
Copied!

Downcast Converter (Model → HTML) 

typo3image.js:1113-1129
editor.conversion.for('dataDowncast').elementToElement({
    model: 'typo3image',
    view: (modelElement, { writer }) => {
        const img = writer.createEmptyElement('img', {
            'src': modelElement.getAttribute('src'),
            'data-htmlarea-file-uid': modelElement.getAttribute('fileUid'),
            'data-htmlarea-file-table': modelElement.getAttribute('fileTable'),
            // ... output all FAL attributes
        });
        return toWidget(figure, writer);
    }
});
Copied!

Frontend Rendering 

ImageRenderingAdapter.php (conceptual)
public function renderImageAttributes(?string $content, array $conf, ServerRequestInterface $request): string
{
    // Extract data-htmlarea-file-uid from HTML
    $fileUid = $this->extractFileUid($content);

    // Load file from FAL
    $file = $this->resourceFactory->getFileObject($fileUid);

    // Apply magic image processing
    $processedImage = $this->imageService->applyProcessingInstructions($file, [
        'width' => $width,
        'height' => $height,
        'crop' => $cropData,
    ]);

    // Generate <img> tag with processed URL
    return sprintf('<img src="%s" alt="%s" />',
        $processedImage->getPublicUrl(),
        $alt
    );
}
Copied!

Consequences 

Positive 

  • Full TYPO3 Integration: Complete FAL support, reference tracking, magic image processing
  • Security: Respects TYPO3's file permission system
  • Flexibility: Can implement exactly what TYPO3 needs, no compromises
  • TypoScript Control: Full configuration via TypoScript
  • Future-Proof: Can add TYPO3-specific features without CKEditor limitations
  • Backward Compatibility: Works with existing TYPO3 content and workflows

Negative 

  • WYSIWYG Features Missing: Must implement caption editing, contextual toolbar, visual resize, image styles manually
  • Development Effort: Significant implementation work for WYSIWYG features
  • Maintenance Burden: Must track CKEditor 5 API changes and update accordingly
  • Feature Parity Challenge: Native plugins have better UX, must match quality
  • Knowledge Requirements: Deep CKEditor 5 plugin architecture expertise needed

Current Limitations 

The custom plugin currently lacks some WYSIWYG features present in native plugins:

  1. Caption Not Inline Editable: Uses dialog instead of click-to-edit

    • Root cause: toWidget() wrapper prevents nested editables
    • Status: Implementation gap - can be addressed with proper editable nesting
  2. No Contextual Toolbar: No balloon toolbar on image selection

    • IMPLEMENTED in feature/wysiwyg-caption-fixes branch via WidgetToolbarRepository
  3. No Visual Resize: No drag handles for resizing

    • Root cause: WidgetResize is NOT available in TYPO3's CKEditor 5 build (confirmed in typo3image.js:1576)
    • Status: Architectural limitation - cannot be implemented without CKEditor build changes
    • Alternative: Resize functionality available via context toolbar buttons
  4. No Image Styles: No pre-defined alignment/sizing options

    • IMPLEMENTED via balloon toolbar (alignment buttons: left/center/right/block)

Summary: Most gaps are implementation issues that can be addressed. Visual resize handles are an architectural limitation due to WidgetResize unavailability in TYPO3's CKEditor 5 build.

Compliance 

  • TYPO3 Core Architecture: Follows FAL (File Abstraction Layer) patterns
  • CKEditor 5 Plugin API: Implements official plugin architecture
  • Security: File access control and permission checking
  • Performance: Optimized image processing pipeline
  • Accessibility: Semantic HTML structure support

References 

Future Considerations 

While native CKEditor 5 plugins cannot be used directly, their implementation patterns can guide our custom plugin development:

  • Study ImageCaption source for inline editable caption UX
  • Study ImageToolbar source for contextual balloon toolbar
  • Study ImageResize source for visual resize handle implementation
  • Study ImageStyle source for style dropdown integration

Goal: Achieve feature parity with native plugins while maintaining full TYPO3 FAL integration.

Revision History 

Date Version Changes
2025-11-09 1.0 Initial ADR documenting why native CKEditor 5 plugins cannot be used with TYPO3 FAL

ADR-003: Security Responsibility Boundaries 

Date

2025-12-14

Status

Accepted

Context

Code Review v13.0.1 → v13.2.x

Summary 

This ADR documents the security responsibility boundaries between this extension (netresearch/rte-ckeditor-image) and TYPO3 Core. Clear boundaries prevent scope creep and ensure security issues are addressed by the appropriate party.

Decision 

The following security responsibilities are explicitly out of scope for this extension and are delegated to TYPO3 Core:

Out of Scope (TYPO3 Core Responsibility) 

  1. SVG Sanitization

    • SVG files can contain embedded JavaScript (<script> tags, event handlers)
    • TYPO3 FAL is responsible for validating and sanitizing uploaded SVG files
    • This extension only references files already accepted by TYPO3
    • Related issue: #474 tracks optional additional protection, but Core sanitization is primary defense
  2. File Extension / MIME Type Validation

    • Ensuring a file's extension matches its actual content type
    • Example: Blocking a .jpg file that contains SVG/XML content
    • TYPO3 FAL validates this during upload via FileNameValidator and MIME checks
    • This extension trusts FAL's validation when referencing sys_file records
  3. General File Upload Security

    • Virus scanning, file size limits, allowed extensions
    • All handled by TYPO3 FAL and $GLOBALS['TYPO3_CONF_VARS']['BE']['fileDenyPattern']
  4. Image Processing Security

    • ImageMagick/GraphicsMagick command injection prevention
    • Handled by TYPO3's GraphicalFunctions and ImageService

In Scope (This Extension's Responsibility) 

  1. Caption XSS Prevention

    • User-editable caption text must be sanitized
    • Implementation: htmlspecialchars($caption, ENT_QUOTES | ENT_HTML5, 'UTF-8')
    • Location: ImageResolverService::sanitizeCaption()
  2. File Visibility Validation

    • Prevent rendering images from non-public storages
    • Backend users should not expose internal files via RTE
    • Location: ImageResolverService::validateFileVisibility()
  3. Dangerous Protocol Blocking

    • Block javascript:, vbscript:, data:text/html in URLs
    • Tracked in #475
    • Location: ImageResolverService::DANGEROUS_PROTOCOLS
  4. SSRF Protection for External Images

    • DNS rebinding prevention
    • Private/reserved IP blocking
    • Cloud metadata endpoint blocking
    • Location: RteImagesDbHook::getSafeIpForExternalFetch()
  5. Style Attribute Exclusion

    • Prevent CSS injection via style attributes
    • Style attributes are explicitly excluded from htmlAttributes
    • Location: ImageResolverService::buildHtmlAttributes()
  6. SVG Data URI Sanitization

    • Sanitize embedded JavaScript in data:image/svg+xml URIs
    • Removes <script> tags, event handlers (onload, onerror, etc.), and javascript: hrefs
    • Uses TYPO3 Core's SvgSanitizer for consistency with FAL sanitization
    • Location: ImageResolverService::sanitizeSvgDataUri()
    • Addresses: #474
  7. External-link rel security (Fluid path)

    • Append rel="noreferrer" on target="_blank" external links in the figure-wrapped Fluid render path, mirroring TYPO3's LinkFactory::addSecurityRelValues()
    • In scope because the Fluid Link.html partial constructs <a> directly and does not go through LinkFactory, so Core's security helper never executes on this path
    • Preserves any pre-existing rel tokens (nofollow, sponsored, noopener); appends noreferrer at most once
    • Location: Service\\SecurityRelComputer::compute(), wired in Service\\ImageResolverService::buildLinkDto() and createDtoFromExternalImage()
    • Addresses: #799

Known Boundaries & Limitations 

  1. Data URI Handling

    Data URIs (data:image/*) bypass FAL upload validation entirely since they are embedded inline rather than uploaded as files. Security measures are in place:

    • Blocked: data:text/html, javascript:, vbscript:, file: protocols
    • Allowed: data:image/* for legitimate inline images (Base64-encoded)
    • Sanitized: data:image/svg+xml content is passed through TYPO3's SvgSanitizer to remove embedded JavaScript (<script> tags, event handlers, javascript: hrefs)

    Rationale: Blocking all data URIs would break legitimate use cases (copy/paste images from clipboard). SVG data URIs are now sanitized at render time using the same sanitizer that TYPO3 FAL uses for uploaded SVG files, providing consistent protection regardless of how the SVG content was introduced.

  2. Frontend Context Processing

    The RteImagesDbHook skips processing in frontend contexts rather than throwing exceptions. This ensures DataHandler operations triggered from frontend (e.g., frontend editing extensions, TypoScript-based content manipulation) do not crash.

    • Images in frontend-saved content retain their original URLs
    • Magic image processing only occurs during backend saves
    • This is intentional to prevent breaking frontend editing workflows

Consequences 

Positive:

  • Clear accountability for security issues
  • Prevents duplicate security implementations
  • Reduces extension complexity by leveraging Core security

Negative:

  • Relies on TYPO3 Core maintaining its security measures
  • Sites with outdated TYPO3 versions may have gaps

Mitigations:

  • Document minimum TYPO3 version requirements
  • Optional additional protections tracked in GitHub issues (#474, #475)
  • Users can enable stricter settings via extension configuration

References 

API Documentation 

Complete API reference for all PHP classes in the RTE CKEditor Image extension.

Table of Contents

Changed in version 13.1.5

Major architecture update: legacy controllers replaced with service-based pipeline. See Services API for the new approach.

API Components 

🎮 Controllers API 

TypoScript adapters and backend controllers for image handling.

⚙️ Services API 

New service architecture with clean separation of concerns.

📦 Data Transfer Objects 

Type-safe DTOs for validated image data.

📊 Data Handling API 

Database hooks, content processing, and image transformations.

🔔 Event Listeners 

PSR-14 event system integration for RTE configuration.

🖼️ ViewHelpers 

Fluid ViewHelpers for Content Blocks backend previews.

Usage Examples 

See Common Use Cases for practical implementation examples of these APIs.

Controllers API 

Controllers handle HTTP requests in the backend and bridge TypoScript to the service architecture.

Changed in version 13.1.5

The legacy ImageRenderingController and ImageLinkRenderingController were removed and replaced with ImageRenderingAdapter using the new service architecture. See Services API for the new service-based approach.

SelectImageController 

class SelectImageController
Fully qualified name
\Netresearch\RteCKEditorImage\Controller\SelectImageController

Main controller for image selection and processing in the CKEditor context.

Backend Route

rteckeditorimage_wizard_select_image/rte/wizard/selectimage

Methods 

mainAction() 

mainAction ( ServerRequestInterface $request) : ResponseInterface

Entry point for the image browser/selection interface.

param ServerRequestInterface $request

PSR-7 server request with query parameters.

returntype

ResponseInterface

Returns

PSR-7 response with file browser HTML.

Query parameters:

  • mode: Browser mode (default: file from route configuration).
  • bparams: Browser parameters passed to file browser.

Usage example:

CKEditor plugin integration
// Called from CKEditor plugin
const contentUrl = routeUrl + '&contentsLanguage=en&editorId=123&bparams=' + bparams.join('|');
Copied!

infoAction() 

infoAction ( ServerRequestInterface $request) : ResponseInterface

Returns JSON with image information and processed variants.

param ServerRequestInterface $request

Server request with file identification and processing parameters.

returntype

ResponseInterface

Returns

JSON response with image data.

Query parameters:

  • fileId: FAL file UID.
  • table: Database table (usually sys_file).
  • P[width]: Desired width (optional).
  • P[height]: Desired height (optional).
  • action: Action type (info).

Response structure:

Image info API response
{
  "uid": 123,
  "url": "/fileadmin/user_upload/image.jpg",
  "width": 1920,
  "height": 1080,
  "title": "Image title",
  "alt": "Alternative text",
  "processed": {
    "url": "/fileadmin/_processed_/image_hash.jpg",
    "width": 800,
    "height": 450
  },
  "lang": {
    "override": "Override %s",
    "overrideNoDefault": "Override (no default)",
    "zoom": "Zoom",
    "cssClass": "CSS Class"
  }
}
Copied!

ImageRenderingAdapter 

New in version 13.1.5

Replaces the legacy ImageRenderingController and ImageLinkRenderingController.

class ImageRenderingAdapter
Fully qualified name
\Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter

TypoScript adapter bridging preUserFunc to the modern service architecture.

The adapter serves as a thin layer between TypoScript's preUserFunc interface and the service-based architecture. It delegates actual processing to:

Methods 

renderImageAttributes() 

renderImageAttributes ( $content, $conf)

Processes <img> tags in RTE content using the service pipeline.

param string $content

Current HTML content (single <img> tag).

param array $conf

TypoScript configuration.

returntype

string

Returns

Processed HTML with updated image URL and attributes.

Processing pipeline:

  1. ImageAttributeParser extracts data attributes from HTML.
  2. ImageResolverService resolves FAL file, applies security checks, processes image.
  3. ImageRenderingService renders via Fluid template.

TypoScript integration:

Image tag processing configuration
lib.parseFunc_RTE {
    tags.img = TEXT
    tags.img {
        current = 1
        preUserFunc = Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter->renderImageAttributes
    }
}
Copied!

Service configuration 

All controllers are configured in Configuration/Services.yaml:

EXT:rte_ckeditor_image/Configuration/Services.yaml
Netresearch\RteCKEditorImage\Controller\SelectImageController:
  tags: ['backend.controller']

Netresearch\RteCKEditorImage\Controller\ImageRenderingAdapter:
  public: true
Copied!

Controllers use constructor injection for dependencies.

Migration from legacy controllers 

Changed in version 13.1.5

If you were extending the legacy controllers via XCLASS, migrate to:

  1. Template overrides (recommended): Override Fluid templates in your site package. See Template Overrides.
  2. Service decoration: Decorate ImageResolverService or ImageRenderingService. See Services API.

The TypoScript interface remains 100% backward compatible - no changes required for standard usage.

Services API 

New in version 13.1.5

The new service architecture replaces legacy controllers with a clean pipeline: Parser → Resolver → Renderer.

The RTE CKEditor Image extension uses a three-service architecture following TYPO3 v13 best practices with clear separation of concerns.

Architecture overview 

ImageAttributeParserImageResolverServiceImageRenderingServiceRendered HTMLHTML ParsingDOMDocumentAttribute extractionBusiness LogicSecurity ValidationFAL ProcessingFluid RenderingViewFactoryInterfaceTemplate SelectionRaw attributesImageRenderingDto
Service pipeline architecture

ImageAttributeParser 

class ImageAttributeParser
Fully qualified name
\Netresearch\RteCKEditorImage\Service\ImageAttributeParser

Pure HTML parsing using DOMDocument - no business logic.

Responsibility 

  • Extract raw attributes from HTML strings.
  • Parse <img> tags within content.
  • Parse <a> tags containing <img> tags.
  • NO validation, NO sanitization - just parsing.

Methods 

parseImageAttributes() 

parseImageAttributes ( $html)

Parse attributes from <img> tag HTML string.

param string $html

HTML string containing <img> tag.

returntype

array<string,string>

Returns

Attribute name => value pairs.

Example:

Parsing image attributes
$parser = GeneralUtility::makeInstance(ImageAttributeParser::class);
$attributes = $parser->parseImageAttributes(
    '<img src="image.jpg" data-htmlarea-file-uid="123" alt="Example" />'
);
// Returns: ['src' => 'image.jpg', 'data-htmlarea-file-uid' => '123', 'alt' => 'Example']
Copied!

parseLinkWithImages() 

parseLinkWithImages ( $html)

Parse attributes from <a> tag containing <img> tags.

param string $html

HTML string containing <a><img /></a>.

returntype

array{link: array<string,string>, images: array}

Returns

Array with link and images keys.

Return structure:

Return value structure
[
    'link' => ['href' => 'page.html', 'title' => 'Link title'],
    'images' => [
        [
            'attributes' => ['src' => 'image.jpg', 'alt' => 'Alt text'],
            'originalHtml' => '<img src="image.jpg" alt="Alt text" />'
        ]
    ]
]
Copied!

ImageResolverService 

class ImageResolverService
Fully qualified name
\Netresearch\RteCKEditorImage\Service\ImageResolverService

Business logic, security validation, and FAL processing.

Responsibility 

  • Transform raw attributes into validated DTOs.
  • Resolve FAL file references.
  • Apply security checks (file visibility, protocol blocking).
  • Process images with quality settings.
  • ALL security validation happens here.

Security features 

New in version 13.1.5

The service includes comprehensive security measures:

  • File visibility validation: Prevents access to hidden/restricted files.
  • Protocol blocking: Blocks dangerous protocols (javascript:, file:, data:text/html, vbscript:).
  • XSS prevention: Uses htmlspecialchars() with ENT_QUOTES | ENT_HTML5.
  • Type safety: Read-only DTO properties prevent modification.

Quality settings 

New in version 13.1.5

The service supports quality multipliers for image processing:

Quality constants in ImageResolverService
const QUALITY_NONE     = 'none';     // N/A - Skip processing entirely
const QUALITY_LOW      = 'low';      // 0.9x - Performance optimized
const QUALITY_STANDARD = 'standard'; // 1.0x - Default
const QUALITY_RETINA   = 'retina';   // 2.0x - High-DPI displays
const QUALITY_ULTRA    = 'ultra';    // 3.0x - Extra sharp
const QUALITY_PRINT    = 'print';    // 6.0x - Print quality
Copied!

Methods 

resolve() 

resolve ( $attributes, $conf, $request, $linkAttributes = null)

Resolve image attributes to validated DTO.

param array $attributes

Raw attributes from parser.

param array $conf

TypoScript configuration.

param ServerRequestInterface $request

Current request.

param array|null $linkAttributes

Optional link attributes for linked images.

returntype

ImageRenderingDto|null

Returns

Validated DTO or null if validation fails.

Example:

Resolving image attributes to DTO
$resolver = GeneralUtility::makeInstance(ImageResolverService::class);
$dto = $resolver->resolve(
    $attributes,
    $typoScriptConfig,
    $request
);

if ($dto === null) {
    // Validation failed - return original content
    return $content;
}
Copied!

ImageRenderingService 

class ImageRenderingService
Fully qualified name
\Netresearch\RteCKEditorImage\Service\ImageRenderingService

Presentation layer using TYPO3 v13 ViewFactoryInterface.

Responsibility 

  • Render validated DTOs via Fluid templates.
  • Select appropriate template based on context.
  • NO business logic, NO validation - trusts the DTO.

Template selection 

The service automatically selects templates based on the rendering context:

Context Template
Standalone image Image/Standalone
Image with caption Image/WithCaption
Image within link Image/Link
Linked image with caption Image/LinkWithCaption
Image with zoom/popup Image/Popup
Popup image with caption Image/PopupWithCaption

Methods 

render() 

render ( ImageRenderingDto $imageData, ServerRequestInterface $request) : string

Render image HTML from validated DTO.

param ImageRenderingDto $imageData

Validated image data.

param ServerRequestInterface $request

Current request.

returntype

string

Returns

Rendered HTML.

Example:

Rendering image from DTO
$renderer = GeneralUtility::makeInstance(ImageRenderingService::class);
$html = $renderer->render($dto, $request);
Copied!

Service decoration 

To customize service behavior, use Symfony service decoration:

EXT:my_extension/Configuration/Services.yaml
App\Service\CustomImageResolver:
  decorates: Netresearch\RteCKEditorImage\Service\ImageResolverService
  arguments:
    $inner: '@.inner'
Copied!
EXT:my_extension/Classes/Service/CustomImageResolver.php
<?php

declare(strict_types=1);

namespace App\Service;

use Netresearch\RteCKEditorImage\Service\ImageResolverService;
use Netresearch\RteCKEditorImage\Domain\Model\ImageRenderingDto;

class CustomImageResolver
{
    public function __construct(
        private readonly ImageResolverService $inner
    ) {}

    public function resolve(...$args): ?ImageRenderingDto
    {
        // Custom pre-processing
        $dto = $this->inner->resolve(...$args);
        // Custom post-processing
        return $dto;
    }
}
Copied!

Data Transfer Objects 

New in version 13.1.5

DTOs provide type-safe data contracts between services.

Data Transfer Objects (DTOs) encapsulate validated, sanitized data for image rendering. They are immutable ( readonly) to ensure data integrity throughout the rendering pipeline.

ImageRenderingDto 

class ImageRenderingDto
Fully qualified name
\Netresearch\RteCKEditorImage\Domain\Model\ImageRenderingDto

Type-safe container for all image rendering data.

Properties 

ImageRenderingDto class definition
final readonly class ImageRenderingDto
{
    public function __construct(
        public string $src,              // Image source URL (validated)
        public int $width,               // Display width in pixels
        public int $height,              // Display height in pixels
        public ?string $alt,             // Alternative text for accessibility
        public ?string $title,           // Title attribute for hover tooltip
        public array $htmlAttributes,    // Additional HTML attributes
        public ?string $caption,         // Caption text (XSS-sanitized)
        public ?LinkDto $link,           // Link/popup configuration
        public bool $isMagicImage,       // Whether TYPO3 processing enabled
    ) {}
}
Copied!

Property details 

src

src
Type
string
Required

true

The processed image URL. Always validated and safe for output.

width

width
Type
int
Required

true

Display width in pixels. Used for proper aspect ratio and layout.

height

height
Type
int
Required

true

Display height in pixels. Used for proper aspect ratio and layout.

alt

alt
Type
?string

Alternative text for accessibility (screen readers, broken images).

title

title
Type
?string

Title attribute shown as tooltip on hover.

htmlAttributes

htmlAttributes
Type
array<string,mixed>
Required

true

Additional HTML attributes such as:

  • class: CSS classes.
  • style: Inline styles.
  • loading: Lazy loading setting (lazy, eager).
  • data-*: Custom data attributes.

caption

caption
Type
?string

Caption text for <figcaption>. Already sanitized with htmlspecialchars().

isMagicImage

isMagicImage
Type
bool
Required

true

Indicates whether TYPO3 image processing (magic images) was applied.

LinkDto 

class LinkDto
Fully qualified name
\Netresearch\RteCKEditorImage\Domain\Model\LinkDto

Encapsulates link/popup configuration for linked images.

Properties 

LinkDto class definition
final readonly class LinkDto
{
    public function __construct(
        public string $url,        // Link URL (validated)
        public ?string $target,    // Link target (_blank, _self, etc.)
        public ?string $class,     // CSS class for link element
        public ?string $params,    // Additional URL parameters
        public bool $isPopup,      // Whether this is a popup/lightbox link
        public ?array $jsConfig,   // JavaScript configuration for lightbox
    ) {}

    /**
     * Get URL with params properly appended.
     */
    public function getUrlWithParams(): string;
}
Copied!

Property details 

url

url
Type
string
Required

true

The link target URL. Validated against dangerous protocols.

target

target
Type
?string

Link target attribute (_blank, _self, _parent, _top).

class

class
Type
?string

CSS classes applied to the <a> element.

params

params
Type
?string

New in version 13.5.0

Additional URL parameters to append to the link URL (e.g., &L=1&type=123). These correspond to TYPO3's TypoLink additionalParams field.

The getUrlWithParams() method handles proper concatenation:

  • If URL has no query string: &L=1 becomes ?L=1
  • If URL already has query: params are appended with &
  • URL fragments (#section) are preserved at the end

isPopup

isPopup
Type
bool
Required

true

Whether the link should open in a popup/lightbox instead of navigating.

jsConfig

jsConfig
Type
?array<string,mixed>

JavaScript configuration for lightbox/popup behavior:

Example jsConfig structure
[
    'width' => 800,
    'height' => 600,
    'effect' => 'fade'
]
Copied!

Methods 

getUrlWithParams ( )

New in version 13.5.0

Returns the URL with additional parameters properly appended.

Handles query string normalization:

// URL without query string
$dto = new LinkDto(url: '/page', params: '&L=1');
$dto->getUrlWithParams(); // '/page?L=1'

// URL with existing query string
$dto = new LinkDto(url: '/page?foo=bar', params: '&L=1');
$dto->getUrlWithParams(); // '/page?foo=bar&L=1'

// URL with fragment (preserved at end)
$dto = new LinkDto(url: '/page#section', params: '&L=1');
$dto->getUrlWithParams(); // '/page?L=1#section'
Copied!

Usage example 

Creating DTOs for image rendering
use Netresearch\RteCKEditorImage\Domain\Model\ImageRenderingDto;
use Netresearch\RteCKEditorImage\Domain\Model\LinkDto;

// Create link DTO for popup
$link = new LinkDto(
    url: '/fileadmin/images/large.jpg',
    target: null,
    class: 'lightbox',
    params: null,
    isPopup: true,
    jsConfig: ['effect' => 'fade']
);

// Create link DTO for external link with parameters
$externalLink = new LinkDto(
    url: 'https://example.com/page',
    target: '_blank',
    class: 'external-link',
    params: '&utm_source=rte&utm_medium=image',
    isPopup: false,
    jsConfig: null
);
// $externalLink->getUrlWithParams() returns:
// 'https://example.com/page?utm_source=rte&utm_medium=image'

// Create image DTO
$image = new ImageRenderingDto(
    src: '/fileadmin/_processed_/image_hash.jpg',
    width: 800,
    height: 600,
    alt: 'Example image',
    title: 'Click to enlarge',
    htmlAttributes: ['class' => 'img-responsive', 'loading' => 'lazy'],
    caption: 'Photo by Photographer',
    link: $link,
    isMagicImage: true
);

// DTOs are immutable - properties cannot be changed
// $image->width = 1000; // Error: Cannot modify readonly property
Copied!

Data Handling API 

Complete API reference for data handling components including soft references and database hooks.

RteImagesDbHook 

Namespace

Netresearch\RteCKEditorImage\Database

Purpose

TCEmain hook for processing RTE content with image references during database operations

Hook Registration

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]

Service Configuration

Public service (automatically registered via ext_localconf.php)

Class Properties 

fetchExternalImages 

fetchExternalImages
Type

bool

Visibility

protected

Controls whether external image URLs should be fetched and uploaded to TYPO3.

Configuration:

Set via Extension Manager or settings.php:

$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['rte_ckeditor_image']['fetchExternalImages'] = true;
Copied!

Constructor 

__construct ( ExtensionConfiguration $extensionConfiguration, LogManager $logManager)

Initializes hook with extension configuration and logging.

param ExtensionConfiguration $extensionConfiguration

TYPO3 extension configuration service

param LogManager $logManager

Logger manager for error logging

throws ExtensionConfigurationExtensionNotConfiguredException

If extension not configured

throws ExtensionConfigurationPathDoesNotExistException

If configuration path missing

Main Hook Methods 

processDatamap_postProcessFieldArray() 

processDatamap_postProcessFieldArray ( $status, $table, $id, &$fieldArray, &$dataHandler)

Main TCEmain hook method called after field processing, before database save.

param string $status

Record status ('new' or 'update')

param string $table

Database table name

param string $id

Record ID (or 'NEW...' for new records)

param array $fieldArray

Reference to field values array

param DataHandler $dataHandler

TYPO3 DataHandler instance

Processing Flow:

  1. Iterates through all fields in $fieldArray
  2. Identifies RTE text fields via TCA configuration
  3. Checks for enableRichtext flag
  4. Processes image tags in RTE content
  5. Updates $fieldArray with processed content

Example Usage (automatic via hook):

// When content is saved:
$dataHandler->process_datamap();
// Hook is automatically called for each RTE field
Copied!

Image Processing Methods 

modifyRteField() 

modifyRteField ( $value)

Main processing method for RTE field content with images.

param string $value

RTE HTML content

returntype

string

visibility

private

Processing Logic:

1. Image Tag Splitting

$imgSplit = $rteHtmlParser->splitTags('img', $value);
// Results in: ['text', '<img...>', 'text', '<img...>', ...]
Copied!

2. URL Processing

  • Converts absolute URLs to relative
  • Handles site subpath scenarios
  • Processes data-htmlarea-file-uid references

3. FAL Integration

if (isset($attribArray['data-htmlarea-file-uid'])) {
    $originalImageFile = $resourceFactory->getFileObject($uid);
}
Copied!

4. Magic Image Processing

$imageConfiguration = [
    'width' => $imageWidth,
    'height' => $imageHeight,
];

$magicImage = $originalImageFile->process(
    ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
    $imageConfiguration
);
Copied!

5. External Image Fetching

  • Only in backend context
  • Only if fetchExternalImages is true
  • Downloads and uploads to user's default folder

6. Local File Detection

  • Checks if image is in fileadmin/
  • Attempts to find FAL reference
  • Adds data-htmlarea-file-uid if found

Scenarios Handled:

Scenario Action
Image with data-htmlarea-file-uid Load from FAL, process if dimensions differ
External URL (backend) Fetch, upload, create FAL record
External URL (frontend) Leave as-is
Local file without UID Search FAL, add UID if found
Relative URL Convert to site-relative path
Returns

Processed HTML content

Helper Methods 

getImageWidthFromAttributes() 

getImageWidthFromAttributes ( array $attributes) : int

Extracts width from image attributes, preferring style attribute.

param array $attributes

Image tag attributes

returntype

int

visibility

private

Priority:

  1. Style attribute: style="width: 800px"
  2. Width attribute: width="800"
Returns

Integer width value

getImageHeightFromAttributes() 

getImageHeightFromAttributes ( array $attributes) : int

Extracts height from image attributes, preferring style attribute.

param array $attributes

Image tag attributes

returntype

int

visibility

private

Priority:

  1. Style attribute: style="height: 600px"
  2. Height attribute: height="600"
Returns

Integer height value

extractFromAttributeValueOrStyle() 

extractFromAttributeValueOrStyle ( array $attributes, string $imageAttribute)

Generic extractor for image dimension from attributes or style.

param array $attributes

Image tag attributes array

param string $imageAttribute

Attribute name ('width' or 'height')

visibility

private

Returns

Attribute value (mixed type) or null

matchStyleAttribute() 

matchStyleAttribute ( $styleAttribute, $imageAttribute)

Extracts dimension value from CSS style attribute.

param string $styleAttribute

CSS style string.

param string $imageAttribute

Attribute name to extract.

returntype

string|null

visibility

private

Pattern: /width[[:space:]]*:[[:space:]]*([0-9]*)[[:space:]]*px/i

Example:

$style = "width: 800px; height: 600px;";
$width = $this->matchStyleAttribute($style, 'width');
// Returns: "800"
Copied!
Returns

Extracted value or null

resolveFieldConfigurationAndRespectColumnsOverrides() 

resolveFieldConfigurationAndRespectColumnsOverrides ( $dataHandler, $table, $field)

Gets TCA field configuration with type-specific overrides applied.

param DataHandler $dataHandler

Data handler instance

param string $table

Table name

param string $field

Field name

returntype

array

visibility

private

Use Case: Handles cases where field config varies by content type (e.g., different RTE configs for header vs. bodytext).

Returns

Merged TCA configuration array

RteImageSoftReferenceParser 

Namespace

Netresearch\RteCKEditorImage\DataHandling\SoftReference

Purpose

Parses soft references to FAL images in RTE content for reference tracking

Service Configuration:

Netresearch\RteCKEditorImage\DataHandling\SoftReference\RteImageSoftReferenceParser:
  public: true
  tags:
    - name: softreference.parser
      parserKey: rtehtmlarea_images
Copied!

Purpose of Soft References 

Soft references allow TYPO3 to:

  • Track where files are used
  • Prevent deletion of referenced files
  • Update references when files are moved
  • Maintain referential integrity

Parser Key 

Key

rtehtmlarea_images

TCA Registration (automatic):

// RTE fields automatically use soft reference parsing
'bodytext' => [
    'config' => [
        'type' => 'text',
        'enableRichtext' => true,
        // Soft references automatically parsed
    ]
]
Copied!

Parsing Logic 

The parser scans RTE content for:

<img data-htmlarea-file-uid="123" ... />
Copied!

And creates soft reference entries:

[
    'matchString' => '<img data-htmlarea-file-uid="123" ... />',
    'subst' => [
        'type' => 'file',
        'tokenID' => '...',
        'tokenValue' => 'file:123',
        'recordRef' => 'sys_file:123'
    ]
]
Copied!

Reference Index Integration 

Soft references populate sys_refindex table:

Field Value
tablename tt_content
recuid 123 (content element ID)
field bodytext
ref_table sys_file
ref_uid 456 (file UID)
softref_key rtehtmlarea_images

Usage Examples 

Custom Hook Extension 

If you need to extend image processing:

// EXT:my_ext/Classes/Hooks/CustomImageHook.php
namespace MyVendor\MyExt\Hooks;

class CustomImageHook
{
    public function processDatamap_postProcessFieldArray(
        string $status,
        string $table,
        string $id,
        array &$fieldArray,
        \TYPO3\CMS\Core\DataHandling\DataHandler &$dataHandler
    ): void {
        // Your custom processing
        foreach ($fieldArray as $field => &$value) {
            if ($this->isRteField($table, $field)) {
                $value = $this->customImageProcessing($value);
            }
        }
    }
}
Copied!

Register in ext_localconf.php:

$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][]
    = \MyVendor\MyExt\Hooks\CustomImageHook::class;
Copied!

Querying Soft References 

Find all content using a specific file:

use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
    ->getQueryBuilderForTable('sys_refindex');

$references = $queryBuilder
    ->select('*')
    ->from('sys_refindex')
    ->where(
        $queryBuilder->expr()->eq(
            'ref_table',
            $queryBuilder->createNamedParameter('sys_file')
        ),
        $queryBuilder->expr()->eq(
            'ref_uid',
            $queryBuilder->createNamedParameter(123, \PDO::PARAM_INT)
        ),
        $queryBuilder->expr()->eq(
            'softref_key',
            $queryBuilder->createNamedParameter('rtehtmlarea_images')
        )
    )
    ->executeQuery()
    ->fetchAllAssociative();
Copied!

Rebuilding Reference Index 

If references become out of sync:

# CLI command
./vendor/bin/typo3 referenceindex:update

# Or programmatically
use TYPO3\CMS\Core\Database\ReferenceIndex;

$referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
$referenceIndex->updateRefIndexTable('tt_content', 123);
Copied!

Magic Images Explained 

What are Magic Images? 

Magic images are TYPO3's automatic image processing system that creates optimized variants of images based on constraints.

How It Works 

  1. Original Image: Stored in FAL (e.g., 4000x3000px)
  2. Constraints: Specified in RTE (e.g., 800x600px)
  3. Processing: TYPO3 creates processed variant
  4. Storage: fileadmin/_processed_/a/b/csm_image_hash.jpg
  5. URL: Points to processed variant, not original

Configuration 

RTE.default.buttons.image.options.magic {
    maxWidth = 1920
    maxHeight = 9999
}
Copied!

Processing Context 

ProcessedFile::CONTEXT_IMAGECROPSCALEMASK
Copied!

Supported operations:

  • Crop: crop parameter
  • Scale: width, height parameters
  • Mask: Alpha channel operations

Debugging 

Enable Detailed Logging 

// LocalConfiguration.php
$GLOBALS['TYPO3_CONF_VARS']['LOG']['Netresearch']['RteCKEditorImage']['writerConfiguration'] = [
    \Psr\Log\LogLevel::DEBUG => [
        \TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
            'logFile' => 'typo3temp/var/log/rte_ckeditor_image.log'
        ]
    ]
];
Copied!

Check Processed Files 

# List processed images
ls -la fileadmin/_processed_/

# Check file processing status
./vendor/bin/typo3 cleanup:processedfiles
Copied!

Verify Soft References 

-- Check soft references for content element
SELECT * FROM sys_refindex
WHERE tablename = 'tt_content'
AND recuid = 123
AND softref_key = 'rtehtmlarea_images';
Copied!

Event Listeners API 

Complete API reference for PSR-14 event listeners in the rte_ckeditor_image extension.

PSR-14 Event System 

What is PSR-14? 

PSR-14 is a standardized event dispatcher interface that allows decoupled components to communicate through events:

  • Events: Objects containing data about what happened
  • Listeners: Callables that respond to specific events
  • Dispatcher: Routes events to registered listeners

Why Use Events Over Hooks? 

Feature PSR-14 Events Traditional Hooks
Standard Yes (PSR standard) No (TYPO3-specific)
Type Safety Strong (typed events) Weak (array parameters)
Discoverability IDE autocomplete Manual documentation
Testing Easy (mock events) Difficult (DataHandler)
Modern PHP 7.4+ features Legacy patterns

Event Flow 

Backend Form Rendering
    ↓
RteCKEditor prepares configuration
    ↓
AfterPrepareConfigurationForEditorEvent dispatched
    ↓
Custom listeners can modify configuration
    ↓
CKEditor loads with final config
Copied!

RtePreviewRendererRegistrar 

New in version 13.5.0

Namespace

Netresearch\RteCKEditorImage\Listener\TCA

Purpose

Automatically registers RteImagePreviewRenderer for all CTypes with RTE-enabled bodytext fields

Event

TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent

Without this listener, CTypes like textmedia and textpic use TYPO3's StandardContentPreviewRenderer, which strips <img> tags via strip_tags(). This listener ensures images inserted via CKEditor are visible in the page module preview for all record types.

Service Configuration:

Netresearch\RteCKEditorImage\Listener\TCA\RtePreviewRendererRegistrar:
  tags:
    - name: event.listener
      identifier: 'rte-ckeditor-image/preview-renderer-registrar'
      event: TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent
Copied!

Processing logic:

  1. Check if enableAutomaticPreviewRenderer is enabled (default: true)
  2. Parse inclusion/exclusion table lists from extension configuration
  3. Iterate all TCA tables and types
  4. For each type with RTE-enabled bodytext (via columnsOverrides or base column): - Skip if a custom previewRenderer is already set - Register RteImagePreviewRenderer

Configuration options:

The listener respects the same excludedTables and includedTablesOnly settings as RteSoftrefEnforcer. See Extension Configuration.

RteSoftrefEnforcer 

Namespace

Netresearch\RteCKEditorImage\Listener\TCA

Purpose

Automatically adds rtehtmlarea_images soft reference to all RTE-enabled text fields

Event

TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent

Without soft references, images inserted via CKEditor are not tracked in TYPO3's reference index. This means images could be lost when records are moved, copied, or deleted. The listener scans all TCA tables, removes the obsolete images softref, and adds the rtehtmlarea_images softref parser key to any type=text column with enableRichtext enabled.

Service Configuration:

Netresearch\RteCKEditorImage\Listener\TCA\RteSoftrefEnforcer:
  tags:
    - name: event.listener
      identifier: 'nr-rte-softref-enforcer'
      event: TYPO3\CMS\Core\Configuration\Event\AfterTcaCompilationEvent
      after: '*'
Copied!

Configuration options:

Controlled via enableAutomaticRteSoftref, excludedTables, and includedTablesOnly.

UpdateImageReferences 

Namespace

Netresearch\RteCKEditorImage\Listener\FileOperation

Purpose

Updates stale src attributes when FAL files are moved or renamed

Events

AfterFileMovedEvent, AfterFileRenamedEvent

When a file is moved or renamed in TYPO3's Filelist module, the src attribute stored in RTE HTML content becomes stale. The data-htmlarea-file-uid attribute still points to the correct FAL file, but the src path no longer matches. This listener detects affected records via the reference index and updates their src attributes to the file's current public URL.

Service Configuration:

Netresearch\RteCKEditorImage\Listener\FileOperation\UpdateImageReferences:
  tags:
    - name: event.listener
      identifier: 'nr-rte-update-image-references-moved'
      event: TYPO3\CMS\Core\Resource\Event\AfterFileMovedEvent
      method: handleFileMoved
    - name: event.listener
      identifier: 'nr-rte-update-image-references-renamed'
      event: TYPO3\CMS\Core\Resource\Event\AfterFileRenamedEvent
      method: handleFileRenamed
Copied!

AfterPrepareConfigurationForEditorEvent 

Event Properties 

class AfterPrepareConfigurationForEditorEvent
{
    private array $configuration;

    public function getConfiguration(): array;
    public function setConfiguration(array $configuration): void;
}
Copied!

Event Lifecycle 

Dispatch Point

After RTE configuration is prepared but before rendering

Mutability

Configuration array can be modified by listeners

Priority

Not configurable (TYPO3 dispatches in registration order)

Multiple Listeners

Supported - each listener receives modified config from previous

Configuration Injection Pattern 

What Gets Injected? 

[
    'style' => [
        'typo3image' => [
            'routeUrl' => '/typo3/rte/wizard/selectimage?token=abc123'
        ]
    ]
]
Copied!

How CKEditor Plugin Accesses It 

// In Resources/Public/JavaScript/Plugins/typo3image.js
const routeUrl = editor.config.get('style').typo3image.routeUrl;

// Used for image selection modal
Modal.advanced({
    type: Modal.types.iframe,
    content: routeUrl + '&contentsLanguage=en&bparams=...'
});
Copied!

Why This Pattern? 

  • Dynamic Routes: Backend routes include CSRF tokens that change per session
  • Environment Independence: Works across different TYPO3 installations
  • Security: CSRF tokens validated by TYPO3 backend
  • Flexibility: Easily extended for additional configuration

Usage Examples 

Accessing Route URL in JavaScript 

// CKEditor plugin initialization
export default class Typo3Image extends Core.Plugin {
    init() {
        const editor = this.editor;
        const routeUrl = editor.config.get('style').typo3image.routeUrl;

        // Use for image info API calls
        function getImageInfo(fileUid) {
            const url = routeUrl + '&action=info&fileId=' + fileUid;
            return fetch(url).then(r => r.json());
        }
    }
}
Copied!

Extending Configuration with Custom Listener 

Create your own listener to add custom configuration:

// EXT:my_ext/Classes/EventListener/CustomRteConfigListener.php
namespace MyVendor\MyExt\EventListener;

use TYPO3\CMS\RteCKEditor\Form\Element\Event\AfterPrepareConfigurationForEditorEvent;

final class CustomRteConfigListener
{
    public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
    {
        $configuration = $event->getConfiguration();

        // Add custom configuration
        $configuration['myext'] = [
            'apiEndpoint' => '/api/my-endpoint',
            'options' => ['foo' => 'bar'],
        ];

        $event->setConfiguration($configuration);
    }
}
Copied!

Register in Configuration/Services.yaml:

MyVendor\MyExt\EventListener\CustomRteConfigListener:
  tags:
    - name: event.listener
      identifier: 'custom_rte_config_listener'
      event: TYPO3\CMS\RteCKEditor\Form\Element\Event\AfterPrepareConfigurationForEditorEvent
Copied!

Access in CKEditor plugin:

const myConfig = editor.config.get('myext');
console.log(myConfig.apiEndpoint);  // '/api/my-endpoint'
Copied!

Modifying Existing Configuration 

Override typo3image Route 

public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
{
    $configuration = $event->getConfiguration();

    // Use custom route instead
    $configuration['style']['typo3image']['routeUrl'] = '/custom/image/route';

    $event->setConfiguration($configuration);
}
Copied!

Add Additional Routes 

public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
{
    $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
    $configuration = $event->getConfiguration();

    // Keep existing typo3image config
    // Add new routes for custom functionality
    $configuration['style']['typo3image']['uploadRoute'] =
        (string) $uriBuilder->buildUriFromRoute('my_custom_upload');
    $configuration['style']['typo3image']['processRoute'] =
        (string) $uriBuilder->buildUriFromRoute('my_custom_process');

    $event->setConfiguration($configuration);
}
Copied!

Listener Execution Order 

Multiple Listeners for Same Event 

When multiple listeners register for AfterPrepareConfigurationForEditorEvent:

  1. Registration Order: Listeners execute in the order they're registered
  2. Configuration Chain: Each listener receives config modified by previous listeners
  3. No Priority: TYPO3 doesn't support listener priority for this event

Example: Two Listeners 

# services.yaml
MyVendor\FirstExt\EventListener\FirstListener:
  tags:
    - name: event.listener
      event: AfterPrepareConfigurationForEditorEvent

MyVendor\SecondExt\EventListener\SecondListener:
  tags:
    - name: event.listener
      event: AfterPrepareConfigurationForEditorEvent
Copied!

Execution:

1. FirstListener receives base config
2. FirstListener modifies config (adds 'first' key)
3. SecondListener receives config with 'first' key
4. SecondListener modifies config (adds 'second' key)
5. Final config has both 'first' and 'second' keys
Copied!

Testing Event Listeners 

Unit Test Example 

use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Backend\Routing\UriBuilder;

class RteConfigurationListenerTest extends UnitTestCase
{
    /**
     * @test
     */
    public function invokeAddsRouteUrlToConfiguration(): void
    {
        // Arrange
        $event = new AfterPrepareConfigurationForEditorEvent(['existing' => 'config']);
        $listener = new RteConfigurationListener();

        // Act
        $listener->__invoke($event);

        // Assert
        $config = $event->getConfiguration();
        self::assertArrayHasKey('style', $config);
        self::assertArrayHasKey('typo3image', $config['style']);
        self::assertArrayHasKey('routeUrl', $config['style']['typo3image']);
        self::assertStringContainsString('rteckeditorimage_wizard_select_image', $config['style']['typo3image']['routeUrl']);
    }

    /**
     * @test
     */
    public function invokePreservesExistingConfiguration(): void
    {
        // Arrange
        $existingConfig = [
            'toolbar' => ['items' => ['bold', 'italic']],
            'style' => ['definitions' => []]
        ];
        $event = new AfterPrepareConfigurationForEditorEvent($existingConfig);
        $listener = new RteConfigurationListener();

        // Act
        $listener->__invoke($event);

        // Assert
        $config = $event->getConfiguration();
        self::assertArrayHasKey('toolbar', $config);
        self::assertArrayHasKey('style', $config);
        self::assertArrayHasKey('definitions', $config['style']);
        self::assertArrayHasKey('typo3image', $config['style']);
    }
}
Copied!

Debugging Event Listeners 

Check if Listener is Registered 

// Debug in TYPO3 backend
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
use TYPO3\CMS\Core\Utility\GeneralUtility;

$listenerProvider = GeneralUtility::makeInstance(ListenerProvider::class);
$listeners = $listenerProvider->getListenersForEvent(
    new AfterPrepareConfigurationForEditorEvent([])
);

// Dump listeners
var_dump($listeners);
Copied!

Log Configuration Changes 

public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
{
    $configuration = $event->getConfiguration();

    // Log before modification
    GeneralUtility::makeInstance(LogManager::class)
        ->getLogger(__CLASS__)
        ->debug('RTE config before', ['config' => $configuration]);

    // Modify configuration
    $configuration['style']['typo3image'] = [
        'routeUrl' => $this->getRouteUrl(),
    ];

    // Log after modification
    GeneralUtility::makeInstance(LogManager::class)
        ->getLogger(__CLASS__)
        ->debug('RTE config after', ['config' => $configuration]);

    $event->setConfiguration($configuration);
}
Copied!

Verify Configuration in Browser 

// In browser console after RTE loads
Object.values(CKEDITOR.instances)[0].config.get('style').typo3image;
// Should output: {routeUrl: '/typo3/rte/wizard/selectimage?...'}
Copied!

Common Issues 

Issue: routeUrl Not Available in Plugin 

Symptoms:

  • JavaScript error: "Cannot read property 'typo3image' of undefined"
  • Image selection modal doesn't open

Cause: Event listener not registered or not executing

Solution:

  1. Verify service configuration in Configuration/Services.yaml
  2. Clear system cache: ./vendor/bin/typo3 cache:flush --group=system
  3. Check event listener is loaded: grep -r "event.listener" var/cache/code/di/

Issue: Multiple Listeners Conflict 

Symptoms:

  • Configuration keys overwritten
  • Expected configuration missing

Cause: Later listener overwrites earlier listener's changes

Solution: Merge instead of replace:

// ❌ Wrong - Overwrites entire 'style' key
$configuration['style'] = ['typo3image' => [...]];

// ✅ Right - Merges with existing
$configuration['style'] = array_merge(
    $configuration['style'] ?? [],
    ['typo3image' => [...]]
);
Copied!

Issue: CSRF Token Errors 

Symptoms:

  • Backend route returns 403 errors
  • "Invalid CSRF token" in logs

Cause: Route URL built incorrectly or cached

Solution:

  • Always use UriBuilder->buildUriFromRoute() (includes token)
  • Never cache route URLs (they expire)
  • Route URL must be generated per request
// ❌ Wrong - Static URL without token
$configuration['style']['typo3image']['routeUrl'] = '/typo3/rte/wizard/selectimage';

// ✅ Right - Dynamic URL with token
$configuration['style']['typo3image']['routeUrl'] =
    (string) $uriBuilder->buildUriFromRoute('rteckeditorimage_wizard_select_image');
Copied!

Advanced Patterns 

Conditional Configuration 

public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
{
    $configuration = $event->getConfiguration();

    // Only add config for specific RTE presets
    if (($configuration['preset'] ?? '') === 'full') {
        $configuration['style']['typo3image'] = [
            'routeUrl' => $this->getRouteUrl(),
            'enableAdvancedFeatures' => true,
        ];
    }

    $event->setConfiguration($configuration);
}
Copied!

User-Specific Configuration 

use TYPO3\CMS\Core\Context\Context;

public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
{
    $context = GeneralUtility::makeInstance(Context::class);
    $configuration = $event->getConfiguration();

    // Add config based on backend user permissions
    if ($context->getPropertyFromAspect('backend.user', 'isAdmin')) {
        $configuration['style']['typo3image']['allowExternalImages'] = true;
    }

    $event->setConfiguration($configuration);
}
Copied!

Environment-Specific Configuration 

use TYPO3\CMS\Core\Core\Environment;

public function __invoke(AfterPrepareConfigurationForEditorEvent $event): void
{
    $configuration = $event->getConfiguration();

    // Development-only features
    if (Environment::getContext()->isDevelopment()) {
        $configuration['style']['typo3image']['debugMode'] = true;
        $configuration['style']['typo3image']['verboseLogging'] = true;
    }

    $event->setConfiguration($configuration);
}
Copied!

ViewHelpers 

Fluid ViewHelpers for rendering RTE image previews in backend templates.

RteImagePreviewViewHelper 

class RteImagePreviewViewHelper
Fully qualified name
\Netresearch\RteCKEditorImage\ViewHelpers\RteImagePreviewViewHelper

Renders a backend preview of RTE HTML content by stripping disallowed tags and truncating text while preserving HTML structure.

This ViewHelper replicates the preview logic from RteImagePreviewRenderer for use in Content Blocks and other custom backend preview templates where the built-in renderer is not available.

New in version 13.7.0

Arguments 

html

html
Type
string
Required

true

The RTE HTML content to preview. Typically {data.bodytext} in a Content Blocks backend preview template.

maxLength

maxLength
Type
int
Default
1500

Maximum number of text characters before truncation. When exceeded, the text is truncated with an ellipsis (...). HTML tags do not count toward this limit.

allowedTags

allowedTags
Type
string
Default
<img><p>

HTML tags to preserve in the preview output, in strip_tags() format. All other tags are stripped (their text content is kept).

Processing Pipeline 

The ViewHelper processes HTML through three stages:

  1. Sanitization — Removes control characters (\x00-\x1F), UTF-16 surrogates, and Unicode non-characters, replacing them with U+FFFD (replacement character).
  2. Tag stripping — Calls strip_tags() with the allowedTags argument, keeping only <img> and <p> by default.
  3. DOM-aware truncation — Parses the remaining HTML with DOMDocument, walks the DOM tree counting text length, and truncates at maxLength while keeping all tags properly closed.

Usage with Content Blocks 

Content Blocks is the official TYPO3-endorsed successor to Mask/DCE/Flux for creating custom content element types. Content Blocks registers its own backend preview templates (backend-preview.html), which do not use the built-in RteImagePreviewRenderer.

To render RTE image previews in a Content Block, use this ViewHelper in the block's backend preview template:

ContentBlocks/ContentElements/my-block/templates/backend-preview.html
<html xmlns:nr="http://typo3.org/ns/Netresearch/RteCKEditorImage/ViewHelpers"
      data-namespace-typo3-fluid="true">
<nr:rteImagePreview html="{data.bodytext}" />
</html>
Copied!

Custom Tag Allowlist 

To also preserve <figure> and <figcaption> in the preview:

<nr:rteImagePreview html="{data.bodytext}"
                    allowedTags="<img><p><figure><figcaption>" />
Copied!

Custom Truncation Length 

To show a shorter preview (e.g., in a compact list view):

<nr:rteImagePreview html="{data.bodytext}" maxLength="300" />
Copied!

Standard Content Elements 

For standard tt_content types with RTE bodytext, you do not need this ViewHelper. The built-in RteImagePreviewRenderer handles backend previews automatically via TYPO3's PreviewRendererInterface.

CKEditor Plugin Development 

Complete documentation for the CKEditor 5 plugin implementation.

The typo3image plugin is a custom CKEditor 5 plugin that integrates TYPO3's File Abstraction Layer (FAL) with the rich text editor, enabling seamless image management within the CKEditor interface.

Plugin Components 

🔌 Plugin Development 

Plugin architecture, UI components, commands, and event handling

📐 Model Element 

The typo3image custom element schema, attributes, and model integration

🎨 Style Integration 

Style system integration with StyleUtils and GeneralHtmlSupport (critical for v13.0.0+)

↔️ Conversions 

HTML ↔ Model conversion patterns for upcast and downcast transformations

🎚️ Image Quality Selector 

Quality multipliers, SVG support, and dimension handling

Critical Information 

Version 13.0.0+ Requirements 

The plugin requires these CKEditor dependencies:

static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];
}
Copied!

CKEditor Plugin Development 

Complete guide to the Typo3Image CKEditor 5 plugin architecture and development patterns.

Plugin Overview 

File: Resources/Public/JavaScript/Plugins/typo3image.js

Plugin Class: Typo3Image extends Core.Plugin

Required Dependencies:

static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];
}
Copied!

Plugin Structure 

export default class Typo3Image extends Core.Plugin {
    static pluginName = 'Typo3Image';

    static get requires() {
        return ['StyleUtils', 'GeneralHtmlSupport'];
    }

    init() {
        // Plugin initialization
        // - Define schema
        // - Register conversions
        // - Add UI components
        // - Register event listeners
    }
}
Copied!

Custom Model Element: typo3image 

Schema Definition 

editor.model.schema.register('typo3image', {
    inheritAllFrom: '$blockObject',
    allowIn: ['$text', '$block'],
    allowAttributes: [
        'src', 'fileUid', 'fileTable',
        'alt', 'altOverride', 'title', 'titleOverride',
        'class', 'enableZoom', 'width', 'height',
        'htmlA', 'linkHref', 'linkTarget', 'linkTitle'
    ],
});
Copied!

Attribute Descriptions 

Attribute Type Description
src string Image source URL
fileUid number TYPO3 FAL file UID
fileTable string Database table (default: 'sys_file')
alt string Alternative text
altOverride boolean Alt text override flag
title string Advisory title
titleOverride boolean Title override flag
class string CSS classes (space-separated)
enableZoom boolean Zoom/clickenlarge functionality
width string Image width
height string Image height
htmlA string Link wrapper HTML
linkHref string Link URL
linkTarget string Link target
linkTitle string Link title

Conversion System 

Upcast: HTML → Model 

Converts <img> elements with FAL attributes to typo3image model elements:

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,
        });
    }
});
Copied!

Downcast: Model → HTML 

Converts typo3image model elements to <img> HTML:

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);
    },
});
Copied!

Class Attribute Converter 

Makes class changes immediately visible in the editor:

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

UI Components 

Insert Image Button 

Registered in editor.ui.componentFactory:

editor.ui.componentFactory.add('insertimage', () => {
    const button = new UI.ButtonView();

    button.set({
        label: 'Insert image',
        icon: '<svg>...</svg>',
        tooltip: true,
        withText: false,
    });

    button.on('execute', () => {
        const selectedElement = editor.model.document.selection.getSelectedElement();

        if (selectedElement && selectedElement.name === 'typo3image') {
            // Edit existing image
            edit(selectedElement, editor, attributes);
        } else {
            // Insert new image
            selectImage(editor).then(selectedImage => {
                edit(selectedImage, editor, {});
            });
        }
    });

    return button;
});
Copied!

Image Selection Flow 

selectImage() Function 

Opens TYPO3 Modal with file browser:

function selectImage(editor) {
    let resolvePromise;
    const promise = new Promise((resolve) => { resolvePromise = resolve; });

    const bparams = ['', '', '', ''];
    const contentUrl = editor.config.get('style').typo3image.routeUrl
        + '&contentsLanguage=en&editorId=123&bparams=' + bparams.join('|');

    const modal = Modal.advanced({
        type: Modal.types.iframe,
        title: 'Select Image',
        content: contentUrl,
        size: Modal.sizes.large,
        callback: function (currentModal) {
            const iframe = currentModal.querySelector('iframe');
            iframe.addEventListener('load', () => {
                const doc = iframe.contentDocument;
                doc.addEventListener('click', (e) => {
                    const el = e.target.closest('[data-filelist-element]');
                    if (!el || el.dataset.filelistType !== 'file') return;

                    const selectedItem = {
                        uid: el.dataset.filelistUid,
                        table: 'sys_file',
                    };
                    currentModal.hideModal();
                    resolvePromise(selectedItem);
                });
            });
        }
    });

    return promise;
}
Copied!

Image Properties Dialog 

getImageDialog() Function 

Creates image properties form:

function getImageDialog(editor, img, attributes) {
    const d = {};
    const fields = [
        {
            width: { label: 'Width', type: 'number' },
            height: { label: 'Height', type: 'number' }
        },
        {
            title: { label: 'Advisory Title', type: 'text' },
            alt: { label: 'Alternative Text', type: 'text' }
        }
    ];

    // Create form elements using native DOM
    const container = document.createElement('div');
    container.className = 'rteckeditorimage';
    d.el = container;

    // ... form generation code ...

    // Aspect ratio preservation for width/height
    el.addEventListener('input', () => {
        const ratio = img.width / img.height;
        const newHeight = Math.ceil(newWidth / ratio);
        oppositeEl.value = newHeight;
    });

    // Override checkboxes for title/alt
    cboxLabel.addEventListener('click', () => {
        el.disabled = !cbox.checked;
        if (!cbox.checked) {
            el.value = '';  // Clear custom value
        }
    });

    d.get = function () {
        // Returns filtered attributes for allowed list
        return filteredAttributes;
    };

    return d;
}
Copied!

Dialog Features 

  • Width/Height: Number inputs with aspect ratio preservation
  • Title/Alt: Text inputs with override checkboxes
  • Zoom: Checkbox for clickenlarge functionality
  • CSS Class: Text input for custom classes

Style System Integration 

Critical for CKEditor style drop-down functionality.

Event Listener: isStyleEnabledForBlock 

Enables img styles when typo3image is selected:

this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = true;
            }
        }
    }
});
Copied!

Event Listener: isStyleActiveForBlock 

Checks if style is currently applied:

this.listenTo(styleUtils, 'isStyleActiveForBlock', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                const classAttribute = item.getAttribute('class');
                if (classAttribute && typeof classAttribute === 'string') {
                    const classlist = classAttribute.split(' ');
                    if (style.classes.filter(value => !classlist.includes(value)).length === 0) {
                        event.return = true;
                    }
                }
            }
        }
    }
});
Copied!

Event Listener: getAffectedBlocks 

Returns correct model element for style operations:

this.listenTo(styleUtils, 'getAffectedBlocks', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = [item];
                break;
            }
        }
    }
});
Copied!

GeneralHtmlSupport Integration 

Manages class attribute updates from style system.

addModelHtmlClass Listener 

const ghs = editor.plugins.get('GeneralHtmlSupport');
ghs.decorate('addModelHtmlClass');

this.listenTo(ghs, 'addModelHtmlClass', (event, [viewElement, className, selectable]) => {
    if (selectable && selectable.name === 'typo3image') {
        editor.model.change(writer => {
            writer.setAttribute('class', className.join(' '), selectable);
        });
    }
});
Copied!

removeModelHtmlClass Listener 

ghs.decorate('removeModelHtmlClass');

this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
    if (selectable && selectable.name === 'typo3image') {
        editor.model.change(writer => {
            writer.removeAttribute('class', selectable);
        });
    }
});
Copied!

Event Observers 

DoubleClickObserver 

Custom observer for double-click detection:

class DoubleClickObserver extends Engine.DomEventObserver {
    constructor(view) {
        super(view);
        this.domEventType = 'dblclick';
    }

    onDomEvent(domEvent) {
        this.fire(domEvent.type, domEvent);
    }
}

// Register observer
editor.editing.view.addObserver(DoubleClickObserver);

// Listen for double-click
editor.listenTo(editor.editing.view.document, 'dblclick', (event, data) => {
    const modelElement = editor.editing.mapper.toModelElement(data.target);
    if (modelElement && modelElement.name === 'typo3image') {
        // Open edit dialog
        edit({...}, editor, {...});
    }
});
Copied!

Click Handler 

Single-click selects image:

editor.listenTo(editor.editing.view.document, 'click', (event, data) => {
    const modelElement = editor.editing.mapper.toModelElement(data.target);
    if (modelElement && modelElement.name === 'typo3image') {
        editor.model.change(writer => {
            writer.setSelection(modelElement, 'on');
        });
    }
});
Copied!

Backend API Integration 

getImageInfo() Function 

Fetches image data from backend:

function getImageInfo(editor, table, uid, params) {
    let url = editor.config.get('style').typo3image.routeUrl
        + '&action=info&fileId=' + encodeURIComponent(uid)
        + '&table=' + encodeURIComponent(table)
        + '&contentsLanguage=en&editorId=123';

    if (params.width) {
        url += '&P[width]=' + params.width;
    }
    if (params.height) {
        url += '&P[height]=' + params.height;
    }

    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
}
Copied!

Plugin Configuration 

Registration (Configuration/RTE/Plugin.yaml) 

editor:
  config:
    importModules:
      - '@netresearch/rte-ckeditor-image/Plugins/typo3image.js'

  externalPlugins:
    typo3image: { route: "rteckeditorimage_wizard_select_image" }

processing:
  allowTagsOutside:
    - img
Copied!

JavaScript Module Registration 

// Configuration/JavaScriptModules.php
return [
    'dependencies' => ['rte_ckeditor'],
    'tags' => ['backend.form'],
    'imports' => [
        '@netresearch/rte-ckeditor-image/' => 'EXT:rte_ckeditor_image/Resources/Public/JavaScript/',
    ],
];
Copied!

Development Tips 

  1. Always test style integration - Verify StyleUtils and GeneralHtmlSupport work correctly
  2. Use browser console - Monitor CKEditor model changes with editor.model.document.on('change')
  3. Check conversions - Verify upcast/downcast produce expected results
  4. Test attribute updates - Ensure class and other attributes update correctly
  5. Debug with breakpoints - Use browser DevTools to step through plugin code

CKEditor Model Element Reference 

Complete reference for the typo3image custom model element in CKEditor 5.

Overview 

The typo3image is a custom model element that represents TYPO3 FAL-integrated images in the CKEditor document model. It extends CKEditor's base $blockObject and includes TYPO3-specific attributes for FAL integration, image processing, and metadata management.

File: Resources/Public/JavaScript/Plugins/typo3image.js

Model vs View Architecture 

Understanding CKEditor 5 Architecture 

CKEditor 5 uses a Model-View-Controller (MVC) architecture:

┌─────────────────────────────────────────────────┐
│ Model Layer (Data)                              │
│ - Abstract representation of document           │
│ - Business logic and validation                 │
│ - typo3image element with attributes            │
└──────────────────┬──────────────────────────────┘
                   │
                   │ Conversions
                   │
┌──────────────────▼──────────────────────────────┐
│ View Layer (DOM)                                │
│ - Visual representation in editor               │
│ - <img> elements with HTML attributes           │
│ - User sees and interacts with                  │
└──────────────────┬──────────────────────────────┘
                   │
                   │ Rendering
                   │
┌──────────────────▼──────────────────────────────┐
│ DOM (Browser)                                   │
│ - Actual HTML in contenteditable               │
│ - <img src="..." data-htmlarea-file-uid="123"/> │
└─────────────────────────────────────────────────┘
Copied!

Why Separate Model and View? 

  • Data Integrity: Model maintains clean data structure regardless of DOM quirks
  • Cross-Platform: Same model can render differently on different platforms
  • Collaboration: Multiple users can edit same model with conflict resolution
  • Undo/Redo: Model changes tracked for history management
  • Validation: Business rules enforced in model layer

Model Elements 

The extension provides two model elements for different use cases:

  • typo3image: Block-level images wrapped in <figure> (with optional caption)
  • typo3imageInline: True inline images that flow with text

Block Image Schema (typo3image) 

editor.model.schema.register('typo3image', {
    inheritAllFrom: '$blockObject',
    allowIn: ['$text', '$block'],
    allowAttributes: [
        'src', 'fileUid', 'fileTable',
        'alt', 'altOverride', 'title', 'titleOverride',
        'class', 'enableZoom', 'width', 'height',
        'htmlA', 'imageLinkHref', 'imageLinkTarget', 'imageLinkTitle',
        'imageLinkClass', 'imageLinkParams'
    ],
});
Copied!

Inline Image Schema (typo3imageInline) 

New in version 13.6

True inline images with cursor positioning support.

editor.model.schema.register('typo3imageInline', {
    inheritAllFrom: '$inlineObject',
    allowIn: ['$block'],
    allowAttributes: [
        'src', 'fileUid', 'fileTable',
        'alt', 'altOverride', 'title', 'titleOverride',
        'class', 'enableZoom', 'width', 'height', 'noScale', 'quality',
        'imageLinkHref', 'imageLinkTarget', 'imageLinkTitle',
        'imageLinkClass', 'imageLinkParams'
    ],
});
Copied!

Key Differences 

Feature typo3image (Block) typo3imageInline (Inline)
Inheritance $blockObject $inlineObject
Caption Support ✅ Yes (typo3imageCaption child) ❌ No
Text Flow Breaks paragraph (block-level) Flows with text (inline)
Cursor Position Before/after figure Before/after on same line
Output HTML <figure><img></figure> <img class="image-inline">
Style Classes image-left, image-right, image-center, image-block image-inline

Usage Example 

In the editor, users can type text before and after inline images on the same line, just like typing around any other inline element (bold text, links, etc.).

Toggle Command 

Users can convert between block and inline via the toggleImageType command:

// Toggle current image between block and inline
editor.execute('toggleImageType');

// Check current type
const isInline = editor.commands.get('toggleImageType').value === 'inline';
Copied!

Block → Inline Conversion:

  • Caption is removed (inline images cannot have captions)
  • Block style classes removed, image-inline added
  • Image becomes inline in text flow

Inline → Block Conversion:

  • Image wrapped in figure
  • image-block class added (or previous alignment class)
  • Image becomes block-level element

Schema Properties Explained 

inheritAllFrom: '$blockObject' 

Inherits all properties from CKEditor's base $blockObject:

  • Selectable: Can be selected like any block element
  • Object: Treated as atomic unit (not text content)
  • Focusable: Can receive focus for editing
  • Non-Breaking: Cannot be split by Enter key

allowIn: ['$text', '$block'] 

Defines where typo3image can exist:

  • $text: Inside text content (inline-like behavior)
  • $block: Inside block elements (paragraphs, divs, etc.)

Result: Images can be placed in any text flow or block context.

allowAttributes: [...] 

Lists all valid attributes for the model element. Attributes not listed are stripped.

Attribute Reference 

Core Attributes 

src 

type: String
required: true
Copied!

Purpose: Image source URL (absolute or relative)

Examples:

src: '/fileadmin/user_upload/image.jpg'
src: '/fileadmin/_processed_/a/b/csm_image_123.jpg'
src: 'https://example.com/external.jpg'
Copied!

Usage:

const src = modelElement.getAttribute('src');
writer.setAttribute('src', '/new/path.jpg', modelElement);
Copied!

fileUid 

type: Number
required: true (for TYPO3 FAL integration)
Copied!

Purpose: TYPO3 File Abstraction Layer file UID

Range: Positive integer matching sys_file.uid

Example:

fileUid: 123
Copied!

Usage:

const fileUid = modelElement.getAttribute('fileUid');
writer.setAttribute('fileUid', 456, modelElement);
Copied!

Backend Integration:

// Fetch file info from backend
const fileUid = modelElement.getAttribute('fileUid');
const fileInfo = await fetch(
    routeUrl + '&action=info&fileId=' + fileUid
).then(r => r.json());
Copied!

fileTable 

type: String
default: 'sys_file'
Copied!

Purpose: Database table name for file reference

Valid Values: 'sys_file' (default), 'sys_file_reference' (rarely used)

Example:

fileTable: 'sys_file'
Copied!

Usage:

const table = modelElement.getAttribute('fileTable') || 'sys_file';
Copied!

Metadata Attributes 

alt 

type: String
default: ''
Copied!

Purpose: Alternative text for accessibility (WCAG compliance)

Recommendations:

  • Describe image content concisely
  • Required for accessibility
  • Max  125 characters for optimal screen reader experience

Example:

alt: 'Product photo showing red widget from front angle'
Copied!

Usage:

writer.setAttribute('alt', 'New alt text', modelElement);
Copied!

altOverride 

type: Boolean
default: false
Copied!

Purpose: Flag indicating alt text was manually overridden by user

Behavior:

  • false: Use alt from FAL file metadata
  • true: Use custom alt text from alt attribute

Example:

altOverride: true  // Custom alt text takes precedence
Copied!

Usage Pattern:

// In image dialog
if (customAltCheckbox.checked) {
    writer.setAttribute('alt', customAltValue, modelElement);
    writer.setAttribute('altOverride', true, modelElement);
} else {
    writer.removeAttribute('alt', modelElement);
    writer.removeAttribute('altOverride', modelElement);
}
Copied!

title 

type: String
default: ''
Copied!

Purpose: Advisory title (tooltip text shown on hover)

Recommendations:

  • Optional supplementary information
  • Not a replacement for alt text
  • Brief contextual information

Example:

title: 'Click to view full size'
Copied!

Usage:

writer.setAttribute('title', 'Tooltip text', modelElement);
Copied!

titleOverride 

type: Boolean
default: false
Copied!

Purpose: Flag indicating title was manually overridden by user

Behavior:

  • false: Use title from FAL file metadata
  • true: Use custom title from title attribute

Example:

titleOverride: true
Copied!

Visual Attributes 

class 

type: String
default: ''
Copied!

Purpose: Space-separated CSS class names for styling

Style Integration: Modified by CKEditor style system via GeneralHtmlSupport

Examples:

class: 'float-left img-responsive'
class: 'img-thumbnail d-block mx-auto'
Copied!

Usage:

// Manual class setting
writer.setAttribute('class', 'my-class another-class', modelElement);

// Style system automatically updates this attribute
// when user selects a style from dropdown
Copied!

Style System Integration:

// CKEditor style definition
{
    name: 'Image Left',
    element: 'img',
    classes: ['float-left', 'mr-3']
}

// Results in:
class: 'float-left mr-3'
Copied!

width 

type: String (pixels without unit)
default: ''
Copied!

Purpose: Image display width in pixels

Format: Numeric string without 'px' unit

Examples:

width: '800'
width: '1200'
Copied!

Usage:

writer.setAttribute('width', '800', modelElement);
Copied!

Aspect Ratio Preservation:

// When width changes, height should be recalculated
const newWidth = 800;
const originalWidth = img.width;
const originalHeight = img.height;
const ratio = originalWidth / originalHeight;
const newHeight = Math.ceil(newWidth / ratio);

writer.setAttribute('width', String(newWidth), modelElement);
writer.setAttribute('height', String(newHeight), modelElement);
Copied!

height 

type: String (pixels without unit)
default: ''
Copied!

Purpose: Image display height in pixels

Format: Numeric string without 'px' unit

Examples:

height: '600'
height: '900'
Copied!

Usage:

writer.setAttribute('height', '600', modelElement);
Copied!

enableZoom 

type: Boolean
default: false
Copied!

Purpose: Enable zoom/click-to-enlarge functionality (TYPO3-specific feature)

Behavior:

  • true: Image becomes clickable, opens larger version
  • false: Image is static, no click interaction

Example:

enableZoom: true
Copied!

Frontend Rendering:

<!-- When enableZoom is true -->
<a href="large-image.jpg" data-lightbox="gallery">
    <img src="thumb.jpg" data-htmlarea-zoom="true" />
</a>
Copied!

Usage:

writer.setAttribute('enableZoom', true, modelElement);
Copied!

Working with Model Elements 

Creating Model Elements 

editor.model.change(writer => {
    const typo3image = writer.createElement('typo3image', {
        src: '/fileadmin/image.jpg',
        fileUid: 123,
        fileTable: 'sys_file',
        width: '800',
        height: '600',
        alt: 'Description',
        class: 'img-fluid'
    });

    // Insert at current selection
    const insertPosition = editor.model.document.selection.getFirstPosition();
    editor.model.insertContent(typo3image, insertPosition);
});
Copied!

Updating Attributes 

editor.model.change(writer => {
    const selectedElement = editor.model.document.selection.getSelectedElement();

    if (selectedElement && selectedElement.name === 'typo3image') {
        // Update single attribute
        writer.setAttribute('width', '1200', selectedElement);

        // Update multiple attributes
        writer.setAttributes({
            width: '1200',
            height: '800',
            class: 'img-large'
        }, selectedElement);
    }
});
Copied!

Reading Attributes 

const selectedElement = editor.model.document.selection.getSelectedElement();

if (selectedElement && selectedElement.name === 'typo3image') {
    // Read single attribute
    const src = selectedElement.getAttribute('src');
    const fileUid = selectedElement.getAttribute('fileUid');

    // Read with default fallback
    const alt = selectedElement.getAttribute('alt') || '';
    const width = selectedElement.getAttribute('width') || '0';

    // Check if attribute exists
    const hasClass = selectedElement.hasAttribute('class');

    // Get all attributes
    const allAttrs = Array.from(selectedElement.getAttributes());
    console.log(allAttrs);  // [['src', '...'], ['fileUid', 123], ...]
}
Copied!

Removing Attributes 

editor.model.change(writer => {
    const selectedElement = editor.model.document.selection.getSelectedElement();

    if (selectedElement && selectedElement.name === 'typo3image') {
        // Remove single attribute
        writer.removeAttribute('class', selectedElement);

        // Remove multiple attributes
        writer.removeAttribute('title', selectedElement);
        writer.removeAttribute('titleOverride', selectedElement);
    }
});
Copied!

Model Selection 

Selecting Elements 

// Select element programmatically
editor.model.change(writer => {
    const element = /* get element reference */;
    writer.setSelection(element, 'on');  // 'on' = select element itself
});
Copied!

Getting Selected Element 

const selection = editor.model.document.selection;
const selectedElement = selection.getSelectedElement();

if (selectedElement && selectedElement.name === 'typo3image') {
    // Image is selected
    console.log('Selected typo3image element');
}
Copied!

Iterating Selection Range 

const selection = editor.model.document.selection;
const range = selection.getFirstRange();

for (const item of range.getItems()) {
    if (item.is('element', 'typo3image')) {
        console.log('Found typo3image:', item.getAttribute('src'));
    }
}
Copied!

Model Traversal 

Finding Parent Elements 

const element = /* typo3image element */;
const parent = element.parent;

console.log(parent.name);  // e.g., 'paragraph', '$root'
Copied!

Finding Previous/Next Siblings 

const element = /* typo3image element */;
const previousSibling = element.previousSibling;
const nextSibling = element.nextSibling;
Copied!

Walking the Model Tree 

function findAllImages(root) {
    const images = [];
    const walker = editor.model.createRangeIn(root).getWalker();

    for (const {item} of walker) {
        if (item.is('element', 'typo3image')) {
            images.push(item);
        }
    }

    return images;
}

// Find all images in document
const allImages = findAllImages(editor.model.document.getRoot());
Copied!

Attribute Validation 

Allowed Attributes Enforcement 

CKEditor automatically strips attributes not in allowAttributes list:

// This attribute will be stripped
writer.setAttribute('invalidAttr', 'value', modelElement);

// Only these attributes are preserved
allowAttributes: [
    'src', 'fileUid', 'fileTable',
    'alt', 'altOverride', 'title', 'titleOverride',
    'class', 'enableZoom', 'width', 'height',
    'htmlA', 'linkHref', 'linkTarget', 'linkTitle',
    'linkClass', 'linkParams'
]
Copied!

Custom Validation 

editor.model.schema.addAttributeCheck((context, attributeName) => {
    // Only allow width/height with valid numeric values
    if (attributeName === 'width' || attributeName === 'height') {
        if (context.endsWith('typo3image')) {
            const value = context.getAttribute(attributeName);
            return /^\d+$/.test(value);  // Must be numeric
        }
    }
    return true;
});
Copied!

Model Change Listeners 

Listening to Attribute Changes 

editor.model.document.on('change:data', () => {
    const changes = editor.model.document.differ.getChanges();

    for (const change of changes) {
        if (change.type === 'attribute' && change.attributeKey === 'class') {
            console.log('Class changed:', {
                element: change.range.start.parent.name,
                oldValue: change.attributeOldValue,
                newValue: change.attributeNewValue
            });
        }
    }
});
Copied!

Listening to Element Insertion 

editor.model.document.on('change:data', () => {
    const changes = editor.model.document.differ.getChanges();

    for (const change of changes) {
        if (change.type === 'insert' && change.name === 'typo3image') {
            console.log('typo3image inserted:', change.position.path);
        }
    }
});
Copied!

Advanced Patterns 

Cloning Elements 

editor.model.change(writer => {
    const original = /* get typo3image element */;

    // Clone with all attributes
    const clone = writer.cloneElement(original);

    // Insert clone
    const insertPosition = /* target position */;
    writer.insert(clone, insertPosition);
});
Copied!

Batch Attribute Updates 

editor.model.change(writer => {
    const images = /* array of typo3image elements */;

    // Apply same class to all images
    images.forEach(img => {
        const currentClass = img.getAttribute('class') || '';
        const newClass = currentClass + ' batch-processed';
        writer.setAttribute('class', newClass.trim(), img);
    });
});
Copied!

Conditional Attribute Setting 

editor.model.change(writer => {
    const element = /* typo3image element */;

    // Only set width if not already set
    if (!element.hasAttribute('width')) {
        writer.setAttribute('width', '800', element);
    }

    // Update alt only if override is enabled
    if (element.getAttribute('altOverride')) {
        writer.setAttribute('alt', customAltText, element);
    }
});
Copied!

Debugging Model Elements 

Inspect Element in Console 

// Get selected element
const element = editor.model.document.selection.getSelectedElement();

// Log all attributes
console.log('Element:', element.name);
console.log('Attributes:', Array.from(element.getAttributes()));

// Log specific attributes
console.log('src:', element.getAttribute('src'));
console.log('fileUid:', element.getAttribute('fileUid'));
console.log('class:', element.getAttribute('class'));
Copied!

Monitor Model Changes 

editor.model.document.on('change', (evt, batch) => {
    console.log('Model changed, batch type:', batch.type);
    console.log('Is undoable:', batch.isUndoable);

    const changes = editor.model.document.differ.getChanges();
    console.log('Changes:', changes);
});
Copied!

Visualize Model Structure 

function logModelTree(element, indent = 0) {
    const prefix = ' '.repeat(indent);
    if (element.is('$text')) {
        console.log(prefix + 'TEXT:', element.data);
    } else {
        console.log(prefix + element.name);
        for (const child of element.getChildren()) {
            logModelTree(child, indent + 2);
        }
    }
}

// Log entire document structure
logModelTree(editor.model.document.getRoot());
Copied!

CKEditor Style Integration 

Complete guide to integrating the typo3image plugin with CKEditor's style system (StyleUtils and GeneralHtmlSupport).

Overview 

New in version 13.0.0

Integration with GeneralHtmlSupport is now required for style functionality. Previous versions only required StyleUtils, which caused the style dropdown to be disabled for images.

Critical Dependencies:

static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];
}
Copied!

The Style System Problem 

Before v13.0.0 (Broken) 

// Missing GeneralHtmlSupport dependency
static get requires() {
    return ['StyleUtils'];  // Incomplete!
}
Copied!

Issue: Style drop-down disabled when image selected

Symptoms:

  • Styles grayed out when typo3image selected
  • Class changes not applied to images
  • No visual feedback when applying styles

After v13.0.0 (Fixed) 

// Both dependencies required
static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];
}
Copied!

Result: Full style system integration working correctly

Style System Architecture 

Three-Layer Integration 

┌─────────────────────────────────────────┐
│ StyleUtils Plugin                       │
│ - Manages style definitions             │
│ - Provides event system                 │
│ - Determines style applicability        │
└───────────┬─────────────────────────────┘
            │
            │ Events
            │
┌───────────▼─────────────────────────────┐
│ Typo3Image Plugin                       │
│ - Listens to StyleUtils events         │
│ - Reports typo3image availability       │
│ - Returns correct model elements        │
└───────────┬─────────────────────────────┘
            │
            │ Operations
            │
┌───────────▼─────────────────────────────┐
│ GeneralHtmlSupport Plugin               │
│ - Applies class changes to model        │
│ - Manages HTML attribute manipulation   │
│ - Ensures class sync with view          │
└─────────────────────────────────────────┘
Copied!

StyleUtils Event System 

Event: isStyleEnabledForBlock 

Purpose: Determines if a style can be applied to the selected element

When Fired: User selects element, style drop-down needs update

Default Behavior: Only enable styles for elements matching style definition

typo3image Override:

this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = true;  // Enable img styles for typo3image
            }
        }
    }
});
Copied!

Logic Breakdown:

  1. Check Style Element: if (style.element === 'img')

    • Only process styles defined for <img> elements
    • Ignore styles for other elements (p, h1, etc.)
  2. Iterate Selection: for (const item of ...getFirstRange().getItems())

    • Get all items in current selection range
    • Check if any item is a typo3image
  3. Enable Style: event.return = true

    • Tell StyleUtils that img styles ARE applicable to typo3image
    • Without this, style drop-down would be disabled

Event: isStyleActiveForBlock 

Purpose: Checks if a style is currently active (applied) on selected element

When Fired: User selects element, style drop-down shows active styles

Default Behavior: Check if element has required classes

typo3image Implementation:

this.listenTo(styleUtils, 'isStyleActiveForBlock', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                const classAttribute = item.getAttribute('class');
                if (classAttribute && typeof classAttribute === 'string') {
                    const classlist = classAttribute.split(' ');
                    // Check if ALL style classes are present
                    if (style.classes.filter(value => !classlist.includes(value)).length === 0) {
                        event.return = true;  // Style is active
                    }
                }
            }
        }
    }
});
Copied!

Logic Breakdown:

  1. Check Style Element: Only process img styles
  2. Find typo3image: Iterate selection to find typo3image element
  3. Get Classes: const classAttribute = item.getAttribute('class')

    • Read current class attribute from model element
    • Returns space-separated string (e.g., "float-left img-responsive")
  4. Parse Classes: const classlist = classAttribute.split(' ')

    • Convert string to array: ["float-left", "img-responsive"]
  5. Check Match: style.classes.filter(value => !classlist.includes(value)).length === 0

    • Check if ALL style classes are present in element
    • Example: Style has ['float-left', 'mr-3'], check both exist
    • If any missing, style is NOT active

Example:

// Style definition
{
    name: 'Image Left',
    element: 'img',
    classes: ['float-left', 'mr-3']
}

// Element class attribute
class: 'float-left mr-3 img-responsive'

// Check: Are 'float-left' AND 'mr-3' both present?
['float-left', 'mr-3'].filter(cls =>
    !['float-left', 'mr-3', 'img-responsive'].includes(cls)
).length === 0  // true → style is active
Copied!

Event: getAffectedBlocks 

Purpose: Returns which model elements should be affected by style operation

When Fired: User applies/removes a style

Default Behavior: Return block elements from selection

typo3image Implementation:

this.listenTo(styleUtils, 'getAffectedBlocks', (event, [style, element]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = [item];  // Return typo3image element
                break;
            }
        }
    }
});
Copied!

Logic Breakdown:

  1. Check Style Element: Only process img styles
  2. Find typo3image: Iterate to find typo3image in selection
  3. Return Element: event.return = [item]

    • Return array with single typo3image element
    • StyleUtils will apply style changes to this element
  4. Break Loop: Once found, stop searching

GeneralHtmlSupport Integration 

What is GeneralHtmlSupport? 

Purpose: Manages HTML attributes that aren't core CKEditor features

Capabilities:

  • Add/remove classes via style system
  • Manage data-* attributes
  • Handle custom HTML attributes
  • Sync model attributes with view

Decoration Pattern 

const ghs = editor.plugins.get('GeneralHtmlSupport');
ghs.decorate('addModelHtmlClass');
ghs.decorate('removeModelHtmlClass');
Copied!

What decorate() Does:

  • Makes method observable via event system
  • Allows plugins to intercept and customize behavior
  • Enables event listeners to modify operations

Event: addModelHtmlClass 

Purpose: Add CSS class to model element

When Fired: Style system applies a style (adds classes)

typo3image Implementation:

this.listenTo(ghs, 'addModelHtmlClass', (event, [viewElement, className, selectable]) => {
    if (selectable && selectable.name === 'typo3image') {
        editor.model.change(writer => {
            writer.setAttribute('class', className.join(' '), selectable);
        });
    }
});
Copied!

Parameters:

  • viewElement: View layer element (not used for typo3image)
  • className: Array of class names to add
  • selectable: Model element to modify

Logic:

  1. Check Element: if (selectable && selectable.name === 'typo3image')

    • Only process typo3image elements
  2. Join Classes: className.join(' ')

    • Convert array to space-separated string
    • Example: ['float-left', 'mr-3']'float-left mr-3'
  3. Update Model: writer.setAttribute('class', ..., selectable)

    • Apply classes to model element
    • Triggers view update automatically

Example Flow:

User clicks "Image Left" style
    ↓
StyleUtils determines style applies to typo3image
    ↓
GeneralHtmlSupport.addModelHtmlClass fired
    ↓
Event handler: className = ['float-left', 'mr-3']
    ↓
Model updated: class = 'float-left mr-3'
    ↓
View automatically updates: <img class="float-left mr-3" ... />
Copied!

Event: removeModelHtmlClass 

Purpose: Remove CSS class from model element

When Fired: Style system removes a style (removes classes)

typo3image Implementation:

this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
    if (selectable && selectable.name === 'typo3image') {
        editor.model.change(writer => {
            writer.removeAttribute('class', selectable);
        });
    }
});
Copied!

Logic:

  1. Check Element: Only process typo3image
  2. Remove Attribute: writer.removeAttribute('class', selectable)

    • Completely removes class attribute
    • Note: Doesn't selectively remove classes, removes all

Enhancement Pattern:

// Better implementation: remove only specific classes
this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
    if (selectable && selectable.name === 'typo3image') {
        editor.model.change(writer => {
            const currentClass = selectable.getAttribute('class') || '';
            const currentClasses = currentClass.split(' ').filter(Boolean);
            const classesToRemove = className;

            // Keep classes not being removed
            const newClasses = currentClasses.filter(
                cls => !classesToRemove.includes(cls)
            );

            if (newClasses.length > 0) {
                writer.setAttribute('class', newClasses.join(' '), selectable);
            } else {
                writer.removeAttribute('class', selectable);
            }
        });
    }
});
Copied!

Complete Integration Example 

Style Configuration (YAML) 

# Configuration/RTE/Default.yaml
editor:
  config:
    style:
      definitions:
        - name: 'Image Left'
          element: 'img'
          classes: ['float-left', 'mr-3']
        - name: 'Image Right'
          element: 'img'
          classes: ['float-right', 'ml-3']
        - name: 'Image Center'
          element: 'img'
          classes: ['d-block', 'mx-auto']
        - name: 'Full Width'
          element: 'img'
          classes: ['w-100']
Copied!

Plugin Integration (JavaScript) 

export default class Typo3Image extends Core.Plugin {
    static get requires() {
        return ['StyleUtils', 'GeneralHtmlSupport'];
    }

    init() {
        const editor = this.editor;
        const styleUtils = editor.plugins.get('StyleUtils');
        const ghs = editor.plugins.get('GeneralHtmlSupport');

        // Enable img styles for typo3image
        this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style, element]) => {
            if (style.element === 'img') {
                for (const item of editor.model.document.selection.getFirstRange().getItems()) {
                    if (item.name === 'typo3image') {
                        event.return = true;
                    }
                }
            }
        });

        // Check if style is active
        this.listenTo(styleUtils, 'isStyleActiveForBlock', (event, [style, element]) => {
            if (style.element === 'img') {
                for (const item of editor.model.document.selection.getFirstRange().getItems()) {
                    if (item.name === 'typo3image') {
                        const classAttribute = item.getAttribute('class');
                        if (classAttribute && typeof classAttribute === 'string') {
                            const classlist = classAttribute.split(' ');
                            if (style.classes.filter(value => !classlist.includes(value)).length === 0) {
                                event.return = true;
                            }
                        }
                    }
                }
            }
        });

        // Return affected elements
        this.listenTo(styleUtils, 'getAffectedBlocks', (event, [style, element]) => {
            if (style.element === 'img') {
                for (const item of editor.model.document.selection.getFirstRange().getItems()) {
                    if (item.name === 'typo3image') {
                        event.return = [item];
                        break;
                    }
                }
            }
        });

        // Apply classes
        ghs.decorate('addModelHtmlClass');
        this.listenTo(ghs, 'addModelHtmlClass', (event, [viewElement, className, selectable]) => {
            if (selectable && selectable.name === 'typo3image') {
                editor.model.change(writer => {
                    writer.setAttribute('class', className.join(' '), selectable);
                });
            }
        });

        // Remove classes
        ghs.decorate('removeModelHtmlClass');
        this.listenTo(ghs, 'removeModelHtmlClass', (event, [viewElement, className, selectable]) => {
            if (selectable && selectable.name === 'typo3image') {
                editor.model.change(writer => {
                    writer.removeAttribute('class', selectable);
                });
            }
        });
    }
}
Copied!

Troubleshooting Style Issues 

Issue: Style Drop-down Disabled for Images 

Symptoms:

  • Select image → style drop-down grayed out
  • No styles available when image selected

Causes:

  1. Missing GeneralHtmlSupport dependency
  2. Missing StyleUtils dependency
  3. Event listeners not registered
  4. Style definitions don't target 'img' element

Solutions:

Verify Dependencies 

static get requires() {
    return ['StyleUtils', 'GeneralHtmlSupport'];  // Both required!
}
Copied!

Verify Style Definitions 

style:
  definitions:
    - name: 'My Style'
      element: 'img'  # Must be 'img', not 'image'
      classes: ['my-class']
Copied!

Check Event Listeners 

// Debug in browser console
const styleUtils = editor.plugins.get('StyleUtils');
console.log(styleUtils.listenerCount('isStyleEnabledForBlock'));
// Should be > 0
Copied!

Issue: Style Changes Not Applied 

Symptoms:

  • Style selected from drop-down
  • No visual change to image
  • Class attribute not updated

Causes:

  1. GeneralHtmlSupport event listeners not registered
  2. Model-to-view conversion missing class attribute
  3. CSS classes not defined in stylesheet

Solutions:

Verify GHS Listeners 

const ghs = editor.plugins.get('GeneralHtmlSupport');
console.log(ghs.listenerCount('addModelHtmlClass'));
// Should be > 0
Copied!

Check Class Attribute Conversion 

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

Verify CSS Loaded 

/* In your stylesheet */
.float-left { float: left; margin-right: 1rem; }
.float-right { float: right; margin-left: 1rem; }
Copied!

Issue: Styles Not Shown as Active 

Symptoms:

  • Image has correct classes
  • Style not checked/highlighted in drop-down
  • Cannot tell which style is applied

Cause: isStyleActiveForBlock listener not working correctly

Solution:

Debug Class Matching 

// In isStyleActiveForBlock listener
console.log('Element classes:', item.getAttribute('class'));
console.log('Style classes:', style.classes);

const classlist = item.getAttribute('class').split(' ');
const missing = style.classes.filter(cls => !classlist.includes(cls));
console.log('Missing classes:', missing);
Copied!

Advanced Style Patterns 

Multiple Class Styles 

# Complex styles with multiple classes
style:
  definitions:
    - name: 'Responsive Image Card'
      element: 'img'
      classes: ['img-fluid', 'rounded', 'shadow-sm', 'd-block']
Copied!

Application:

// Results in model:
class: 'img-fluid rounded shadow-sm d-block'

// View output:
<img class="img-fluid rounded shadow-sm d-block" src="..." />
Copied!

Conditional Style Availability 

// Only enable certain styles for specific users
this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style, element]) => {
    if (style.element === 'img' && style.name === 'Admin Only Style') {
        // Check user permission
        if (!userHasAdminPermission()) {
            event.return = false;  // Disable this style
            event.stop();  // Prevent further processing
            return;
        }
    }

    // Default behavior for other styles
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = true;
            }
        }
    }
});
Copied!

Style Groups 

# Organize styles into groups
style:
  definitions:
    - name: 'Left Align'
      element: 'img'
      classes: ['float-left']
    - name: 'Right Align'
      element: 'img'
      classes: ['float-right']
    - name: 'Center Align'
      element: 'img'
      classes: ['mx-auto', 'd-block']

  groupDefinitions:
    - name: 'Image Alignment'
      styles: ['Left Align', 'Right Align', 'Center Align']
Copied!

Performance Considerations 

Event Listener Efficiency 

// Inefficient: Iterates entire range multiple times
this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style]) => {
    if (style.element === 'img') {
        for (const item of editor.model.document.selection.getFirstRange().getItems()) {
            if (item.name === 'typo3image') {
                event.return = true;
            }
        }
    }
});

// Efficient: Cache selection check
const isTypo3ImageSelected = () => {
    const selection = editor.model.document.selection;
    const element = selection.getSelectedElement();
    return element && element.name === 'typo3image';
};

this.listenTo(styleUtils, 'isStyleEnabledForBlock', (event, [style]) => {
    if (style.element === 'img' && isTypo3ImageSelected()) {
        event.return = true;
    }
});
Copied!

CKEditor Conversion System 

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

Conversion Architecture 

Three-Layer System 

┌──────────────────────────────────────┐
│ 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="..." />        │
└──────────────────────────────────────┘
Copied!

Upcast Conversions 

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 

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,
        });
    }
});
Copied!

Configuration Breakdown 

View Matcher 

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

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):

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

Not matched (regular img passthrough):

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

Model Creator Function 

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

Parameters:

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

Return: New model element with extracted attributes

Attribute Extraction 

fileUid: viewElement.getAttribute('data-htmlarea-file-uid'),
Copied!

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 

Input 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"
/>
Copied!

Upcast Process:

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

Result Model Element:

{
    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
    }
}
Copied!

Downcast Conversions 

Purpose 

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

Two Pipelines:

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

Image Element Downcast 

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);
    },
});
Copied!

Configuration Breakdown 

Model Matcher 

model: {
    name: 'typo3image',
    attributes: ['fileUid', 'fileTable', 'src']
}
Copied!

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: (modelElement, { writer }) => {
    return writer.createEmptyElement('img', attributes);
}
Copied!

Parameters:

  • modelElement: The typo3image model element
  • writer: View writer for creating elements

Return: New view element (<img>)

Attribute Mapping 

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

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 

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

Pattern: Only add boolean attributes when true

Why: Cleaner HTML output, avoid unnecessary attributes

Result:

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

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

Downcast Example Flow 

Input Model Element:

{
    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
    }
}
Copied!

Downcast Process:

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

Result 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"
/>
Copied!

Attribute Converters 

Class Attribute Converter 

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

Purpose: Immediately sync class attribute changes to view

Behavior:

// 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" ... />
Copied!

Custom Attribute Converters 

You can add converters for any attribute:

// 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'
});
Copied!

Data Pipeline 

Complete Load → Edit → Save Flow 

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 (ModelView)
   ─────────────────────►
   View Updates
   <img width="1200" class="float-left" ... />

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

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

Paste Handling 

Paste from External Source 

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

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
Copied!

Paste from Same Editor 

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

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

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

Custom Conversion Patterns 

Pattern 1: Transformation During Upcast 

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
        });
    }
});
Copied!

Pattern 2: Conditional Downcast 

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);
    }
});
Copied!

Pattern 3: Multi-Element Conversion 

// 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);
        }
    }
});
Copied!

Debugging Conversions 

Logging Upcast 

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
        });
    }
});
Copied!

Logging Downcast 

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);
    }
});
Copied!

Inspecting Conversion Results 

// 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')
            });
        }
    }
});
Copied!

Common Issues 

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
  2. Upcast converter not registered
  3. View matcher too restrictive

Solutions:

Verify HTML has required attributes:

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

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

Check converter registration:

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

Issue: Attributes Lost During Conversion 

Symptoms:

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

Causes:

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

Solutions:

Verify schema allows attribute:

allowAttributes: [
    'src', 'fileUid', /* add missing attribute here */
]
Copied!

Add to conversion:

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

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

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:

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

Performance Optimization 

Batch Conversions 

// 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);
    });
});
Copied!

Lazy Attribute Reading 

// 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');
Copied!

Image Quality Selector 

New in version 13.1.5

The image quality selector with SVG dimension support and multiplier-based processing.

The image dialog includes a quality selector dropdown that controls image processing for optimal display on different devices and use cases.

Quality Options 

The extension provides five quality levels for processed images:

No Scaling

No Scaling
Multiplier

N/A (original file used)

Processing

Skip image processing entirely, use original file unchanged

Indicator

● Gray

Best for: - Pre-optimized images (WebP, optimized PNG) - SVG files (vector graphics) - Newsletters and email - PDF exports - When maximum quality is required

Standard (1.0x)

Standard (1.0x)
Multiplier

1.0x

Processing

Match display dimensions exactly

Indicator

● Yellow

Best for: - Standard web displays - Content images - Balanced quality and file size

Retina (2.0x)

Retina (2.0x)
Multiplier

2.0x

Processing

2× display dimensions

Indicator

● Green

Best for: - High-DPI displays (default) - MacBook Retina screens - Modern mobile devices - Sharp images on 4K monitors

Ultra (3.0x)

Ultra (3.0x)
Multiplier

3.0x

Processing

3× display dimensions

Indicator

● Cyan

Best for: - Very sharp images required - Large format displays - Hero images and key visuals

Print (6.0x)

Print (6.0x)
Multiplier

6.0x

Processing

6× display dimensions

Indicator

● Blue

Best for: - Print-quality output - High-resolution downloads - Professional photography - SVG files (recommended default)

Dialog Layout 

The image properties dialog features a responsive 3-row layout:

Row 1: Dimensions and Quality
  • Display width in px (col-sm-4)
  • Display height in px (col-sm-4)
  • Scaling quality selector (col-sm-4)
Row 2: Advisory Title
  • Advisory Title text input (full width)
Row 3: Alternative Text
  • Alternative Text for accessibility (full width)

Visual Indicators 

Each quality option includes:

  • Color-coded marker (●) - Visual quality indicator
  • Multiplier display - Processing factor (e.g., "2.0x")
  • Persistent selection - Saved with image via data-quality attribute

Technical Implementation 

Backend Processing 

The backend automatically:

  1. Detects SVG files - Extracts dimensions from viewBox or width/height attributes
  2. Calculates target dimensions - Multiplies display dimensions by quality multiplier
  3. Preserves aspect ratio - Maintains original image proportions
  4. Suggests optimal dimensions - Provides backend dimension suggestions
  5. Respects user input - Never overwrites user-entered dimensions

Frontend Persistence 

Quality selection persists using:

  • data-quality attribute - Stores selected quality (none, standard, retina, ultra, print)
  • Backward compatibility - Maps legacy data-noscale to "No Scaling"
  • Priority order - data-quality > data-noscale > SVG default (print) > standard default (retina)
Rendered HTML with quality attribute
<img src="image.jpg"
     width="400"
     height="300"
     data-quality="retina"
     alt="Example image">
Copied!

SVG Support 

SVG (Scalable Vector Graphics) files receive special handling:

Dimension Extraction:

SVG with viewBox
<svg viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg">
  <!-- Vector content -->
</svg>
Copied!
SVG with width/height attributes
<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
  <!-- Vector content -->
</svg>
Copied!

Processing:

  • Default quality: Print (6.0x) for maximum sharpness
  • No rasterization: Original SVG file always used
  • Dimension calculation: Extracted from viewBox or width/height
  • Aspect ratio preservation: Automatic scaling calculation

Best Practices 

Choosing Quality Levels 

Use Case Recommended Quality Reason File Size Impact
Hero images Ultra (3.0x) Maximum sharpness Large
Content images Retina (2.0x) High-DPI displays Medium
Thumbnails Standard (1.0x) Sufficient quality Small
Newsletters/Email No Scaling Original quality Varies
SVG graphics Print (6.0x) Vector sharpness None (vector)

Performance Considerations 

Higher quality = Larger file size

  • Retina (2.0x):  4× file size vs Standard
  • Ultra (3.0x):  9× file size vs Standard
  • Print (6.0x):  36× file size vs Standard

Optimization tips:

  1. Use appropriate quality for context (not always maximum)
  2. Consider mobile bandwidth for content images
  3. Use "No Scaling" for SVG files when possible
  4. Balance visual quality with page load performance

Migration from noScale 

The quality selector replaces the legacy "Skip Image Processing" checkbox:

Before (deprecated):

<img src="image.jpg" data-noscale="1">
Copied!

After (modern):

<img src="image.jpg" data-quality="none">
Copied!

Backward compatibility:

  • Legacy data-noscale attributes are automatically mapped to "No Scaling" quality
  • Existing images continue to work without modification
  • New images use data-quality attribute

See Also 

Installation 

Installation steps live alongside the RTE setup workflow in RTE Setup. See also the README on GitHub for the quick-start composer command.

Usage 

Usage examples and editor workflows are documented under Examples & Use Cases. That index covers basic image insertion, captions, linked images (including the security rel behaviour), and advanced features.

Screenshots Needed for Examples 

The following screenshots are needed for the Linked Images documentation.

Quick Start with DDEV 

The easiest approach is to use the existing DDEV environment:

# Start DDEV (if not running)
ddev start

# Access TYPO3 v13 backend
# URL: https://v13.rte-ckeditor-image.ddev.site/typo3/
# Login: admin / Joh316!!

# Navigate to: Page > Home > Edit the "Regular Text Element"
# Double-click the blue test image to open Image Properties dialog
Copied!

Required Screenshots 

ClickBehaviorDropdown.png 

Purpose: Show the Click Behavior dropdown with its three options.

Steps:

  1. Edit the text content element on the Home page
  2. Double-click the blue test image to open Image Properties
  3. Click the "Click Behavior" dropdown to expand it
  4. Screenshot showing "None", "Enlarge", "Link" options

Dimensions: Cropped to dropdown area,  400px wide

LinkFieldsExpanded.png 

Purpose: Show all link fields when "Link" is selected.

Steps:

  1. In Image Properties dialog, select "Link" from Click Behavior
  2. Fill in example values: - Link URL: https://example.com/page - Link Target: _blank - Link Title: Click for details - Link CSS Class: image-link - Additional Parameters: &L=1
  3. Screenshot showing all fields populated

Dimensions: Cropped to link fields,  500px wide

LinkBrowserDialog.png 

Purpose: Show the TYPO3 link browser modal.

Steps:

  1. In Image Properties with "Link" selected
  2. Click "Browse..." button next to Link URL
  3. Screenshot the link browser showing Page/File/URL tabs

Dimensions: Full dialog,  800px wide

Screenshot Requirements 

  • Format: PNG
  • Theme: Light mode (not dark)
  • Tool: Any screenshot tool (macOS: Cmd+Shift+4, Windows: Win+Shift+S)
  • Annotations: Use TYPO3 orange (#FF8700) for highlights if needed
  • Alt text: Descriptive text required for accessibility

Alternative: Screenshot Container 

For consistent, clean screenshots:

docker run -d --name typo3-screenshots -p 8080:80 linawolf/typo3-screenshots
docker exec -it typo3-screenshots bash
composer require netresearch/rte-ckeditor-image
./vendor/bin/typo3 extension:setup
# Access http://localhost:8080/typo3 (user: j.doe)
Copied!

Images 

This directory contains images and screenshots used in the documentation.