Skip to content

Symfony UX Image

Edit this page

EXPERIMENTAL This component is currently experimental and is likely to change, or even change drastically.

Symfony UX Image renders responsive images in Symfony applications by delegating every transformation to a URL-based image provider, such as Cloudflare, Cloudinary, imgix or KeyCDN. It is part of the Symfony UX initiative.

The package never decodes, resizes or encodes an image itself. Every transformation is expressed as a URL, built by whichever provider is active; the package's own job is generating that URL, a srcset, a sizes attribute and a layout style.

Installation

Install the bundle using Composer and Symfony Flex:

1
$ composer require symfony/ux-image

Then install one of the provider bridges, for example Cloudflare:

1
$ composer require symfony/ux-cloudflare-image

The Twig functions and the <twig:ux:image> and <twig:ux:picture> components need symfony/ux-twig-component. Without it, the bundle still provides the ImageUrlGenerator service (see Getting a single URL), which is enough for an API or a worker:

1
$ composer require symfony/ux-twig-component

Rendering an image

The <twig:ux:image> component renders a single image:

1
<twig:ux:image src="/uploads/hero.jpg" alt="Hero" width="800" height="450" />

src is the public URL path of the original image, as your application serves it. Every provider reads it the same way: Cloudflare, Cloudinary, imgix and KeyCDN fetch it from your origin, and null:// renders it as is. Switching providers therefore never changes your templates.

For example, with UX_IMAGE_DSN=cloudflare://cdn.example.com, this template:

1
<twig:ux:image src="/uploads/hero.jpg" alt="Hero" width="400" height="300" />

renders the following HTML (line breaks added for readability):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<img
    src="https://cdn.example.com/cdn-cgi/image/width=400,height=300,fit=cover,format=auto/uploads/hero.jpg"
    alt="Hero"
    srcset="
        https://cdn.example.com/cdn-cgi/image/width=400,height=300,fit=cover,format=auto/uploads/hero.jpg 400w,
        https://cdn.example.com/cdn-cgi/image/width=640,height=480,fit=cover,format=auto/uploads/hero.jpg 640w,
        https://cdn.example.com/cdn-cgi/image/width=750,height=563,fit=cover,format=auto/uploads/hero.jpg 750w,
        https://cdn.example.com/cdn-cgi/image/width=800,height=600,fit=cover,format=auto/uploads/hero.jpg 800w"
    sizes="(min-width: 400px) 400px, 100vw"
    loading="lazy"
    width="400"
    height="300"
    style="object-fit: cover; max-width: 400px; max-height: 300px; aspect-ratio: 400 / 300; width: 100%; height: auto;"
/>

The browser picks the smallest candidate that covers the displayed width at the screen's pixel density, and Cloudflare picks the output format from the request's Accept header.

The equivalent ux_image() Twig function is available for programmatic use, for example when the source path is only known inside a Twig macro:

1
{{ ux_image('/uploads/hero.jpg', 'Hero', {width: 800, height: 450}) }}

Both accept the same rendering options: the component exposes them as props, the function takes them as its third, associative-array argument, keyed by prop name in camelCase (objectFit, not object-fit). An unknown key there throws an InvalidArgumentException.

Both accept HTML attributes too: the component takes them next to its props, the function as its fourth argument:

1
{{ ux_image('/uploads/hero.jpg', 'Hero', {width: 800}, {class: 'rounded', loading: 'eager'}) }}

Props

Prop Type Default
src string required
alt string required
layout fixed|constrained|full-width constrained
width int|null null
height int|null null
fit cover|contain|null cover or null, see below
format string|null null
quality int|null (1 to 100) null
priority bool false
object-fit string|null the fit value
breakpoints int[]|null the resolutions option
operations array<string, array> {}
preset string|null null

layout and its breakpoints, sizes and generated style are covered under Layout and rendering. priority sets loading="eager" fetchpriority="high"; without it, an image only gets loading="lazy".

preset applies a named set of transformations from the configuration (see The presets option).

Caution

Images rendered by the component and by ux_image() are lazy-loaded by default. If an image is likely to be the page's Largest Contentful Paint (a hero image, the first product photo), set priority on it; otherwise the browser waits before loading it, which delays the LCP, and Lighthouse will report it. Here is an example:

1
<twig:ux:image src="/uploads/hero.jpg" alt="Hero" width="1200" height="600" priority />

fit decides how the provider reshapes the source image into the requested width x height. cover fills the box and crops the excess, and contain fits the whole image inside it. Both enlarge a source that is smaller than the box. fit defaults to cover when both width and height are set, and it only has an effect then, since without both there is no target box to fit into. It only produces a visible difference when the source image's aspect ratio differs from the requested one: at equal ratios there is nothing to crop and nothing to letterbox, so both modes come out identical.

The generated object-fit follows fit (cover -> cover, contain -> contain), so the browser doesn't redo a crop the provider was asked to avoid. Without that, a contain image would be cropped back to fill its box by CSS, and fit would never be observable.

format pins the output format for this one image, in place of both the provider's own negotiation and the per-format <picture> fallbacks: the <img> is rendered in that format, and a <picture> gets no <source>. A format the active provider cannot produce throws an InvalidArgumentException naming its supported list.

Any attribute not listed above (class, data-*, a caller-supplied style or sizes, …), like any attribute passed to ux_image(), is passed through to the rendered <img> and wins over a generated one such as loading. A caller-supplied style merges with the generated layout style instead of replacing it; a caller-supplied sizes replaces the generated value.

Provider-specific operations

Every provider accepts extra, provider-specific parameters beyond the common width/height/fit/format/quality set (see Providers for the full list per bridge). Pass them through operations, keyed by provider name:

1
2
<twig:ux:image src="/uploads/hero.jpg" alt="Hero" width="800"
    :operations="{cloudflare: {sharpen: 1}, keycdn: {sharpen: 10}}" />

At render time, only operations[activeProviderName] is read; the rest is ignored. Keying by provider name is deliberate: the active DSN changes between environments, and a flat, un-keyed gravity option would silently vanish the moment the application switched from Cloudflare to another provider. Passing an operation the active provider does not support throws an InvalidArgumentException naming the provider's supported list. A key that names no installed provider throws as well, so a typo such as cloudfare fails in development rather than being silently ignored in production.

Getting a single URL

Some places need one URL rather than a responsive image: an Open Graph image, an email, an API response or a CSS background. The ux_image_url() Twig function returns it:

1
<meta property="og:image" content="{{ absolute_url(ux_image_url('/uploads/hero.jpg', {width: 1200, height: 630})) }}">

It accepts width, height, fit, format, quality, operations and preset, with the same meaning as the component's props. Like the component, it defaults fit to cover when both width and height are set. An unknown option throws an InvalidArgumentException.

In PHP, autowire Symfony\UX\Image\ImageUrlGenerator, which applies the same validation:

1
2
3
4
5
6
7
8
9
10
11
use Symfony\UX\Image\ImageUrlGenerator;

public function __construct(
    private ImageUrlGenerator $imageUrlGenerator,
) {
}

public function ogImage(): string
{
    return $this->imageUrlGenerator->generate('/uploads/hero.jpg', width: 1200, height: 630);
}

Replacing an original

A transformation URL only depends on the original's path and on the transformation parameters. When you replace an original under the same path, the provider and the browsers keep serving the variants they already cached. Give each version of an image its own path instead, for example with a hash or a version in the filename (/uploads/hero.3f2a1c.jpg).

Configuration

Configuration is done in your config/packages/ux_image.yaml file:

1
2
3
4
5
6
7
# config/packages/ux_image.yaml
ux_image:
    provider: '%env(resolve:UX_IMAGE_DSN)%'
    formats: ['avif', 'webp', 'jpeg']
    resolutions: [6016, 5120, 4480, 3840, 3200, 2560, 2048, 1920, 1668, 1280, 1080, 960, 828, 750, 640]
    quality: null
    presets: {}

The resolve: processor is required: a DSN may reference container parameters such as %kernel.project_dir%, and parameter resolution does not recurse into environment variable values on its own.

The provider DSN

The provider option is a DSN string that selects which bridge renders your images, and configures it. Define it in your .env files so it can change per environment:

1
2
# .env
UX_IMAGE_DSN=null://null
1
2
# .env.prod
UX_IMAGE_DSN=cloudflare://cdn.example.com

null://null is the default when no provider is configured. It needs no bridge and transforms nothing: each image renders with its original URL, no srcset and no sizes, but keeps its dimensions and layout style. That makes it the provider to develop and test templates with when no CDN is available.

Each bridge is only registered when its Composer package is actually installed. Install the one matching the scheme used in the DSN:

Scheme Install DSN example
keycdn composer require symfony/ux-keycdn-image keycdn://myzone.kxcdn.com
cloudflare composer require symfony/ux-cloudflare-image cloudflare://cdn.example.com
cloudinary composer require symfony/ux-cloudinary-image cloudinary://my-cloud?origin=https://example.com
imgix composer require symfony/ux-imgix-image imgix://my-source.imgix.net

See Providers for what each DSN option means and how transformation parameters map to that provider's own query string. A DSN option the provider does not support throws an InvalidArgumentException naming the options it does support.

The formats option

formats is the candidate output format list, in preference order. It is intersected with the active provider's own supported formats (see Providers), then:

  • ux_picture() renders one <source> per surviving entry (see Layout and rendering);
  • ux_image() renders its single <img> in the last surviving entry, unless the provider negotiates the format itself, like Cloudflare with its own format=auto; formats then has no effect on it.

An empty intersection throws an exception naming both lists, so asking a provider that cannot encode AVIF to serve only AVIF fails at first render rather than serving the wrong format silently.

The default is ['avif', 'webp', 'jpeg']. Narrowing it is how an application keeps a format off the wire.

The resolutions option

resolutions is the resolution ladder the constrained and full-width layouts draw their srcset candidates from (see The resolution ladder). It defaults to Symfony\UX\Image\Renderer\LayoutResolver::DEFAULT_RESOLUTIONS, the common screen widths ported from unpic.

Narrow it when your images never reach those sizes, or when you want fewer candidates per srcset; a component can still override it for one image through the breakpoints prop. Each candidate is one more variant the provider generates and caches, so a shorter ladder also means fewer transformations on your CDN.

The quality option

quality is the output quality, from 1 to 100, of every generated image that does not set its own through the quality prop or option, or through a preset. It defaults to null, which leaves the quality to the provider.

The presets option

presets gives a name to a set of transformations, which templates then apply through the preset prop or option:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# config/packages/ux_image.yaml
ux_image:
    presets:
        thumbnail:
            width: 200
            height: 200
            quality: 70
        og:
            width: 1200
            height: 630
            format: jpeg
        vintage:
            operations:
                cloudflare: { saturation: 0.3, contrast: 1.2 }
1
2
3
4
5
<twig:ux:image src="/uploads/hero.jpg" alt="Hero" preset="thumbnail" />

{{ ux_image('/uploads/hero.jpg', 'Hero', {preset: 'vintage', width: 800}) }}

<meta property="og:image" content="{{ absolute_url(ux_image_url('/uploads/hero.jpg', {preset: 'og'})) }}">

In PHP, pass the preset name to ImageUrlGenerator::generate(), or to the RenderOptions given to ImageRendererInterface:

1
2
3
$url = $this->imageUrlGenerator->generate('/uploads/hero.jpg', preset: 'og');

$image = $this->imageRenderer->render('/uploads/hero.jpg', 'Hero', new RenderOptions(preset: 'thumbnail'));

A preset holds only width, height, fit, format, quality and operations. layout, breakpoints, object-fit and priority describe where the image sits on the page, so they stay in the template.

A value set in the template, or passed to ImageUrlGenerator::generate(), wins over the preset's value. Each value is replaced on its own. For example, setting only width on a 200x200 preset keeps its height of 200, so the aspect ratio changes. operations are merged per provider and per operation, so a template can add an operation or change one. The global quality option only applies when neither the template nor the preset sets one. A template cannot remove a value from a preset. Define another preset instead.

An unknown preset name throws an InvalidArgumentException listing the defined presets. A preset whose operations name a provider that is not installed fails when the container is compiled, so a typo is caught even in an environment whose active provider is null://.

Layout and rendering

The layout prop

layout decides the breakpoint ladder used to build srcset, the sizes attribute, and the generated style. constrained is the default.

fixed
Breakpoints: [width, 2 × width]. sizes: {width}px. style: object-fit: cover; width: {width}px; height: {height}px.
constrained (the default)
Breakpoints: [width, 2 × width, …ladder entries below 2 × width]. sizes: (min-width: {width}px) {width}px, 100vw. style: object-fit: cover; max-width: {width}px; max-height: {height}px; aspect-ratio: {width/height}; width: 100%; height: auto.
full-width
Breakpoints: the full resolution ladder. sizes: 100vw. With both width and height given, style: object-fit: cover; width: 100%; aspect-ratio: {width/height}; height: auto. With only height (no derivable ratio), style: object-fit: cover; width: 100%; height: {height}px.

Both constrained and full-width (when a ratio applies) declare height: auto alongside aspect-ratio: the width/height HTML attributes on <img> become a definite CSS height through the browser's own presentational hint, which would otherwise make CSS ignore aspect-ratio outright.

fixed and constrained require width; full-width requires height. Passing neither throws an InvalidArgumentException.

The generated sizes assumes the image spans the whole viewport when the viewport is narrower than its width. That is right for a full-width container, not for a grid or a sidebar, where the browser would download a candidate several times too wide. Pass your own sizes there:

1
2
3
{# three columns from 1024px, two from 640px, one below #}
<twig:ux:image src="/uploads/hero.jpg" alt="Hero" width="400" height="300"
    sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw" />

object-fit overrides the value derived from fit. It defaults to the fit value itself, or to cover when no fit applies.

The resolution ladder

Unless breakpoints is passed explicitly, candidates for constrained and full-width are drawn from the resolutions option, which defaults to a descending ladder of common screen widths ported from unpic:

1
6016, 5120, 4480, 3840, 3200, 2560, 2048, 1920, 1668, 1280, 1080, 960, 828, 750, 640

constrained keeps only the entries below twice the requested width; full-width keeps the whole ladder.

<img> or <picture>

The element is the caller's choice, never the provider's, so switching UX_IMAGE_DSN never changes the markup:

  • ux_image() and <twig:ux:image> always render a single <img>. When the provider picks the output format from the request itself, like Cloudflare's format=auto, that <img> still gets a modern format. When it cannot, like KeyCDN, the <img> uses the last entry of the formats option.
  • ux_picture() and <twig:ux:picture> always render a <picture>, with one <source type="image/…" srcset="…"> per entry of the formats option intersected with the provider's supported formats, in that order, followed by the <img> as the last-resort fallback. Every source names its format in its URLs, which suits a provider without automatic negotiation, and caches that ignore Vary: Accept.
1
2
3
<twig:ux:picture src="/uploads/hero.jpg" alt="Hero" width="800" height="450" />

{{ ux_picture('/uploads/hero.jpg', 'Hero', {width: 800, height: 450}) }}

Both take the same props, options and attributes as <twig:ux:image> and ux_image(). With the same Cloudflare DSN, <twig:ux:picture src="/uploads/hero.jpg" alt="Hero" width="400" height="300" /> renders one <source> per configured format, then the <img> fallback:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<picture>
    <source type="image/avif" sizes="(min-width: 400px) 400px, 100vw" srcset="
        https://cdn.example.com/cdn-cgi/image/width=400,height=300,fit=cover,format=avif/uploads/hero.jpg 400w,
        https://cdn.example.com/cdn-cgi/image/width=640,height=480,fit=cover,format=avif/uploads/hero.jpg 640w,
        https://cdn.example.com/cdn-cgi/image/width=750,height=563,fit=cover,format=avif/uploads/hero.jpg 750w,
        https://cdn.example.com/cdn-cgi/image/width=800,height=600,fit=cover,format=avif/uploads/hero.jpg 800w" />
    <source type="image/webp" sizes="(min-width: 400px) 400px, 100vw" srcset="
        https://cdn.example.com/cdn-cgi/image/width=400,height=300,fit=cover,format=webp/uploads/hero.jpg 400w,
        https://cdn.example.com/cdn-cgi/image/width=640,height=480,fit=cover,format=webp/uploads/hero.jpg 640w,
        https://cdn.example.com/cdn-cgi/image/width=750,height=563,fit=cover,format=webp/uploads/hero.jpg 750w,
        https://cdn.example.com/cdn-cgi/image/width=800,height=600,fit=cover,format=webp/uploads/hero.jpg 800w" />
    <source type="image/jpeg" sizes="(min-width: 400px) 400px, 100vw" srcset="
        https://cdn.example.com/cdn-cgi/image/width=400,height=300,fit=cover,format=jpeg/uploads/hero.jpg 400w,
        https://cdn.example.com/cdn-cgi/image/width=640,height=480,fit=cover,format=jpeg/uploads/hero.jpg 640w,
        https://cdn.example.com/cdn-cgi/image/width=750,height=563,fit=cover,format=jpeg/uploads/hero.jpg 750w,
        https://cdn.example.com/cdn-cgi/image/width=800,height=600,fit=cover,format=jpeg/uploads/hero.jpg 800w" />
    <img
        src="https://cdn.example.com/cdn-cgi/image/width=400,height=300,fit=cover,format=jpeg/uploads/hero.jpg"
        alt="Hero"
        srcset="
            https://cdn.example.com/cdn-cgi/image/width=400,height=300,fit=cover,format=jpeg/uploads/hero.jpg 400w,
            https://cdn.example.com/cdn-cgi/image/width=640,height=480,fit=cover,format=jpeg/uploads/hero.jpg 640w,
            https://cdn.example.com/cdn-cgi/image/width=750,height=563,fit=cover,format=jpeg/uploads/hero.jpg 750w,
            https://cdn.example.com/cdn-cgi/image/width=800,height=600,fit=cover,format=jpeg/uploads/hero.jpg 800w"
        sizes="(min-width: 400px) 400px, 100vw"
        loading="lazy"
        width="400"
        height="300"
        style="object-fit: cover; max-width: 400px; max-height: 300px; aspect-ratio: 400 / 300; width: 100%; height: auto;"
    />
</picture>

A format prop short-circuits the choice: it names one format, so there is nothing left to fall back to. ux_image() renders its <img> in that format, and ux_picture() renders a <picture> that only holds that <img>.

The <picture> emitted here only ever carries per-format fallbacks. It never carries a different crop per media query (art direction); that is out of scope for this version.

Providers

Parameter mapping

Every ImageTransformation property maps to a provider-specific query parameter:

ImageTransformation Cloudflare Cloudinary imgix KeyCDN
width width w w width
height height h h height
format format f fm format
format: auto format=auto f_auto auto=format  
quality quality q q quality
fit: Fit::Cover fit=cover c_fill fit=crop fit=cover
fit: Fit::Contain fit=contain c_fit fit=clip fit=contain

operations (see Provider-specific operations) is merged into the generated URL verbatim, once resolved for the active provider.

Supported formats and negotiation

Provider Supported formats Negotiates automatically?
Cloudflare avif, webp, jpeg, png Yes
Cloudinary avif, webp, jpeg, png Yes
imgix avif, webp, jpeg, png Yes
KeyCDN webp, jpeg, png No

Cloudflare, Cloudinary and imgix negotiate natively, through their own format=auto, f_auto and auto=format. KeyCDN has no automatic format negotiation at all, and no AVIF support either: with it, ux_image() serves the last configured format KeyCDN supports, and ux_picture() is the way to offer WebP to the browsers that accept it (see or ).

Cloudflare

Cloudflare Image Resizing transforms images already served from your own origin, through the /cdn-cgi/image/ URL path on a Cloudflare zone. No image-processing server of your own is needed.

1
$ composer require symfony/ux-cloudflare-image
1
2
# .env.prod
UX_IMAGE_DSN=cloudflare://cdn.example.com

The host is the domain proxied by your Cloudflare zone; it must be the domain your origin images are served from. Image transformations must be enabled on that zone before /cdn-cgi/image/ URLs work — see Enable transformations in the Cloudflare docs.

Extra operations, forwarded as-is: gravity, dpr, rotate, trim, blur, brightness, contrast, gamma, saturation, sharpen, background, border, anim, metadata, onerror, compression. See Cloudflare's own options reference for what each one does.

Cloudinary

Cloudinary transforms images either from your own origin or from your Cloudinary media library.

1
$ composer require symfony/ux-cloudinary-image
1
2
# .env.prod
UX_IMAGE_DSN=cloudinary://my-cloud?origin=https://example.com

The host is your Cloudinary cloud name. The origin option picks how images are delivered:

  • With origin, the provider uses fetch delivery. origin is the base URL your application serves the original images from, and every src is appended to it. Cloudinary downloads each original from there, then transforms and caches it. No upload to Cloudinary is needed.
  • Without origin (cloudinary://my-cloud), the provider uses upload delivery, and src is the public ID of an image in your media library, without its leading slash. To keep the same src as your application, set up an auto-upload mapping from a folder to your origin: Cloudinary then copies each original into the media library on the first request.

Fetch delivery only works when the "Fetched URL" box is cleared in the "Restricted media types" section of the account's security settings. Cloudinary ticks this box by itself when an account does not use fetch within 7 days of its creation. If the box is ticked, sign every URL with the api_secret option described below.

An account can require every delivery URL to be signed, with the Strict transformations setting. Give the account's API secret as the api_secret DSN option, and every generated URL then carries the matching s--…-- signature:

1
2
3
4
5
6
# .env.prod
CLOUDINARY_API_SECRET="<override me>"
UX_IMAGE_DSN=cloudinary://my-cloud?origin=https://example.com&api_secret=${CLOUDINARY_API_SECRET}

# .env.prod.local
CLOUDINARY_API_SECRET=the-api-secret

Warning

The API secret signs every URL, and it also grants full access to your Cloudinary account through its API. Keep it out of the files you commit, and store it the way you store your other secrets.

Extra operations, forwarded as-is with Cloudinary's own short names: a (angle), b (background), bo (border), co (color), d (default image), dpr, e (effect), fl (flags), g (gravity), o (opacity), r (radius), t (named transformation), x, y, z (zoom). A value can contain a :, like {e: 'sharpen:100'}, but not a / or a ,. See Cloudinary's own transformation reference for what each one does.

imgix

imgix transforms images from a source you configure in its dashboard, through query string parameters appended to the source's URL.

1
$ composer require symfony/ux-imgix-image
1
2
# .env.prod
UX_IMAGE_DSN=imgix://my-source.imgix.net

The host is the domain of your imgix source, either its imgix.net subdomain or a custom domain. With a Web Folder source pointing at your origin, src stays the path your application serves the image from. With a storage source, like Amazon S3, src is the path of the image in the bucket.

When the source has secure URLs enabled, give its token as the sign_key option, and every generated URL then carries the matching s parameter. Keep the token out of the files you commit, for example with a placeholder that a .env.prod.local file overrides:

1
2
3
4
5
6
# .env.prod
IMGIX_SIGN_KEY="<override me>"
UX_IMAGE_DSN=imgix://my-source.imgix.net?sign_key=${IMGIX_SIGN_KEY}

# .env.prod.local
IMGIX_SIGN_KEY=the-token

Warning

The token signs every URL: anyone holding it can request any transformation of any image of the source. Store it the way you store your other secrets.

Extra operations, forwarded as-is: auto, bg, blur, border, bri, con, crop, dpr, exp, fill, fill-color, flip, fp-x, fp-y, fp-z, gam, high, invert, monochrome, orient, pad, rect, rot, sat, sepia, shad, sharp, trim, usm, vib. An auto operation, like compress, is joined to the auto=format of automatic format negotiation. See imgix's own rendering API reference for what each one does.

KeyCDN

KeyCDN Image Processing transforms images already served from your own origin, through query string parameters appended to your zone's URL.

1
$ composer require symfony/ux-keycdn-image
1
2
# .env.prod
UX_IMAGE_DSN=keycdn://myzone.kxcdn.com

The host is your KeyCDN zone.

A zone can require every image processing request to be signed, with the Secure Token setting. Give the zone's Secure Token Key as the secure_token DSN option, and every generated URL then carries the matching token parameter:

1
2
3
4
5
6
# .env.prod
UX_IMAGE_DSN=keycdn://myzone.kxcdn.com?secure_token=%env(KEYCDN_SECURE_TOKEN)%
KEYCDN_SECURE_TOKEN=<override me>

# .env.prod.local
KEYCDN_SECURE_TOKEN=the-secure-token-key

Warning

The Secure Token Key signs every URL, so anyone holding it can have your zone transform any image. Keep it out of the files you commit, and store it the way you store your other secrets.

Extra operations, forwarded as-is: position, enlarge, trim, crop, bg, rotate, flip, flop, sharpen, blur, gamma, grayscale, progressive, lossless, metadata. See KeyCDN's own parameter reference for what each one does.

Protecting transformations

Anyone can request a transformation URL, with any size or quality, and each distinct URL is one more image the provider generates and caches:

  • Cloudflare only transforms images from the zone that serves the transformations by default. Keep it that way unless you need other origins, see the source origins settings of Cloudflare Image Resizing.
  • Cloudinary can require signed URLs on the account, with Strict transformations. Turn it on, and give the API secret to the provider with the api_secret DSN option: the account then serves only the URLs your application generated.
  • With fetch delivery, anyone can also ask your Cloudinary account to fetch an image from any other domain. Restrict the allowed fetch domains to your origin.
  • imgix can require signed URLs on a source, with secure URLs. Turn it on, and give the source's token to the provider with the sign_key DSN option: the source then serves only the URLs your application generated.
  • KeyCDN can require a signed token on the zone, with Secure Token. Turn it on, and give the key to the provider with the secure_token DSN option: the zone then serves only the URLs your application generated.

Writing your own provider

A provider implements Symfony\UX\Image\Provider\ProviderInterface and is built from the DSN by a factory implementing Symfony\UX\Image\Provider\ProviderFactoryInterface. Extending Symfony\UX\Image\Provider\AbstractProviderFactory gives the factory the scheme matching. The factory declares the DSN options it reads in configureOptions(), with the OptionsResolver component, and gets them back, checked, from resolveOptions().

Register the factory with the ux_image.provider_factory tag, and a provider attribute holding the provider's name, which is both its DSN scheme and its operations key:

1
2
3
4
5
# config/services.yaml
services:
    App\Image\MyCdnProviderFactory:
        tags:
            - { name: 'ux_image.provider_factory', provider: 'mycdn' }

Without the provider attribute, the factory still works, but operations keys are no longer checked against the installed providers.

The package supports PHP 8.4 or later and Symfony 7.4 or 8.x.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version