PHPackages                             silarhi/picasso-bundle - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Image &amp; Media](/categories/media)
4. /
5. silarhi/picasso-bundle

ActiveSymfony-bundle[Image &amp; Media](/categories/media)

silarhi/picasso-bundle
======================

Responsive image component for Symfony, inspired by next/image

v1.3.0(1w ago)015.8k[1 issues](https://github.com/silarhi/picasso-bundle/issues)[1 PRs](https://github.com/silarhi/picasso-bundle/pulls)MITPHPPHP &gt;=8.2CI passing

Since Apr 11Pushed 1w agoCompare

[ Source](https://github.com/silarhi/picasso-bundle)[ Packagist](https://packagist.org/packages/silarhi/picasso-bundle)[ RSS](/packages/silarhi-picasso-bundle/feed)WikiDiscussions main Synced today

READMEChangelog (5)Dependencies (61)Versions (23)Used By (0)

   ![Latest Stable Version](https://camo.githubusercontent.com/fdf461a4abded0e775a69b71fc274e0ff2867a8a9407dd2fee5530e29b346096/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f73696c617268692f7069636173736f2d62756e646c653f7374796c653d666f722d7468652d6261646765266c6162656c3d737461626c6526636f6c6f723d306436656664)    ![Total Downloads](https://camo.githubusercontent.com/2807d7d4a65f8960cba3a9ccc8181a236559e8112c0ed5257e4d9d0be6a67a16/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f73696c617268692f7069636173736f2d62756e646c653f7374796c653d666f722d7468652d626164676526636f6c6f723d313938373534)    ![License](https://camo.githubusercontent.com/eeccdfb0995769efd05ea69080f1337c725ee2972f0c48b4cecfe9af2533693c/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f73696c617268692f7069636173736f2d62756e646c653f7374796c653d666f722d7468652d626164676526636f6c6f723d366634326331)    ![PHP Version](https://camo.githubusercontent.com/686a9165e7d6b932cdc4ae542c7991982350584c5551bd7d4f258b3341a41baf/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f7068702d762f73696c617268692f7069636173736f2d62756e646c653f7374796c653d666f722d7468652d626164676526636f6c6f723d373737626234)    ![CI Status](https://camo.githubusercontent.com/b1f0ebe7528c736ce0fe5b54ff286ff2ecbf47b116340620ce0da1c4a040d32c/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f73696c617268692f7069636173736f2d62756e646c652f636f6e74696e756f75732d696e746567726174696f6e2e796d6c3f7374796c653d666f722d7468652d6261646765266c6162656c3d434926636f6c6f723d323063393937)

PicassoBundle
=============

[](#picassobundle)

 **The missing image component for Symfony.**
 Inspired by [Next.js Image](https://nextjs.org/docs/app/api-reference/components/image) — built for the Symfony ecosystem.

 Write one line of Twig. Get AVIF, WebP, responsive srcset, blur placeholders, and lazy loading — automatically.

---

### Before PicassoBundle

[](#before-picassobundle)

```

```

### After PicassoBundle

[](#after-picassobundle)

```

```

> Same output. Zero boilerplate. All formats, srcsets, and placeholders generated automatically.

---

Why PicassoBundle?
------------------

[](#why-picassobundle)

Images account for the largest share of page weight on most websites. Serving them correctly — with modern formats, responsive srcsets, proper lazy loading, and blur placeholders — is critical for both **Core Web Vitals** and **user experience**, but the implementation is tedious and error-prone.

PicassoBundle solves this the same way Next.js Image did for React: **a single component that handles everything**.

Without PicassoBundleWith PicassoBundle**Format negotiation**Manual AVIF/WebP/JPEG `` tagsAutomatic from config**Responsive srcset**Hand-crafted per breakpointGenerated from `sizes` prop**Blur placeholders**DIY or skip itBuilt-in (LQIP, BlurHash, or custom)**Dimension detection**Hardcoded or forgottenAuto-detected from image stream**LCP optimization**Manually set loading/fetchpriorityOne `priority` prop**Image sources**Filesystem onlyFilesystem, S3, Flysystem, Vich, URL**CDN support**Build your own integrationImgix out of the box, or plug in any CDN---

Table of Contents
-----------------

[](#table-of-contents)

- [Why PicassoBundle?](#why-picassobundle)
- [Features](#features)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
    - [Minimal Configuration](#minimal-configuration)
    - [Full Configuration Reference](#full-configuration-reference)
    - [Configuration Options Explained](#configuration-options-explained)
- [Usage](#usage)
    - [Twig Component](#twig-component-recommended)
    - [Twig Function](#twig-function)
    - [ImageHelper Service](#imagehelper-service)
- [Placeholders](#placeholders)
    - [Transformer Placeholder (LQIP)](#transformer-placeholder-lqip)
    - [BlurHash Placeholder](#blurhash-placeholder)
    - [Custom Placeholder Service](#custom-placeholder-service)
    - [Controlling Placeholders Per Image](#controlling-placeholders-per-image)
- [Priority Images](#priority-images)
- [Loaders](#loaders)
    - [Filesystem Loader](#filesystem-loader)
    - [Flysystem Loader](#flysystem-loader)
    - [VichUploaderBundle Loader](#vichuploaderbundle-loader)
    - [URL Loader](#url-loader)
    - [Custom Loader](#custom-loader)
- [Transformers](#transformers)
    - [Glide (Local)](#glide-local)
    - [Imgix (CDN)](#imgix-cdn)
    - [Custom Transformer](#custom-transformer)
- [Routes](#routes)
- [Cache Purge](#cache-purge)
- [How It Works](#how-it-works)
- [Testing &amp; Quality](#testing--quality)
- [Contributing](#contributing)
- [License](#license)

---

Features
--------

[](#features)

- **One component, full optimization** — `` renders a complete `` with AVIF, WebP, and JPEG sources
- **Automatic responsive srcset** — generates width descriptors for all configured breakpoints, no manual work
- **Blur placeholders** — built-in LQIP and [BlurHash](https://blurha.sh/) support for instant perceived loading
- **Smart dimension detection** — reads image dimensions from the stream automatically, preserves aspect ratio
- **Priority images** — one prop for `loading="eager"` + `fetchpriority="high"` (LCP optimization)
- **Multiple image sources** — Local filesystem, [Flysystem](https://flysystem.thephpleague.com/) (S3, GCS, Azure), [VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle), remote URLs
- **Local or CDN transforms** — [Glide](https://glide.thephpleague.com/) for self-hosted, [Imgix](https://imgix.com/) for CDN, or bring your own
- **Signed URLs** — HMAC-signed transformation URLs prevent abuse
- **PSR-6 metadata caching** — dimension detection and BlurHash results cached
- **Fully extensible** — add custom loaders, transformers, or placeholders with PHP attributes (`#[AsImageLoader]`, `#[AsImageTransformer]`, `#[AsPlaceholder]`)

Requirements
------------

[](#requirements)

DependencyVersionPHP8.2+Symfony6.4 / 7.0 / 8.0[Symfony UX Twig Component](https://symfony.com/bundles/ux-twig-component/current/index.html)2.13+### Optional Dependencies

[](#optional-dependencies)

PackageRequired for`league/glide` + `league/glide-symfony`Glide transformer (local image processing)`kornrunner/blurhash` + `imagine/imagine`BlurHash placeholder`league/flysystem-bundle`Flysystem loader`vich/uploader-bundle`VichUploader loader`symfony/http-client`URL loaderInstallation
------------

[](#installation)

```
composer require silarhi/picasso-bundle
```

If not using Symfony Flex, register the bundle in `config/bundles.php`:

```
return [
    // ...
    Silarhi\PicassoBundle\PicassoBundle::class => ['all' => true],
];
```

Install a transformer — at least one is required:

```
# Option A: Glide (local image transformation)
composer require league/glide league/glide-symfony

# Option B: Imgix (CDN-based transformation)
# No extra package needed, just configure your Imgix base URL
```

Quick Start
-----------

[](#quick-start)

**1. Configure** a loader and a transformer:

```
# config/packages/picasso.yaml
picasso:
    loaders:
        filesystem:
            paths:
                - '%kernel.project_dir%/public/uploads'
    transformers:
        glide:
            sign_key: '%env(PICASSO_SIGN_KEY)%'
```

**2. Import the routes** (required for Glide local serving):

```
# config/routes/picasso.yaml
picasso:
    resource: '@PicassoBundle/config/routes.php'
```

**3. Use the Twig component** in your templates:

```

```

This renders a `` element with `` tags for AVIF and WebP, a fallback `` with JPEG srcset, and an inline blur placeholder — all automatically.

Configuration
-------------

[](#configuration)

### Minimal Configuration

[](#minimal-configuration)

When only one loader and one transformer are configured, they are automatically used as defaults — no need to set `default_loader` or `default_transformer`.

```
picasso:
    loaders:
        filesystem:
            paths:
                - '%kernel.project_dir%/public/uploads'
    transformers:
        glide:
            sign_key: '%env(PICASSO_SIGN_KEY)%'
```

### Full Configuration Reference

[](#full-configuration-reference)

```
picasso:
    # --- Defaults (auto-detected when only one of each type is configured) ---
    default_loader: ~
    default_transformer: ~
    default_placeholder: ~

    # --- Responsive breakpoints ---
    device_sizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840]
    image_sizes: [16, 32, 48, 64, 96, 128, 256, 384]

    # --- Output formats (last entry is the  fallback) ---
    formats: [avif, webp, jpg]

    # --- Image quality & fit ---
    default_quality: 75 # 1–100
    default_fit: contain # contain | cover | crop | fill

    # --- Metadata resolution ---
    resolve_metadata: false # Whether to auto-detect image dimensions from source (default: false)

    # --- Metadata cache ---
    cache: true # true = cache.app, false = disabled, or a PSR-6 service ID

    # --- Web profiler data collector (dev only) ---
    collector: false # set to true to record image renders / URL generations in the Symfony toolbar

    # --- Placeholders ---
    placeholders:
        blur:
            type: transformer # inferred from key name when matching a known type
            size: 10 # tiny image width/height in px
            blur: 5 # blur radius
            quality: 30 # JPEG quality for blur image (1–100)

        # blurhash:
        #     type: blurhash
        #     components_x: 4    # horizontal components (1–9)
        #     components_y: 3    # vertical components (1–9)
        #     size: 32           # decoded placeholder image size in px
        #     driver: gd         # gd | imagick

        # my_placeholder:
        #     type: service
        #     service: 'App\Image\MyPlaceholder'

    # --- Loaders ---
    loaders:
        filesystem:
            type: filesystem # inferred from key name
            paths:
                - '%kernel.project_dir%/public/uploads'
            # resolve_metadata: ~  # auto-set to true for filesystem loaders

        # my_flysystem:
        #     type: flysystem
        #     storage: 'default.storage'
        #     resolve_metadata: ~  # inherits from global (false)

        # vich:
        #     type: vich

        # url:
        #     type: url
        #     http_client: ~       # optional: custom PSR-18 HTTP client service ID
        #     request_factory: ~   # optional: custom PSR-17 request factory service ID
        #     resolve_metadata: ~  # inherits from global (false)

    # --- Transformers ---
    transformers:
        glide:
            type: glide # inferred from key name
            sign_key: ~ # signing key for secure URLs
            cache: '%kernel.project_dir%/var/glide-cache' # local path OR a Flysystem storage name (e.g. 'thumbs.storage')
            driver: gd # gd | imagick
            max_image_size: ~ # optional max pixel count
            public_cache:
                enabled: false # serve transformed images from public directory

        # imgix:
        #     type: imgix
        #     base_url: ~      # e.g. https://my-source.imgix.net
        #     sign_key: ~      # optional signing key
        #     api_key: ~       # optional API key for cache purge
        #     http_client: ~   # PSR-18 HTTP client service ID for purge
        #     request_factory: ~ # PSR-17 request factory service ID for purge
        #     stream_factory: ~  # PSR-17 stream factory service ID for purge

        # my_transformer:
        #     type: service
        #     service: 'App\Image\MyTransformer'
```

### Configuration Options Explained

[](#configuration-options-explained)

#### `device_sizes` and `image_sizes`

[](#device_sizes-and-image_sizes)

These arrays define which widths are generated in the srcset attribute:

- **`device_sizes`** — Breakpoint widths for responsive (fluid) images. When the component has a `sizes` attribute, all device and image sizes are merged and included in the srcset.
- **`image_sizes`** — Smaller widths for fixed-size images (icons, thumbnails). When no `sizes` attribute is provided, srcset includes only `1x` and `2x` descriptors based on the specified `width`.

#### `formats`

[](#formats)

The list of output formats. A `` element is generated for each format except the last one, which is used as the `` fallback. The default `[avif, webp, jpg]` produces:

```

```

Supported formats: `avif`, `webp`, `jpg`, `jpeg`, `pjpg`, `png`, `gif`.

#### `default_fit`

[](#default_fit)

Controls how images are resized within the target dimensions:

FitDescription`contain`Scales down to fit within the box, preserving aspect ratio (default)`cover`Scales to fill the box, cropping excess`crop`Crops to exact dimensions`fill`Stretches to fill the box exactly#### `resolve_metadata`

[](#resolve_metadata)

Controls whether the bundle reads image streams to auto-detect dimensions (width/height).

- `false` (default) — dimensions are not auto-detected; provide them explicitly or accept no `width`/`height` in the HTML
- `true` — enables auto-detection via the `MetadataGuesser`

This can also be set **per-loader** (filesystem loaders default to `true`) and overridden at **runtime** with the `resolveMetadata` component prop or `imageData()` parameter.

Detection reads the stream progressively (64KB initially, doubling up to 2MB), so dimensions are found even in files whose headers sit behind large embedded EXIF/ICC/XMP segments. SVG is not supported (`getimagesize()` cannot parse XML); pass explicit dimensions or use `unoptimized` for SVG sources.

#### `cache`

[](#cache)

Configures PSR-6 caching for metadata detection (image dimensions) and BlurHash encoding:

- `true` (default) — uses the `cache.app` service
- `false` — disables caching
- `'my_cache_pool'` — uses a custom PSR-6 cache pool service ID

> **Tip:** The `type` option for loaders, transformers, and placeholders is automatically inferred from the key name when it matches a known type (`filesystem`, `flysystem`, `vich`, `url`, `glide`, `imgix`, `transformer`, `blurhash`). Use `type` explicitly only when your key name differs from the type.

Usage
-----

[](#usage)

### Twig Component (recommended)

[](#twig-component-recommended)

The `` component renders a responsive `` element with `` tags for each configured format and a fallback ``with a full srcset.

```

```

#### Component Properties

[](#component-properties)

PropertyTypeDefaultDescription`src``string`—Image path relative to the loader's base`width``int`autoDisplay width (auto-detected from source)`height``int`autoDisplay height (auto-detected from source)`sizes``string`—Responsive `sizes` attribute`sourceWidth``int`autoExplicit source width (skips detection)`sourceHeight``int`autoExplicit source height (skips detection)`loader``string`—Override default loader`transformer``string`—Override default transformer`quality``int`75Override quality (1–100)`fit``string`containFit mode: `contain`, `cover`, `crop`, `fill``placeholder``string|bool`—`true`/`false` to enable/disable, or a placeholder name`placeholderData``string`—Literal data URI, bypasses placeholder services`priority``bool`falseEager loading, `fetchpriority="high"`, no placeholder`loading``string`lazy`lazy` or `eager`. Auto-set when priority`fetchPriority``string`—`high`, `low`, `auto`. Auto-set when priority`unoptimized``bool`falseServe original image without transformation`resolveMetadata``bool`—Override metadata resolution (see [below](#metadata-resolution))`context``array``[]`Extra context for the loader (e.g. Vich)#### Automatic Dimension Detection

[](#automatic-dimension-detection)

When `width` and `height` are not provided, PicassoBundle can detect them from the image stream. You can also provide `sourceWidth`and `sourceHeight` to skip detection entirely, which is useful for performance when you already know the image dimensions:

```
{# Auto-detected dimensions (requires resolve_metadata enabled) #}

{# Explicit source dimensions (skips stream detection) #}

```

The component also preserves aspect ratio when only one display dimension is provided:

```
{# height is calculated automatically from the source aspect ratio #}

```

#### Metadata Resolution

[](#metadata-resolution)

To reduce **Cumulative Layout Shift (CLS)**, `width` and `height`attributes are only rendered in the HTML when **both** are available. If only one dimension is provided and the other cannot be resolved, neither is output — preventing the browser from reserving incorrect space.

Metadata resolution (reading the image stream to detect dimensions) is controlled at three levels, with this precedence: **runtime &gt; per-loader &gt; global**.

LevelOptionDefaultGlobal`resolve_metadata``false`Per-loader`resolve_metadata``null` (inherit global); `true` for filesystemRuntime`resolveMetadata``null` (inherit per-loader/global)```
{# Force metadata resolution for this image, regardless of config #}

{# Disable metadata resolution for this image #}

```

Filesystem loaders default to `resolve_metadata: true` because reading local files is cheap. For remote loaders (URL, Flysystem with remote backends), it defaults to `false` to avoid unnecessary network requests.

#### Extra HTML Attributes

[](#extra-html-attributes)

The component forwards any extra attributes to the inner `` tag:

```

```

#### Unoptimized Mode

[](#unoptimized-mode)

Use `unoptimized` to serve the image as-is, without any transformation. The `src` value is passed directly to the `` tag:

```

```

### Twig Function

[](#twig-function)

Two Twig functions are available:

- **`picasso_image()`** — renders a full responsive `` element (same output as the `` component) for consumers who don't want to install `symfony/ux-twig-component`.
- **`picasso_image_url()`** — generates a single transformed image URL.

#### `picasso_image()`

[](#picasso_image)

Renders a complete responsive `` element with all configured formats, srcsets, placeholder, and `` fallback. It accepts the same named arguments as the `` Twig component.

```
{# Same output as  #}
{{ picasso_image(
    src='photo.jpg',
    width=800,
    height=600,
    sizes='100vw',
    attributes={alt: 'A photo', class: 'rounded shadow-lg'}
) }}
```

Extra HTML attributes (`alt`, `class`, `id`, `data-*`, …) are passed via the `attributes` named argument and forwarded to the inner `` tag.

All available parameters mirror the `` component: `src`, `width`, `height`, `sizes`, `sourceWidth`, `sourceHeight`, `loader`, `transformer`, `quality`, `fit`, `placeholder`, `placeholderData`, `priority`, `loading`, `fetchPriority`, `unoptimized`, `resolveMetadata`, `context`, `attributes`.

See [Component Properties](#component-properties) for the full reference.

#### `picasso_image_url()`

[](#picasso_image_url)

Generates a single transformed image URL. Useful for backgrounds, meta tags, Open Graph images, or anywhere you need a plain URL.

```
{# Simple thumbnail #}

{# Open Graph meta tag #}

{# CSS background image #}

```

All available parameters:

```
{{ picasso_image_url(
    'photo.jpg',
    width: 800,
    height: 600,
    format: 'webp',
    quality: 85,
    fit: 'cover',
    blur: 10,
    dpr: 2,
    loader: 'vich',
    transformer: 'imgix',
    context: { entity: product, field: 'imageFile' }
) }}
```

ParameterTypeDescription`width``int`Target width in pixels`height``int`Target height in pixels`format``string`Output format (`avif`, `webp`, `jpg`, etc.)`quality``int`Output quality (1–100)`fit``string`Fit mode (`contain`, `cover`, `crop`, `fill`)`blur``int`Blur radius`dpr``int`Device pixel ratio`loader``string`Override default loader`transformer``string`Override default transformer`context``array`Extra context for the loader### ImageHelper Service

[](#imagehelper-service)

The `picasso_image_url()` Twig function delegates to `ImageHelperInterface`, which you can also inject directly in your PHP code:

```
use Silarhi\PicassoBundle\Service\ImageHelperInterface;

class MyController
{
    public function __construct(private ImageHelperInterface $imageHelper) {}

    public function index(): Response
    {
        $url = $this->imageHelper->imageUrl(
            path: 'photo.jpg',
            width: 300,
            format: 'webp',
        );

        // ...
    }
}
```

#### Image Data for JSON APIs

[](#image-data-for-json-apis)

The `imageData()` method returns an `ImageRenderData` DTO containing all rendering data (sources, srcset, placeholder, dimensions, loading attributes) plus the resolved loader, transformer and placeholder names. It implements `JsonSerializable`, making it ideal for headless / API-driven frontends (React, Vue, mobile apps, etc.):

```
use Silarhi\PicassoBundle\Service\ImageHelperInterface;
use Symfony\Component\HttpFoundation\JsonResponse;

class ImageApiController
{
    public function __construct(private ImageHelperInterface $imageHelper) {}

    public function show(): JsonResponse
    {
        $data = $this->imageHelper->imageData(
            src: 'hero.jpg',
            width: 1200,
            height: 800,
            sizes: '100vw',
            placeholder: true,
        );

        return new JsonResponse($data);
    }
}
```

The JSON response contains everything a frontend needs to render a responsive `` element:

```
{
    "fallbackSrc": "/image/glide/filesystem/hero.jpg?w=1200&h=800&fm=jpg&s=...",
    "fallbackSrcset": "/image/glide/.../hero.jpg?w=640&fm=jpg&s=... 640w, ... 1920w",
    "sources": [
        { "type": "image/avif", "srcset": "..." },
        { "type": "image/webp", "srcset": "..." }
    ],
    "placeholderUri": "data:image/jpeg;base64,...",
    "width": 1200,
    "height": 800,
    "loading": "lazy",
    "fetchPriority": null,
    "sizes": "100vw",
    "unoptimized": false,
    "attributes": {},
    "loader": "filesystem",
    "transformer": "glide",
    "placeholder": "blur"
}
```

`imageData()` accepts the same parameters as the `` Twig component (`src`, `width`, `height`, `sizes`, `quality`, `fit`, `placeholder`, `priority`, `loader`, `transformer`, etc.).

Placeholders
------------

[](#placeholders)

Placeholders generate a low-quality preview displayed while the full image loads. The placeholder is inlined as a CSS `background-image` on the `` tag and automatically removed via an `onload` handler once the full image has loaded.

### Transformer Placeholder (LQIP)

[](#transformer-placeholder-lqip)

The transformer placeholder generates a tiny blurred version of the image using your configured transformer (Glide or Imgix). This is the simplest placeholder to set up — it requires no extra dependencies.

```
picasso:
    default_placeholder: blur
    placeholders:
        blur:
            type: transformer
            size: 10 # tiny image width/height in px
            blur: 5 # blur radius
            quality: 30 # JPEG quality (1–100)
```

### BlurHash Placeholder

[](#blurhash-placeholder)

The BlurHash placeholder encodes the image as a [BlurHash](https://blurha.sh/) string and decodes it to a tiny PNG data URI. This produces a smooth gradient-like preview that is very small (around 20–30 bytes as a hash).

```
composer require kornrunner/blurhash imagine/imagine
```

```
picasso:
    default_placeholder: blurhash
    placeholders:
        blurhash:
            type: blurhash
            components_x: 4 # horizontal components (1–9, higher = more detail)
            components_y: 3 # vertical components (1–9, higher = more detail)
            size: 32 # decoded placeholder image size in px
            driver: gd # gd | imagick
```

### Custom Placeholder Service

[](#custom-placeholder-service)

You can create your own placeholder by implementing `PlaceholderInterface`:

```
use Silarhi\PicassoBundle\Attribute\AsPlaceholder;
use Silarhi\PicassoBundle\Placeholder\PlaceholderInterface;
use Silarhi\PicassoBundle\Dto\Image;
use Silarhi\PicassoBundle\Dto\ImageTransformation;

#[AsPlaceholder('thumbhash')]
class ThumbHashPlaceholder implements PlaceholderInterface
{
    public function generate(Image $image, ImageTransformation $transformation, array $context = []): string
    {
        // Generate and return a data URI
        return 'data:image/png;base64,...';
    }
}
```

Or register it via configuration:

```
picasso:
    default_placeholder: thumbhash
    placeholders:
        thumbhash:
            type: service
            service: 'App\Image\ThumbHashPlaceholder'
```

### Controlling Placeholders Per Image

[](#controlling-placeholders-per-image)

```
{# Uses the default placeholder from config #}

{# Disable placeholder for this image #}

{# Select a specific named placeholder #}

{# Pass a literal data URI directly (bypasses all placeholder services) #}

```

Priority Images
---------------

[](#priority-images)

For above-the-fold images (hero banners, LCP images), use the `priority` prop. This sets `loading="eager"`, `fetchpriority="high"`, and disables the blur placeholder for optimal Largest Contentful Paint (LCP) performance:

```

```

> **Note:** Placeholders are automatically disabled when `priority` is `true`, since priority images should load immediately without showing a placeholder first.

Loaders
-------

[](#loaders)

Loaders fetch image data from a source. Each loader implements `ImageLoaderInterface` and is registered by name.

### Filesystem Loader

[](#filesystem-loader)

Reads images from local directories. Supports multiple paths (searched in order).

```
picasso:
    loaders:
        filesystem:
            paths:
                - '%kernel.project_dir%/public/uploads'
                - '%kernel.project_dir%/assets/images'
```

```

```

### Flysystem Loader

[](#flysystem-loader)

Reads images via a [Flysystem](https://flysystem.thephpleague.com/) storage, supporting S3, GCS, Azure, and more.

```
composer require league/flysystem-bundle
```

```
picasso:
    loaders:
        my_s3:
            type: flysystem
            storage: 'default.storage' # your Flysystem service ID
```

```

```

### VichUploaderBundle Loader

[](#vichuploaderbundle-loader)

Loads images managed by [VichUploaderBundle](https://github.com/dustin10/VichUploaderBundle).

```
composer require vich/uploader-bundle
```

```
picasso:
    loaders:
        vich: ~ # type inferred from key name
```

```

```

The `context` must include the `entity` (the Doctrine entity instance) and `field` (the VichUploader mapping field name).

### URL Loader

[](#url-loader)

Loads and transforms remote images by URL. Requires a PSR-18 HTTP client.

```
composer require symfony/http-client
```

```
picasso:
    loaders:
        url: ~ # type inferred from key name
```

```

```

### Custom Loader

[](#custom-loader)

Create a custom loader by implementing `ImageLoaderInterface` and tagging it with `#[AsImageLoader]`:

```
use Silarhi\PicassoBundle\Attribute\AsImageLoader;
use Silarhi\PicassoBundle\Loader\ImageLoaderInterface;
use Silarhi\PicassoBundle\Dto\Image;
use Silarhi\PicassoBundle\Dto\ImageReference;

#[AsImageLoader('s3')]
class S3Loader implements ImageLoaderInterface
{
    public function load(ImageReference $reference, bool $withMetadata = false): Image
    {
        // Fetch from S3, return an Image DTO
    }
}
```

If your loader provides direct filesystem access for local transformers (like Glide), implement `ServableLoaderInterface` instead.

Transformers
------------

[](#transformers)

Transformers generate URLs for on-demand image transformation.

### Glide (Local)

[](#glide-local)

[Glide](https://glide.thephpleague.com/) processes images locally using GD or Imagick.

```
composer require league/glide league/glide-symfony
```

```
picasso:
    transformers:
        glide:
            sign_key: '%env(PICASSO_SIGN_KEY)%'
            cache: '%kernel.project_dir%/var/glide-cache'
            driver: gd # gd | imagick
            max_image_size: ~ # optional: max pixel count (width x height)
            public_cache:
                enabled: false # serve from public dir for better performance
```

> **Important:** When using Glide, you must [import the bundle routes](#routes) so that the image controller can serve transformed images.

#### Storing the Glide cache on Flysystem

[](#storing-the-glide-cache-on-flysystem)

The `cache` option is polymorphic: pass a local path *or* the name of a [Flysystem bundle](https://github.com/thephpleague/flysystem-bundle) storage. The bundle resolves the name at boot via its internal `FlysystemRegistry` (the same one used by the Vich loader) — if no storage matches, the value is treated as a path. This lets you keep sources and derivatives on different Flysystem instances:

```
# config/packages/flysystem.yaml
flysystem:
    storages:
        sources.storage:
            adapter: 'aws'
            options:
                client: 'aws_s3_client'
                bucket: 'my-originals'
        thumbs.storage:
            adapter: 'aws'
            options:
                client: 'aws_s3_client'
                bucket: 'my-thumbs'
```

```
# config/packages/picasso.yaml
picasso:
    loaders:
        flysystem:
            storage: 'sources.storage' # source images
    transformers:
        glide:
            sign_key: '%env(PICASSO_SIGN_KEY)%'
            cache: 'thumbs.storage' # rendered cache on a different Flysystem
            driver: gd
```

> **Note:** `public_cache: enabled: true` writes rendered files at a path your web server is expected to serve directly. Combined with a remote Flysystem cache, that only works if the underlying bucket is publicly served at the matching URL prefix — otherwise leave `public_cache` disabled and let the bundle's controller stream the cached file.

### Imgix (CDN)

[](#imgix-cdn)

[Imgix](https://imgix.com/) processes images via their CDN. No local processing is needed.

```
picasso:
    transformers:
        imgix:
            base_url: 'https://my-source.imgix.net'
            sign_key: '%env(IMGIX_SIGN_KEY)%' # optional
            api_key: '%env(IMGIX_API_KEY)%' # optional, enables cache purge
```

### Custom Transformer

[](#custom-transformer)

Create a custom transformer by implementing `ImageTransformerInterface`:

```
use Silarhi\PicassoBundle\Attribute\AsImageTransformer;
use Silarhi\PicassoBundle\Transformer\ImageTransformerInterface;
use Silarhi\PicassoBundle\Dto\Image;
use Silarhi\PicassoBundle\Dto\ImageTransformation;

#[AsImageTransformer('cloudinary')]
class CloudinaryTransformer implements ImageTransformerInterface
{
    public function url(Image $image, ImageTransformation $transformation, array $context = []): string
    {
        // Build and return a Cloudinary URL
    }
}
```

Or register it via configuration:

```
picasso:
    transformers:
        cloudinary:
            type: service
            service: 'App\Image\CloudinaryTransformer'
```

Routes
------

[](#routes)

The bundle registers a route for on-demand image transformation (used by Glide and other local transformers):

```
GET /image/{transformer}/{loader}/{path}

```

Import the routes in your application:

```
# config/routes/picasso.yaml
picasso:
    resource: '@PicassoBundle/config/routes.php'
```

> **Note:** Routes are only required when using a local transformer like Glide. CDN-based transformers (Imgix) generate external URLs and do not need this route.

Cache Purge
-----------

[](#cache-purge)

Both built-in transformers support purging cached image variants via the `PurgableTransformerInterface`.

### Glide

[](#glide)

Glide cache is purged automatically — no extra configuration needed. In standard mode, `Server::deleteCache()` removes all cached variants. In public cache mode, the bundle deletes the cache directory for the specific transformer/loader/path combination.

### Imgix

[](#imgix)

Imgix purge requires an API key and a PSR-18 HTTP client:

```
picasso:
    transformers:
        imgix:
            type: imgix
            base_url: 'https://my-source.imgix.net'
            api_key: '%env(IMGIX_API_KEY)%'
            # Optional: defaults to psr18.http_client for all three
            # http_client: 'psr18.http_client'
            # request_factory: 'psr18.http_client'
            # stream_factory: 'psr18.http_client'
```

### Programmatic Usage

[](#programmatic-usage)

Use the `ImagePipeline` service to purge from your code:

```
use Silarhi\PicassoBundle\Service\ImagePipeline;

class ImageManager
{
    public function __construct(private ImagePipeline $pipeline) {}

    public function deleteImage(string $path): void
    {
        // Purge all cached variants
        $this->pipeline->purge($path);

        // Or specify loader/transformer explicitly
        $this->pipeline->purge($path, loader: 'filesystem', transformer: 'glide');
    }
}
```

You can also use the `PurgableTransformerInterface` directly:

```
use Silarhi\PicassoBundle\Transformer\PurgableTransformerInterface;

if ($transformer instanceof PurgableTransformerInterface) {
    $transformer->purge($path, ['loader' => 'filesystem', 'transformer' => 'glide']);
}
```

How It Works
------------

[](#how-it-works)

When you use ``, the component:

1. **Loads** the image metadata via the configured loader (filesystem, Flysystem, Vich, URL)
2. **Detects dimensions** from the image stream (or uses explicitly provided values)
3. **Generates srcset** entries for each configured format at all responsive breakpoints
4. **Generates a placeholder** (if configured) — a tiny blurred image inlined as a CSS background
5. **Renders** a `` element with `` tags per format and a fallback ``

The generated HTML follows modern best practices:

- `` elements for modern formats (AVIF, WebP) with automatic MIME type detection
- Full `srcset` with width descriptors for responsive loading
- `sizes` attribute for accurate viewport-based selection
- `loading="lazy"` by default for below-the-fold images
- Blur placeholder with CSS `background-image` and `onload` cleanup

Testing &amp; Quality
---------------------

[](#testing--quality)

```
# Install dependencies
composer install

# Run tests
vendor/bin/phpunit

# Static analysis (level: max)
vendor/bin/phpstan analyse

# Code style check
vendor/bin/php-cs-fixer fix --dry-run --diff

# Code style fix
vendor/bin/php-cs-fixer fix

# Twig code style
vendor/bin/twig-cs-fixer lint

# Code modernization check
vendor/bin/rector process --dry-run
```

Contributing
------------

[](#contributing)

Contributions are welcome! Please make sure your changes pass all quality checks before submitting a pull request:

```
vendor/bin/phpunit && vendor/bin/phpstan analyse && vendor/bin/php-cs-fixer fix --dry-run --diff
```

License
-------

[](#license)

MIT License. See [LICENSE](LICENSE) for details.

---

 Built with care by [SILARHI](https://github.com/silarhi).
 If PicassoBundle saves you time, consider giving it a star on GitHub.

###  Health Score

51

—

FairBetter than 95% of packages

Maintenance98

Actively maintained with recent releases

Popularity27

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity56

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 82.6% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~18 days

Total

5

Last Release

11d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/5052984?v=4)[Guillaume Sainthillier](/maintainers/guillaume-sainthillier)[@guillaume-sainthillier](https://github.com/guillaume-sainthillier)

---

Top Contributors

[![guillaume-sainthillier](https://avatars.githubusercontent.com/u/5052984?v=4)](https://github.com/guillaume-sainthillier "guillaume-sainthillier (57 commits)")[![renovate[bot]](https://avatars.githubusercontent.com/in/2740?v=4)](https://github.com/renovate[bot] "renovate[bot] (10 commits)")[![claude](https://avatars.githubusercontent.com/u/81847?v=4)](https://github.com/claude "claude (2 commits)")

---

Tags

imagephpsymfony

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/silarhi-picasso-bundle/health.svg)

```
[![Health](https://phpackages.com/badges/silarhi-picasso-bundle/health.svg)](https://phpackages.com/packages/silarhi-picasso-bundle)
```

###  Alternatives

[easycorp/easyadmin-bundle

Admin generator for Symfony applications

4.3k17.9M388](/packages/easycorp-easyadmin-bundle)[sylius/sylius

E-Commerce platform for PHP, based on Symfony framework.

8.5k5.9M738](/packages/sylius-sylius)[prestashop/prestashop

PrestaShop is an Open Source e-commerce platform, committed to providing the best shopping cart experience for both merchants and customers.

9.1k17.8k](/packages/prestashop-prestashop)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

1189.8k](/packages/rcsofttech-audit-trail-bundle)[symfony/ux-toolkit

A tool to easily create a design system in your Symfony app with customizable, well-crafted Twig components

16126.1k1](/packages/symfony-ux-toolkit)[2lenet/crudit-bundle

The easy like Crud'it Bundle.

1616.4k14](/packages/2lenet-crudit-bundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
