Skip to content
Open
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
6 changes: 6 additions & 0 deletions src/Image/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.php-cs-fixer.dist.php export-ignore
phpunit.dist.xml export-ignore
tests/ export-ignore
Comment on lines +1 to +6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.php-cs-fixer.dist.php export-ignore
phpunit.dist.xml export-ignore
tests/ export-ignore
/.git* export-ignore
/.symfony.bundle.yaml export-ignore
/doc export-ignore
/phpunit.xml.dist export-ignore
/tests export-ignore

6 changes: 6 additions & 0 deletions src/Image/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/vendor/
/composer.lock
/phpunit.xml
/.phpunit.result.cache
.phpunit.cache
/var/
27 changes: 27 additions & 0 deletions src/Image/.php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

use PhpCsFixer\Finder;
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;

return (new PhpCsFixer\Config())
->setParallelConfig(ParallelConfigFactory::detect())
->setRules([
'@Symfony' => true,
'@Symfony:risky' => true,
])
->setRiskyAllowed(true)
->setFinder(
(new Finder())
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
)
;
3 changes: 3 additions & 0 deletions src/Image/.symfony.bundle.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
branches: ['2.x', '3.x']
maintained_branches: ['3.x']
doc_dir: 'doc'
5 changes: 5 additions & 0 deletions src/Image/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CHANGELOG

## 2.32.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## 2.32.0
## 3.1


- Add Symfony UX Image component
Empty file added src/Image/LICENSE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file is empty; you need the associated content.

You can find it on other components.

Empty file.
81 changes: 81 additions & 0 deletions src/Image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Symfony UX Image

Optimized responsive image components with automatic format conversion and Core Web Vitals optimization.

## Installation

```bash
composer require symfony/ux-image
```

## Quick Start

### Basic Usage

```twig
{# Simple responsive image #}
<twig:ux:img
src="/images/hero.jpg"
alt="Hero image"
width="100vw md:80vw"
/>
```

### Art Direction

```twig
{# Different aspect ratios per breakpoint #}
<twig:ux:picture
src="/images/banner.jpg"
alt="Banner"
width="100vw md:80vw"
ratio="sm:1:1 md:16:9"
/>
```

### Without Twig Components

```twig
{{ ux_image('/images/hero.jpg', 'Hero image', { width: '100vw md:80vw' }) }}
```

## Features

- **Automatic WebP conversion** — via configurable providers
- **Responsive srcset/sizes** — viewport-based width syntax (`100vw md:80vw`)
- **Density support** — Retina displays (`densities="x1 x2"`)
- **Art direction** — different crops per breakpoint with `<twig:ux:picture>`
- **Provider-based** — works with local files, CDN, or any image service
- **No hard dependencies** — no image processing library required

## Configuration

```yaml
# config/packages/ux_image.yaml
ux_image:
default_provider: passthrough
breakpoints:
sm: 640
md: 768
lg: 1024
xl: 1280
2xl: 1536
defaults:
format: webp
quality: 80
loading: lazy
fit: cover
```

### Using a CDN

```yaml
ux_image:
providers:
cloudflare:
pattern: '/cdn-cgi/image/w={width},f={format},q={quality}/{src}'
```

## License

MIT License — see [LICENSE](LICENSE) for details.
72 changes: 72 additions & 0 deletions src/Image/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"name": "symfony/ux-image",
"type": "symfony-bundle",
"description": "Optimized responsive image components with automatic format conversion and Core Web Vitals optimization.",
"keywords": [
"symfony-ux",
"symfony",
"image",
"responsive",
"webp",
"optimization",
"core web vitals"
],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Aleksey Razbakov",
"email": "aleksey@razbakov.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"autoload": {
"psr-4": {
"Symfony\\UX\\Image\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Symfony\\UX\\Image\\Tests\\": "tests/"
}
},
"require": {
"php": ">=8.4",
"symfony/framework-bundle": "^7.4|^8.0",
"symfony/twig-bundle": "^7.4|^8.0",
"symfony/config": "^7.4|^8.0",
"symfony/dependency-injection": "^7.4|^8.0",
"symfony/http-kernel": "^7.4|^8.0",
"symfony/options-resolver": "^7.4|^8.0",
"twig/twig": "^3.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.65",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be deleted here.

"phpunit/phpunit": "^11.1|^12.0",
"symfony/browser-kit": "^7.4|^8.0",
"symfony/css-selector": "^7.4|^8.0",
"symfony/debug-bundle": "^7.4|^8.0",
"symfony/phpunit-bridge": "^7.4|^8.0",
"symfony/yaml": "^7.4|^8.0",
"symfony/ux-twig-component": "^2.21|^3.0"
},
"suggest": {
"symfony/ux-twig-component": "For using the Twig component syntax (<twig:ux:img>)"
},
"config": {
"sort-packages": true
},
"conflict": {
"symfony/flex": "<1.13"
},
"extra": {
"thanks": {
"name": "symfony/ux",
"url": "https://github.com/symfony/ux"
}
},
"minimum-stability": "dev"
}
25 changes: 25 additions & 0 deletions src/Image/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
_defaults:
autowire: true
autoconfigure: true

Symfony\UX\Image\Provider\ProviderRegistry:
public: true

Symfony\UX\Image\Service\Transformer:
arguments:
$breakpoints: '%ux_image.breakpoints%'

Symfony\UX\Image\Twig\Img:
tags:
- { name: 'twig.component' }

Symfony\UX\Image\Twig\Picture:
tags:
- { name: 'twig.component' }

# Auto-register providers
Symfony\UX\Image\Provider\ProviderInterface:
abstract: true
tags:
- { name: 'ux_image.provider' }
19 changes: 19 additions & 0 deletions src/Image/phpunit.dist.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review the file to make it look something like this: https://github.com/symfony/ux/blob/3.x/src/Native/phpunit.xml.dist.

<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
failOnRisky="true"
failOnWarning="true"
>
<testsuites>
<testsuite name="Symfony UX Image Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>
42 changes: 42 additions & 0 deletions src/Image/src/DependencyInjection/Compiler/ProviderPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Image\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\UX\Image\Provider\ProviderRegistry;

/**
* Registers all tagged providers into the ProviderRegistry.
*
* @author Aleksey Razbakov <aleksey@razbakov.com>
*/
class ProviderPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition(ProviderRegistry::class)) {
return;
}

$registry = $container->getDefinition(ProviderRegistry::class);
$taggedServices = $container->findTaggedServiceIds('ux_image.provider');

foreach ($taggedServices as $id => $tags) {
foreach ($tags as $attributes) {
$name = $attributes['name'] ?? $id;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$name is unused.

$registry->addMethodCall('addProvider', [new Reference($id)]);
}
}
}
}
74 changes: 74 additions & 0 deletions src/Image/src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\UX\Image\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

/**
* @author Aleksey Razbakov <aleksey@razbakov.com>
*/
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('ux_image');
$rootNode = $treeBuilder->getRootNode();

$rootNode
->children()
->scalarNode('default_provider')
->defaultValue('passthrough')
->info('The default provider to use when none is specified.')
->end()
->arrayNode('providers')
->useAttributeAsKey('name')
->arrayPrototype()
->children()
->scalarNode('pattern')->end()
->scalarNode('assets_path')->end()
->arrayNode('config')
->useAttributeAsKey('key')
->scalarPrototype()->end()
->end()
->end()
->end()
->end()
->arrayNode('breakpoints')
->useAttributeAsKey('name')
->integerPrototype()->end()
->defaultValue([
'sm' => 640,
'md' => 768,
'lg' => 1024,
'xl' => 1280,
'2xl' => 1536,
])
->end()
->arrayNode('defaults')
->addDefaultsIfNotSet()
->children()
->scalarNode('format')->defaultValue('webp')->end()
->integerNode('quality')->defaultValue(80)->end()
->scalarNode('loading')->defaultValue('lazy')->end()
->scalarNode('fit')->defaultValue('cover')->end()
->scalarNode('focal')->defaultValue('center')->end()
->scalarNode('fallback')->defaultValue('lg')->end()
->scalarNode('fallback_format')->defaultValue('auto')->end()
->end()
->end()
->end()
;

return $treeBuilder;
}
}
Loading
Loading