---
title: "Custom Processor"
manual: "Imaginator"
version: "1.0"
permalink: "https://docs.typo3.org/permalink/schliesser/imaginator:extend-custom-processor@1.0"
source: "Extend/CustomProcessor.rst"
rendered: "2026-10-13T11:21:40+00:00"
---

# Custom Processor {#extend-custom-processor}

A processor turns an `ImageVariant` into the URL that goes into `srcset`
(and, for local processors, processes the file). All built-ins (`local:async`,
`local:sync`, `imgproxy` and `imagor`) implement the same interface; your
own is added the same way: put `#[AsImaginatorProcessor('my-key')]` on your
class, pick the interface that fits, set `processor = my-key` — no YAML.

## New CDN provider: a URL builder is enough {#new-cdn-provider-a-url-builder-is-enough}

For an external provider (CDN / image service) you rarely need a full processor —
the non-grammar work (source-URL resolution, editor-crop replay, offloaded
semantics) is already written once in `ExternalImageProcessor`. Implement
only the provider's URL grammar:

**Classes/UrlBuilder/MyCdnUrlBuilder.php (in your extension)**

```php
use Schliesser\Imaginator\Attribute\AsImaginatorProcessor;
use Schliesser\Imaginator\Dto\ExternalConfig;
use Schliesser\Imaginator\Dto\ImageVariant;
use Schliesser\Imaginator\Dto\Rectangle;
use Schliesser\Imaginator\UrlBuilder\UrlBuilderInterface;

#[AsImaginatorProcessor('my-cdn')]
final readonly class MyCdnUrlBuilder implements UrlBuilderInterface
{
    public function __construct(private ExternalConfig $config) {}

    public function build(ImageVariant $variant, string $sourceUrl, ?Rectangle $crop = null): string
    {
        return sprintf(
            '%s/%dx%d/%s',
            rtrim($this->config->baseUrl, '/'),
            $variant->width,
            $variant->height,
            $sourceUrl,
        );
    }
}
```

The compiler pass wraps the builder in a configured
`ExternalImageProcessor` automatically. Convention: the builder takes
`ExternalConfig` as its **sole constructor argument** — it carries the
unified [processorBaseUrl](https://docs.typo3.org/permalink/schliesser/imaginator:confval-conf-processorbaseurl@1.0) /
[processorSignKey](https://docs.typo3.org/permalink/schliesser/imaginator:confval-conf-processorsignkey@1.0) /
[processorSalt](https://docs.typo3.org/permalink/schliesser/imaginator:confval-conf-processorsalt@1.0) settings; ignore the fields your
provider has no equivalent for (imagor ignores `salt`). Keep the builder pure
(no I/O), so it stays golden-file testable.

Provider-specific extras (e.g. a Cloudflare account hash) arrive in
`ExternalConfig::$options`; read them in the builder via
`$this->config->option('accountHash')` or
`$this->config->requireOption('accountHash')` — the latter throws a
descriptive exception naming the exact configuration path, so a
misconfiguration fails loudly instead of emitting broken URLs.

Where the options live depends on who ships the builder. Extension
Configuration is namespaced per extension, so a builder shipped in **your own
extension** declares its extension key on the attribute and keeps the options
in **its own** namespace — its own [`ext_conf_template.txt`](https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ExtensionArchitecture/FileStructure/ExtConfTemplate.html#file-extension-ext-conf-template-txt), its own
backend form:

```php
#[AsImaginatorProcessor('my-cdn', extensionKey: 'my_cdn')]
final readonly class MyCdnUrlBuilder implements UrlBuilderInterface { /* … */ }
```

**config/system/settings.php (or your ext_conf_template.txt backend form)**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['my_cdn'] = [
    'accountHash' => 'abc123',
];
```

Without `extensionKey` the options come from imaginator's own
`processorOptions` map (used by the built-in providers; it has no backend
UI):

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['imaginator']['processorOptions'] = [
    'accountHash' => 'abc123',
];
```

## The full interface {#the-full-interface}

When URL grammar is not enough (local processing, exotic materialization),
implement the processor interface itself — same attribute:

**Classes/Imaging/ImageProcessorInterface.php**

```php
interface ImageProcessorInterface
{
    /** Render-time: URL that belongs in srcset for this variant. */
    public function buildUrl(ImageVariant $variant): string;

    /** True when pixels happen elsewhere (external CDN/service); local processing is skipped. */
    public function isOffloaded(): bool;

    /** Local processors only: produce/return the processed binary. */
    public function materialize(ImageVariant $variant): ProcessedImage;
}
```

-   Return `true` from `isOffloaded()` for an external service (CDN /
    provider). `buildUrl()` then maps the variant onto the provider's URL
    grammar and `materialize()` is never called (the webserver never
    touches pixels).
-   Return `false` for a local processor; `materialize()` produces the
    derivative through TYPO3's `ImageService`.

## Register it {#register-it}

One attribute covers both shapes — the implemented interface decides:

```php
#[AsImaginatorProcessor('my-cdn')]
final class MyProcessor implements ImageProcessorInterface { /* … */ }
```

-   `ImageProcessorInterface` → the service itself is registered under the
    key.
-   `UrlBuilderInterface` → a configured `ExternalImageProcessor`
    wrapping the builder is registered under the key.
-   Neither or both → the container compile fails with a descriptive exception;
    duplicate and empty keys fail too (nothing silently last-wins).

Manual tagging remains a supported alternative — e.g. for multi-key aliasing or
when your service needs its own factory. Tag it with
`imaginator.image_processor` and a `key`; the registry is a lazy service
locator indexed by that key:

**Configuration/Services.yaml (in your extension)**

```yaml
Vendor\MyExt\Imaging\MyCdnProcessor:
  tags:
    - { name: 'imaginator.image_processor', key: 'my-cdn' }
```

Escalation ladder: attribute on a URL builder (zero configuration code) →
builder + options (`extensionKey` for your own namespace, or imaginator's
`processorOptions`) → manual yaml named service with your own factory
→ full `ImageProcessorInterface` class (same attribute).

Select it with the [processor](https://docs.typo3.org/permalink/schliesser/imaginator:confval-conf-processor@1.0) setting:

**config/system/settings.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['imaginator']['processor'] = 'my-cdn';
```

> [!NOTE]
> TYPO3 caches the compiled container without tracking class files — after
> adding or changing the attribute, flush caches (same as after editing a
> yaml tag).

> [!NOTE]
> Keep the field set and order of `CanonicalParams` untouched if you reuse
> the signed-endpoint path — the HMAC signature depends on it byte-for-byte.
> An external processor that points `srcset` straight at its provider does not
> sign at all.
