Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions splitsh.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"ux-image": { "prefixes": [{ "from": "src/Image", "to": "", "excludes": ["src/Bridge"] }] },
"ux-cloudflare-image": "src/Image/src/Bridge/Cloudflare",
"ux-cloudinary-image": "src/Image/src/Bridge/Cloudinary",
"ux-imagor-image": "src/Image/src/Bridge/Imagor",
"ux-imgix-image": "src/Image/src/Bridge/Imgix",
"ux-ipx-image": "src/Image/src/Bridge/Ipx",
"ux-keycdn-image": "src/Image/src/Bridge/KeyCdn",
Expand Down
62 changes: 55 additions & 7 deletions src/Image/doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ 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`_, `IPX`_ or `KeyCDN`_. It is part
of `the Symfony UX initiative`_.
`Cloudflare`_, `Cloudinary`_, `Imagor`_, `imgix`_, `IPX`_ 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
Expand Down Expand Up @@ -51,7 +51,8 @@ The ``<twig:ux:image>`` component renders a single image:
``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, IPX from your ``public/`` directory,
and ``null://`` renders it as is.
and ``null://`` renders it as is. Imagor fetches it from your origin once its
``origin`` DSN option is set.
Switching providers therefore never changes your templates.

For example, with ``UX_IMAGE_DSN=cloudflare://cdn.example.com``, this template:
Expand Down Expand Up @@ -289,6 +290,7 @@ 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``
``imagor`` ``composer require symfony/ux-imagor-image`` ``imagor://images.example.com?origin=https://example.com``
``imgix`` ``composer require symfony/ux-imgix-image`` ``imgix://my-source.imgix.net``
``ipx`` ``composer require symfony/ux-ipx-image`` ``ipx://images.example.com``
============== ================================================ ===========================================================
Expand Down Expand Up @@ -548,8 +550,8 @@ Providers
Parameter mapping
~~~~~~~~~~~~~~~~~

Every ``ImageTransformation`` property maps to a provider-specific query
parameter:
Every ``ImageTransformation`` property maps to provider-specific URL parameters.
Imagor uses path segments, described in its section below:

========================= =============== =============== =============== =============== ===============
``ImageTransformation`` `Cloudflare`_ `Cloudinary`_ `imgix`_ `IPX`_ `KeyCDN`_
Expand All @@ -567,8 +569,8 @@ IPX takes ``width`` and ``height`` together as one ``s`` modifier, like
``s_800x450``.

``operations`` (see
:ref:`Provider-specific operations <image_provider_operations>`) is merged
into the generated URL verbatim, once resolved for the active provider.
:ref:`Provider-specific operations <image_provider_operations>`) is translated
into the generated URL, once resolved for the active provider.

Supported formats and negotiation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -578,6 +580,7 @@ Provider Supported formats Negotiates automatically?
============= ===================================== =========================
`Cloudflare`_ ``avif``, ``webp``, ``jpeg``, ``png`` **Yes**
`Cloudinary`_ ``avif``, ``webp``, ``jpeg``, ``png`` **Yes**
`Imagor`_ ``avif``, ``webp``, ``jpeg``, ``png`` **No**
`imgix`_ ``avif``, ``webp``, ``jpeg``, ``png`` **Yes**
`IPX`_ ``avif``, ``webp``, ``jpeg``, ``png`` **Yes**
`KeyCDN`_ ``webp``, ``jpeg``, ``png`` **No**
Expand Down Expand Up @@ -681,6 +684,46 @@ image), ``dpr``, ``e`` (effect), ``fl`` (flags), ``g`` (gravity), ``o``
``/`` or a ``,``. See `Cloudinary's own transformation reference`_ for what
each one does.

Imagor
~~~~~~

`Imagor`_ is a self-hosted image processing server:

.. code-block:: terminal

$ composer require symfony/ux-imagor-image

.. code-block:: dotenv

UX_IMAGE_DSN=imagor://images.example.com?origin=https%3A%2F%2Fexample.com&sign_key=your-secret

The host is the image server. ``origin`` is the source HTTP(S) base URL;
without it, ``src`` is a storage/loader key. With ``origin``, ``src`` always
resolves under it, even when it is an absolute URL. The DSN preserves ports
and an optional reverse-proxy path prefix. Add ``secure=false`` for a local
HTTP server.

``sign_key`` must match ``IMAGOR_SECRET``. The default ``signer=sha1`` and
``sign_truncate=0`` match stock Imagor. ``sha256`` and ``sha512`` are also
supported; match the signer and truncation to the server configuration.
Without a key, URLs use ``unsafe`` and require unsafe mode on the server.
URL-encode DSN values and keep signing keys outside committed files.

Dimensions become ``WIDTHxHEIGHT`` (``0`` for an unspecified dimension).
Cover uses the default centered crop; contain uses ``fit-in`` without
upscaling. Quality and format become ``quality()`` and ``format()`` filters.
Automatic format negotiation depends on server flags, so the bridge does
not advertise it; use ``ux_picture()`` to offer multiple explicit formats.

Supported operations are ``background_color``, ``blur``, ``brightness``,
``contrast``, ``crop``, ``dpi``, ``fill``, ``focal``, ``grayscale``, ``hue``,
``no_upscale``, ``page``, ``rgb``, ``rotate``, ``round_corner``, ``saturation``,
``sharpen``, ``strip_exif`` and ``strip_metadata``. The four no-argument filters
(``grayscale``, ``no_upscale``, ``strip_exif``, ``strip_metadata``) take booleans.
Other filters take numbers or argument strings, such as ``'10,20,200,150'``
for ``crop``. Filters preserve insertion order; crop runs after resize.
See the bridge README for argument restrictions and signing examples.

imgix
~~~~~

Expand Down Expand Up @@ -893,6 +936,10 @@ distinct URL is one more image the provider generates and caches:
* 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.
* Imagor requires signed URLs unless ``IMAGOR_UNSAFE`` is on. Give the
server's ``IMAGOR_SECRET`` to the provider with the ``sign_key`` DSN option,
and keep ``IMAGOR_UNSAFE`` off in production: the server then serves only
the URLs your application generated.
* 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.
Expand Down Expand Up @@ -942,6 +989,7 @@ The package supports PHP 8.4 or later and Symfony 7.4 or 8.x.
.. _`auto-upload mapping`: https://cloudinary.com/documentation/migration#lazy_migration_with_auto_upload
.. _`Cloudinary's own transformation reference`: https://cloudinary.com/documentation/transformation_reference
.. _`Strict transformations`: https://cloudinary.com/documentation/control_access_to_media#strict_transformations
.. _`Imagor`: https://github.com/symfony/ux/blob/3.x/src/Image/src/Bridge/Imagor/README.md
.. _`KeyCDN`: https://github.com/symfony/ux/blob/3.x/src/Image/src/Bridge/KeyCdn/README.md
.. _`KeyCDN Image Processing`: https://www.keycdn.com/support/image-processing
.. _`KeyCDN's own parameter reference`: https://www.keycdn.com/support/image-processing
Expand Down
3 changes: 3 additions & 0 deletions src/Image/src/Bridge/Imagor/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.git* export-ignore
/phpunit.dist.xml export-ignore
/tests export-ignore
8 changes: 8 additions & 0 deletions src/Image/src/Bridge/Imagor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/config/reference.php
/config/schema.json
/vendor/
/composer.lock
/phpunit.xml
/.phpunit.cache
/.php-cs-fixer.cache
/var
5 changes: 5 additions & 0 deletions src/Image/src/Bridge/Imagor/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CHANGELOG

## 3.7.0

- Bridge added
19 changes: 19 additions & 0 deletions src/Image/src/Bridge/Imagor/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2026-present Fabien Potencier

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
86 changes: 86 additions & 0 deletions src/Image/src/Bridge/Imagor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Symfony UX Image: Imagor

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

[Imagor](https://github.com/cshum/imagor) integration for Symfony UX Image, through transformation parameters in the image server's URL path.

## Installation

Install the bridge using Composer and Symfony Flex:

```shell
composer require symfony/ux-imagor-image
```

## DSN example

```dotenv
UX_IMAGE_DSN=imagor://images.example.com?origin=https%3A%2F%2Fwww.example.com
```

The host is the domain of your Imagor server. With `origin` pointing at your application's HTTP(S) base URL, `src` stays the path your application serves the image from. Without `origin`, `src` is a key for the server's configured loader, such as `bucket/uploads/photo.jpg` for S3. With `origin`, `src` always resolves under it, even when it is an absolute URL.

The DSN supports a port and an optional reverse-proxy path prefix, which the proxy must strip. HTTPS is the default; add `secure=false` for a local HTTP server:

```dotenv
UX_IMAGE_DSN=imagor://localhost:8000?secure=false
```

Pass filenames as unencoded paths and remote URLs as valid URLs, including any existing percent escapes. URL-encode DSN option values.

## Secure URLs

Imagor can require every URL to be signed with a [URL signature](https://docs.imagor.net/security/). Give the server's `IMAGOR_SECRET` as the `sign_key` DSN option:

```dotenv
UX_IMAGE_DSN=imagor://images.example.com?sign_key=your-secret
```

Every generated URL then carries a signature covering the encoded transformation and source path. The default `signer=sha1` and `sign_truncate=0` match stock Imagor. For a server using `IMAGOR_SIGNER_TYPE=sha256` and `IMAGOR_SIGNER_TRUNCATE=40`:

```dotenv
UX_IMAGE_DSN=imagor://images.example.com?sign_key=your-secret&signer=sha256&sign_truncate=40
```

`signer` also accepts `sha512`. `sign_truncate` accepts `0` (no truncation) or 17–88 characters. Without a signing key, URLs use `unsafe` and require `IMAGOR_UNSAFE=true` on the server, intended for local development.

> [!WARNING]
> The key signs every URL: anyone holding it can request transformations from your Imagor server. Keep it out of the files you commit, and store it the way you store your other secrets. Configure allowed sources on the server.

## Parameter mapping

`ImageTransformation` properties are mapped to Imagor's [image endpoint parameters](https://docs.imagor.net/image-endpoint/):

| `ImageTransformation` | Imagor parameter | Notes |
| --------------------- | -------------------- | ---------------------------------------------- |
| `width` | `WIDTHxHEIGHT` | An unspecified width becomes `0` |
| `height` | `WIDTHxHEIGHT` | An unspecified height becomes `0` |
| `fit: Fit::Cover` | _(default)_ | Centered crop to the requested dimensions |
| `fit: Fit::Contain` | `fit-in` | Fits within the dimensions without enlargement |
| `format` | `format(NAME)` | Explicit output format |
| `quality` | `quality(N)` | Output quality |
| `operations` | `filters:NAME(ARGS)` | See below |

## Supported operations

Any of the following keys can be passed through `ImageTransformation::$operations` and are added as filters:

`background_color`, `blur`, `brightness`, `contrast`, `crop`, `dpi`, `fill`, `focal`, `grayscale`, `hue`, `no_upscale`, `page`, `rgb`, `rotate`, `round_corner`, `saturation`, `sharpen`, `strip_exif`, `strip_metadata`.

`grayscale`, `no_upscale`, `strip_exif` and `strip_metadata` take booleans; `false` omits the filter. Other operations take numbers or argument strings, such as `crop: '10,20,200,150'`. Arguments may contain letters, digits, commas, periods, colons, underscores and hyphens.

Filters retain insertion order, after the common quality and format filters. Each filter can appear once; `crop` runs after resizing. See [Imagor's filter reference](https://docs.imagor.net/filters/) for what each one does.

## Supported formats

`avif`, `webp`, `jpeg`, `png`.

Automatic format negotiation depends on Imagor's `IMAGOR_AUTO_*` server configuration, so the bridge does not advertise support for `format: 'auto'`. Use `ux_picture()` to offer browsers multiple explicit formats. If no format is specified, the server chooses its default.

## Resources

- [Documentation](https://symfony.com/bundles/ux-image/current/index.html)
- [Report issues](https://github.com/symfony/ux/issues) and
[send Pull Requests](https://github.com/symfony/ux/pulls)
in the [main Symfony UX repository](https://github.com/symfony/ux)
39 changes: 39 additions & 0 deletions src/Image/src/Bridge/Imagor/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "symfony/ux-imagor-image",
"type": "symfony-ux-image-bridge",
"description": "Symfony UX Image Imagor Bridge",
"keywords": [
"symfony-ux",
"imagor",
"image",
"responsive"
],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.4",
"symfony/ux-image": "^3.7"
},
"require-dev": {
"phpunit/phpunit": "^12.0",
"spatie/phpunit-snapshot-assertions": "^5.2.3"
},
"autoload": {
"psr-4": {
"Symfony\\UX\\Image\\Bridge\\Imagor\\": "src/"
},
"exclude-from-classmap": []
},
"autoload-dev": {
"psr-4": {
"Symfony\\UX\\Image\\Bridge\\Imagor\\Tests\\": "tests/"
}
},
"minimum-stability": "dev"
}
38 changes: 38 additions & 0 deletions src/Image/src/Bridge/Imagor/phpunit.dist.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
colors="true"
bootstrap="tests/bootstrap.php"
failOnDeprecation="true"
failOnRisky="true"
failOnWarning="true"
cacheDirectory=".phpunit.cache"
>
<php>
<ini name="error_reporting" value="-1"/>
<env name="SHELL_VERBOSITY" value="-1"/>
</php>

<testsuites>
<testsuite name="Symfony UX Image Imagor Bridge Test Suite">
<directory>./tests</directory>
</testsuite>
</testsuites>

<source
ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>
<directory>src</directory>
</include>

<deprecationTrigger>
<function>trigger_deprecation</function>
</deprecationTrigger>
</source>
</phpunit>
Loading
Loading