Aller au contenu

Image

composant

An image of the content, in two components under the prop names of Symfony UX Image: Image, one img, and Picture, its sources in the order given, AVIF first, before that img. No JavaScript, no inline style: the sources are given whole, already produced. Two Twig functions of the bundle of phil/ux-kit serve them: image_origin(), which keeps the images of other hosts off the page unless their host is listed, and image_sources(), which reads the files of an image among the assets of AssetMapper.

Aperçu

Clair

A bridge at dusk
Code
<twig:Picture
    src="/images/bridge.jpg"
    :sources="[{type: 'image/avif', srcset: '/images/bridge.avif'}, {type: 'image/webp', srcset: '/images/bridge.webp'}]"
    alt="A bridge at dusk"
    :width="800"
    :height="600"
/>

Sombre

A bridge at dusk
Code
<twig:Picture
    src="/images/bridge.jpg"
    :sources="[{type: 'image/avif', srcset: '/images/bridge.avif'}, {type: 'image/webp', srcset: '/images/bridge.webp'}]"
    alt="A bridge at dusk"
    :width="800"
    :height="600"
/>

Usage

Usage

A bridge at dusk
Code
<twig:Image src="{{ asset('images/bridge.jpg') }}" alt="A bridge at dusk" :width="800" :height="600" />

Installation

php bin/console ux:install image --kit=github.com/parler-haut-interagir-librement/ux-kit

Propriétés

PropriétéTypePar défautDescription
srcset string|array<int|string,string>|null null The candidates as HTML writes them, or a map of densities, or of widths with sizes or a fluid layout, to URLs.
sizes string|null null The sizes of a srcset of widths, preceded by "auto, " while the image loads lazily; computed from the layout when null.
layout string|null null How the image fills its container: "fixed", at its width; "constrained", never wider than its width; "full-width"; none by default.
aspect string|null null The ratio of its box, "16/9", "4/3", "3/2", "1/1", "21/9", "3/4", "2/3", "9/16", or "custom", read from a custom property a class sets.
fit string|null null How the image fills its box, "cover" or "contain": "cover" by default when it has a ratio.
position string|null null Where the image is cropped in its box: "center", "top", "bottom", "left" or "right".
priority bool false Whether it is the main image above the fold: loaded eagerly, fetched first and preloaded, its decoding left to the browser.
preload bool|null null Whether the image is preloaded through Symfony WebLink, in a Link header the server may send as an early hint: with priority when null.
loading string|null null How it loads, "lazy" or "eager": "lazy" by default, "eager" with priority.
decoding string|null null How it is decoded, "async", "sync" or "auto": "async" by default, none written with priority.
caption string|null null A caption under the image, which a figure then holds with it; shown even when the image is decorative.
credit string|null null Who made the image, as the site writes it, "Photo: Ana Lima", under the caption.
license string|null null The name of the license of the image, "CC BY 4.0", under the caption.
licenseHref string|null null The address of the license, which its name then links to with rel="license".
creation string|BackedEnum|null null How the image was made, a code of the IPTC Digital Source Type vocabulary such as "digitalCapture" or "trainedAlgorithmicMedia", or an enum whose value is one.
creationLabel string|null null The words that say how the image was made, in the language of the page; the name the vocabulary gives the code when null.

Examples

AVIF, then WebP, then the fallback

The browser takes the first source whose type it reads, then the src of the img.

<twig:Picture
    src="{{ asset('images/bridge.jpg') }}"
    :sources="[
        {type: 'image/avif', srcset: asset('images/bridge.avif')},
        {type: 'image/webp', srcset: asset('images/bridge.webp')},
    ]"
    alt="A bridge at dusk"
    :width="800"
    :height="600"
/>

By density

A map of densities to URLs becomes a srcset of 1x, 2x and 3x candidates, for each source and for the fallback: the static images of a site, produced once and served by AssetMapper.

<twig:Picture
    src="{{ asset('images/1x/network.png') }}"
    :srcset="{1: asset('images/1x/network.png'), 2: asset('images/2x/network.png'), 3: asset('images/3x/network.png')}"
    :sources="[
        {type: 'image/avif', srcset: {1: asset('images/1x/network.avif'), 2: asset('images/2x/network.avif')}},
        {type: 'image/webp', srcset: {1: asset('images/1x/network.webp'), 2: asset('images/2x/network.webp')}},
    ]"
    alt="The network"
    :width="300"
    :height="200"
/>

By width

With sizes, the maps list widths, 480w and 960w; srcset also takes its candidates as HTML writes them. While the image loads lazily, sizes is written after auto,, so that the browsers that know it pick the candidate from the laid-out width, and the others read the rest of the list.

<twig:Image
    src="/images/hall.jpg"
    :srcset="{480: '/images/hall-480.jpg', 960: '/images/hall-960.jpg'}"
    sizes="(min-width: 48rem) 50vw, 100vw"
    alt="The main hall"
    :width="960"
    :height="640"
/>

A source takes its own sizes, or that of the picture.

A layout

layout says how the image fills its container, and computes sizes from width when none is given: constrained never grows wider than its width ((min-width: 960px) 960px, 100vw), full-width spans its container (100vw), fixed keeps its width and reads its maps as densities. Without a layout, the image shrinks with its container and sizes is as given.

<twig:Image
    src="/images/hall.jpg"
    :srcset="{480: '/images/hall-480.jpg', 960: '/images/hall-960.jpg'}"
    layout="constrained"
    alt="The main hall"
    :width="960"
    :height="640"
/>

A ratio, a fit and a crop

aspect keeps the image's box to a ratio, 16/9, 4/3, 3/2, 1/1, 21/9, 3/4, 2/3 or 9/16, the picture covering it by default; fit="contain" shows it whole, and position says where it is cropped. All are classes, never an inline style, so a strict Content Security Policy keeps them.

<twig:Image src="/images/team.jpg" aspect="1/1" position="top" alt="The team" :width="1200" :height="800" />

aspect="custom" reads --phil-image-ratio, set by a class of the site's stylesheet passed in class.

Art direction

A source with media is chosen only where its media query holds: a tight crop on phones, a wide one elsewhere. The sources with a query come first, in the order of their first query, each group AVIF, then WebP, then the others; then the sources without a query. Each source takes the width and height of its crop, so that its space is reserved too.

<twig:Picture
    src="/images/crowd-wide.jpg"
    :sources="[
        {type: 'image/avif', srcset: '/images/crowd-wide.avif'},
        {type: 'image/avif', srcset: '/images/crowd-square.avif', media: '(max-width: 40rem)', width: 600, height: 600},
        {type: 'image/webp', srcset: '/images/crowd-square.webp', media: '(max-width: 40rem)', width: 600, height: 600},
    ]"
    alt="The crowd"
    :width="1600"
    :height="900"
/>

A media holding <, >, {, } or ; leaves its source out. A URL in a srcset holds no space, which would end it: write %20.

The main image of the page

priority writes loading="eager" and fetchpriority="high" and leaves the decoding to the browser, as the prop of the same name of Symfony UX Image. loading and decoding still override it. It also preloads the image through Symfony WebLink: preload() adds a Link: </images/cover.jpg>; rel="preload"; as="image" header to the response, with its imagesrcset, imagesizes, fetchpriority, crossorigin and referrerpolicy, which FrankenPHP can send as an early hint (103) when the controller asks for it. preload sets the preload apart from priority: false leaves it out, true asks for it without priority.

<twig:Image src="/images/cover.jpg" alt="The stage" :width="1600" :height="900" :priority="true" />

A Picture preloads its first source, with its type, so that a browser that does not read that type skips the preload and fetches the fallback as usual. With a media query, a dark or a still variant, which only the browser can choose among, nothing is preloaded: the image is still fetched first.

Dark and still variants

dark is offered where the system is dark, prefers-color-scheme: dark; still, the still image of an animated one, where the reader reduces motion, prefers-reduced-motion: reduce. Each is a URL, a map of densities, or sources of their own types, written before the other sources, the still image first.

<twig:Picture
    src="/images/map.png"
    :dark="[{type: 'image/avif', srcset: '/images/map-dark.avif'}, {type: 'image/png', srcset: '/images/map-dark.png'}]"
    alt="The map of the city"
    :width="800"
    :height="600"
/>
<twig:Picture src="/images/spinning-top.gif" still="/images/spinning-top.png" alt="A spinning top" :width="320" :height="320" />

The system's scheme decides, not the data-theme a page may set: a site whose theme the reader chooses gives the logo of its theme through {theme}, as the Logo recipe does.

Credit, license and creation

credit, license with licenseHref, and creation follow the caption, each in a small of its own, and make a figure on their own. The license links to its address with rel="license". creation takes a code of the IPTC Digital Source Type vocabulary, digitalCapture for a photograph, humanEdits, compositeWithTrainedAlgorithmicMedia for an image edited with generative AI, trainedAlgorithmicMedia for one created with it, or an enum whose value is one, as phil/creation-disclosure holds them; it is said in the vocabulary's words, or in those of creationLabel, and written in data-digital-source-type.

<twig:Image
    src="/images/bridge.jpg"
    alt="A bridge at dusk"
    :width="800"
    :height="600"
    caption="The bridge at dusk"
    credit="Photo: Ana Lima"
    license="CC BY 4.0"
    licenseHref="https://creativecommons.org/licenses/by/4.0/"
    creation="digitalCapture"
    creationLabel="{{ 'image.creation.photo'|trans }}"
/>

The files of an image among the assets

image_sources() finds the files of one image among the assets of AssetMapper by their names, logo.avif, logo@2x.avif, logo.webp, logo.png, logo@2x.png, and gives the src, srcset, sources, width and height of a Picture, with the versioned URLs of AssetMapper. It converts nothing: the files are those the site produced.

<twig:Picture {{ ...image_sources('images/logo') }} alt="Studio" />
<twig:Picture {{ ...image_sources('images/hall', {widths: [480, 960]}) }} sizes="100vw" alt="The main hall" />

The options are densities, beyond 1x, [2, 3] by default; widths, which read hall@480w.avif and the others instead; formats, the types of the sources in order, ['avif', 'webp']; and fallbacks, the formats of the img tried in order, ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp']. The width and height are those of the fallback's file without suffix, read by PHP, never for SVG. An image without fallback is an error.

A caption

caption puts the image, or the picture, in a figure with its figcaption, the text escaped. A decorative image, with an empty alt, stays decorative: its caption still shows.

<twig:Image src="/images/map.png" alt="The map of the city" :width="800" :height="600" caption="Where we meet" />

Other attributes

Of the other attributes, only id, class, title, crossorigin, referrerpolicy, itemprop, draggable, data-* and aria-* reach the img, the class added to isImage; an event handler, a style or any other is left out. On a Picture, they go to its img too.

<twig:Image src="/images/team.jpg" alt="The team" :width="800" :height="533" class="rounded" itemprop="image" />

Images of other hosts

An image of another host is refused unless its host is listed, so that no page calls a third party behind the reader's back: a relative path, a data:image/ one and an address of the page's own origin are the page's; any other address, //cdn.example.com/a.jpg included, is read by phil/url and allowed when its host is one of the list or under one.

# config/packages/phil_ux_kit.yaml
phil_ux_kit:
    image:
        hosts: ['images.example.com']

An image of a listed host is fetched with referrerpolicy="no-referrer", unless the page gives another policy; a crossorigin stays the page's choice, as it fails where the host sends no CORS header. A refused src renders nothing; a refused srcset is left out whole, as is a source holding a refused address. allow_localhost: true lets localhost be listed, in development.

Security

Every value is escaped. A javascript:, vbscript: or data: URL is refused, in whatever case, after spaces or control characters, or cut by tabs and line breaks, but a data:image/ one, which an img shows without running anything. A refused src renders nothing; a refused srcset is left out whole, as is a source holding a refused URL, without candidates, or of a type other than image/*.

Accessibility

alt is required: it says what the image shows, for those who do not see it. An empty alt makes the image decorative, skipped by screen readers through its implicit role; its title and aria-* attributes, which would name it again, are then left out. width and height are required: the browser reserves the image's space at its ratio before it loads, and the page does not shift under the reader (Cumulative Layout Shift); isImage keeps the ratio when the container is narrower. Give priority to the main image above the fold, never to an image below it. The still variant serves those whom motion makes ill (WCAG 2.3.3); the creation is said in words, not by an icon alone.

Fichiers à copier

templates/components/Image.html.twig
{#-
 # An image: one img, in a figure when it has a caption, a credit, a license or a creation, under the prop names of
 # Symfony UX Image, without a service, a script or an inline style; its layout, ratio, fit and crop written as classes.
 # A javascript:, vbscript: or data: URL other than data:image/ is refused, and so is an address of another host than
 # those image_origin() allows; a refused src renders nothing, a refused srcset is left out whole. The image of a listed
 # host is fetched without a referrer; the main image is preloaded through Symfony WebLink.
 #}
{%- props
    ## string The URL of the image.
    src,
    ## string The text alternative; an empty string for a decorative image, which screen readers then skip.
    alt,
    ## int The width of the image, so that its space is reserved at its ratio before it loads.
    width,
    ## int The height of the image, so that its space is reserved at its ratio before it loads.
    height,
    ## string|array<int|string,string>|null The candidates as HTML writes them, or a map of densities, or of widths with sizes or a fluid layout, to URLs.
    srcset = null,
    ## string|null The sizes of a srcset of widths, preceded by "auto, " while the image loads lazily; computed from the layout when null.
    sizes = null,
    ## string|null How the image fills its container: "fixed", at its width; "constrained", never wider than its width; "full-width"; none by default.
    layout = null,
    ## string|null The ratio of its box, "16/9", "4/3", "3/2", "1/1", "21/9", "3/4", "2/3", "9/16", or "custom", read from a custom property a class sets.
    aspect = null,
    ## string|null How the image fills its box, "cover" or "contain": "cover" by default when it has a ratio.
    fit = null,
    ## string|null Where the image is cropped in its box: "center", "top", "bottom", "left" or "right".
    position = null,
    ## bool Whether it is the main image above the fold: loaded eagerly, fetched first and preloaded, its decoding left to the browser.
    priority = false,
    ## bool|null Whether the image is preloaded through Symfony WebLink, in a Link header the server may send as an early hint: with priority when null.
    preload = null,
    ## string|null How it loads, "lazy" or "eager": "lazy" by default, "eager" with priority.
    loading = null,
    ## string|null How it is decoded, "async", "sync" or "auto": "async" by default, none written with priority.
    decoding = null,
    ## string|null A caption under the image, which a figure then holds with it; shown even when the image is decorative.
    caption = null,
    ## string|null Who made the image, as the site writes it, "Photo: Ana Lima", under the caption.
    credit = null,
    ## string|null The name of the license of the image, "CC BY 4.0", under the caption.
    license = null,
    ## string|null The address of the license, which its name then links to with rel="license".
    licenseHref = null,
    ## string|BackedEnum|null How the image was made, a code of the IPTC Digital Source Type vocabulary such as "digitalCapture" or "trainedAlgorithmicMedia", or an enum whose value is one.
    creation = null,
    ## string|null The words that say how the image was made, in the language of the page; the name the vocabulary gives the code when null.
    creationLabel = null
-%}
{%- set _refused = '#(^|,)[[:cntrl:] ]*(javascript:|vbscript:|data:(?!image/))#i' -%}
{%- set _controls = {"\t": '', "\n": '', "\r": ''} -%}
{%- set _origin = src is empty or src|replace(_controls) matches _refused ? 'refused' : image_origin(src) -%}
{%- if _origin is not same as('refused') -%}
    {%- set _loading = loading in ['lazy', 'eager'] ? loading : (priority ? 'eager' : 'lazy') -%}
    {%- set _sizes = sizes is not empty ? sizes : {
        constrained: width is empty ? null : '(min-width: ' ~ width ~ 'px) ' ~ width ~ 'px, 100vw',
        'full-width': '100vw',
    }[layout] ?? null -%}
    {%- set _srcset = srcset is iterable ? srcset|map((url, key) => url ~ ' ' ~ key ~ (_sizes is empty ? 'x' : 'w'))|join(', ') : srcset -%}
    {%- set _set_origin = _srcset is empty ? null : (_srcset|replace(_controls) matches _refused ? 'refused' : image_origin(_srcset, true)) -%}
    {%- set _srcset = _set_origin in [null, 'refused'] ? null : _srcset -%}
    {%- set _preload_sizes = _srcset is null or _sizes is empty ? null : _sizes -%}
    {%- set _sizes = _preload_sizes is null ? null : (_loading is same as('lazy') and not (_sizes|lower starts with 'auto') ? 'auto, ' ~ _sizes : _sizes) -%}
    {%- set _referrer = attributes.all.referrerpolicy ?? (_origin is same as('external') or _set_origin is same as('external') ? 'no-referrer' : null) -%}
    {%- if preload ?? priority -%}
        {%- do preload(src, {
            as: 'image',
            imagesrcset: _srcset,
            imagesizes: _preload_sizes,
            fetchpriority: priority ? 'high' : null,
            crossorigin: attributes.all.crossorigin ?? null,
            referrerpolicy: _referrer,
        }|filter(value => value is not null)) -%}
    {%- endif -%}
    {%- set _ratio = {'16/9': 'hasRatio16x9', '4/3': 'hasRatio4x3', '3/2': 'hasRatio3x2', '1/1': 'hasRatio1x1', '21/9': 'hasRatio21x9', '3/4': 'hasRatio3x4', '2/3': 'hasRatio2x3', '9/16': 'hasRatio9x16', custom: 'hasRatio'}[aspect|default('')|replace({' ': ''})] ?? null -%}
    {%- set _class = [
        'isImage',
        {fixed: 'hasLayoutFixed', 'full-width': 'hasLayoutFullWidth'}[layout] ?? null,
        _ratio,
        {cover: 'hasFitCover', contain: 'hasFitContain'}[fit ?? (_ratio is null ? '' : 'cover')] ?? null,
        {center: 'hasPositionCenter', top: 'hasPositionTop', bottom: 'hasPositionBottom', left: 'hasPositionLeft', right: 'hasPositionRight'}[position] ?? null,
        attributes.all.class ?? null,
    ]|filter(name => name is not empty)|join(' ') -%}
    {%- set _allowed = alt is same as('')
        ? '#^(id|crossorigin|referrerpolicy|itemprop|draggable|data-[a-z0-9-]+)$#'
        : '#^(id|title|crossorigin|referrerpolicy|itemprop|draggable|(data|aria)-[a-z0-9-]+)$#' -%}
    {%- set _attributes = attributes.all|filter((value, name) => name matches _allowed)|merge(_referrer is null ? {} : {referrerpolicy: _referrer}) -%}
    <twig:Image:Figure :caption="caption" :credit="credit" :license="license" :licenseHref="licenseHref" :creation="creation" :creationLabel="creationLabel">
        <img {{ html_attr({
            class: _class,
            src: src,
            srcset: _srcset,
            sizes: _sizes,
            alt: alt,
            width: width,
            height: height,
            loading: _loading,
            decoding: decoding in ['async', 'sync', 'auto'] ? decoding : (priority ? null : 'async'),
            fetchpriority: priority ? 'high' : null,
        }, _attributes) }}>
    </twig:Image:Figure>
{%- endif -%}
templates/components/Image/Figure.html.twig
{#-
 # The figure of an image or a picture: its content alone, or held in a figure with a figcaption when it has a caption,
 # a credit, a license or a creation of the IPTC Digital Source Type vocabulary, every text escaped. A license links to
 # its address with rel="license" unless that address is refused; a code outside the vocabulary says nothing.
 #}
{%- props
    ## string|null A caption under the image.
    caption = null,
    ## string|null Who made the image, as the site writes it, "Photo: Ana Lima".
    credit = null,
    ## string|null The name of the license of the image, "CC BY 4.0".
    license = null,
    ## string|null The address of the license, which its name then links to with rel="license".
    licenseHref = null,
    ## string|BackedEnum|null How the image was made, a code of the IPTC Digital Source Type vocabulary, or an enum whose value is one.
    creation = null,
    ## string|null The words that say how the image was made; the name the vocabulary gives the code when null.
    creationLabel = null
-%}
{%- set _vocabulary = {
    digitalCapture: 'Digital capture sampled from real life',
    computationalCapture: 'Multi-frame computational capture sampled from real life',
    negativeFilm: 'Digitised from a transparent negative',
    positiveFilm: 'Digitised from a transparent positive',
    print: 'Digitised from a non-transparent medium',
    humanEdits: 'Human-edited media',
    compositeWithTrainedAlgorithmicMedia: 'Edited using Generative AI',
    algorithmicallyEnhanced: 'Algorithmically-altered media',
    digitalCreation: 'Digital creation',
    dataDrivenMedia: 'Data-driven media',
    trainedAlgorithmicMedia: 'Created using Generative AI',
    algorithmicMedia: 'Pure algorithmic media',
    screenCapture: 'Screen capture',
    virtualRecording: 'Virtual event recording',
    composite: 'Composite of elements',
    compositeCapture: 'Composite of captured elements',
    compositeSynthetic: 'Composite including generative AI elements',
    minorHumanEdits: 'Original media with minor human edits',
    softwareImage: 'Created by software',
    digitalArt: 'Digital art',
} -%}
{%- set _code = creation is null ? null : (creation.value is defined ? creation.value : creation) -%}
{%- set _creation = _code is not null and _code in _vocabulary|keys ? (creationLabel is not empty ? creationLabel : _vocabulary[_code]) : null -%}
{%- set _href = licenseHref is empty or licenseHref|replace({"\t": '', "\n": '', "\r": ''}) matches '#^[[:cntrl:] ]*(javascript:|vbscript:|data:)#i' ? null : licenseHref -%}
{%- if caption is not empty or credit is not empty or license is not empty or _creation is not null -%}
    <figure>
        {##- The image or the picture the figure holds. -#}
        {%- block content %}{% endblock -%}
        <figcaption>
            {{- caption -}}
            {%- if credit is not empty %}<small class="isCredit">{{ credit }}</small>{% endif -%}
            {%- if license is not empty %}<small class="isLicense">{% if _href is not null %}<a href="{{ _href }}" rel="license">{{ license }}</a>{% else %}{{ license }}{% endif %}</small>{% endif -%}
            {%- if _creation is not null %}<small {{ html_attr({class: 'isCreation', 'data-digital-source-type': _code}) }}>{{ _creation }}</small>{% endif -%}
        </figcaption>
    </figure>
{%- else -%}
    {{- block('content') -}}
{%- endif -%}
templates/components/Picture.html.twig
{#-
 # A picture: its sources before the Image that holds the fallback, under the prop names of Symfony UX Image, in a figure
 # when it has a caption, a credit, a license or a creation. The still image for a reader who reduces motion comes
 # first, then the dark variant, then the sources with a media query, in the order of their first media query, each
 # group AVIF, then WebP, then the other types; a source of another type than image/*, without candidates, holding a
 # refused URL, an address of a host image_origin() refuses or a media query of the wrong shape is left out; a refused
 # src renders nothing. Without a media query, the main picture preloads its first source. The other attributes go to
 # the img.
 #}
{%- props
    ## string The URL of the fallback image, for the browsers that read none of the sources.
    src,
    ## string The text alternative; an empty string for a decorative image, which screen readers then skip.
    alt,
    ## int The width of the image, so that its space is reserved at its ratio before it loads.
    width,
    ## int The height of the image, so that its space is reserved at its ratio before it loads.
    height,
    ## list<array{type?:string,srcset:string|array<int|string,string>,sizes?:string,media?:string,width?:int,height?:int}> The sources: a type, candidates as for srcset, sizes, a media query and the size of its crop.
    sources = [],
    ## string|array<int|string,string>|list<array{type?:string,srcset:string|array<int|string,string>}>|null The variant drawn for a dark system, offered where prefers-color-scheme is dark: candidates as for srcset, or sources of their own types.
    dark = null,
    ## string|array<int|string,string>|list<array{type?:string,srcset:string|array<int|string,string>}>|null The still image of an animated one, offered where prefers-reduced-motion is reduce: candidates as for srcset, or sources of their own types.
    still = null,
    ## string|array<int|string,string>|null The candidates as HTML writes them, or a map of densities, or of widths with sizes or a fluid layout, to URLs.
    srcset = null,
    ## string|null The sizes of a srcset of widths, preceded by "auto, " while the image loads lazily; computed from the layout when null.
    sizes = null,
    ## string|null How the image fills its container: "fixed", at its width; "constrained", never wider than its width; "full-width"; none by default.
    layout = null,
    ## string|null The ratio of its box, "16/9", "4/3", "3/2", "1/1", "21/9", "3/4", "2/3", "9/16", or "custom", read from a custom property a class sets.
    aspect = null,
    ## string|null How the image fills its box, "cover" or "contain": "cover" by default when it has a ratio.
    fit = null,
    ## string|null Where the image is cropped in its box: "center", "top", "bottom", "left" or "right".
    position = null,
    ## bool Whether it is the main image above the fold: loaded eagerly, fetched first and preloaded, its decoding left to the browser.
    priority = false,
    ## bool|null Whether the first source, or the image without sources, is preloaded through Symfony WebLink: with priority when null; never with a media query.
    preload = null,
    ## string|null How it loads, "lazy" or "eager": "lazy" by default, "eager" with priority.
    loading = null,
    ## string|null How it is decoded, "async", "sync" or "auto": "async" by default, none written with priority.
    decoding = null,
    ## string|null A caption under the image, which a figure then holds with it; shown even when the image is decorative.
    caption = null,
    ## string|null Who made the image, as the site writes it, "Photo: Ana Lima", under the caption.
    credit = null,
    ## string|null The name of the license of the image, "CC BY 4.0", under the caption.
    license = null,
    ## string|null The address of the license, which its name then links to with rel="license".
    licenseHref = null,
    ## string|BackedEnum|null How the image was made, a code of the IPTC Digital Source Type vocabulary such as "digitalCapture" or "trainedAlgorithmicMedia", or an enum whose value is one.
    creation = null,
    ## string|null The words that say how the image was made, in the language of the page; the name the vocabulary gives the code when null.
    creationLabel = null
-%}
{%- set _refused = '#(^|,)[[:cntrl:] ]*(javascript:|vbscript:|data:(?!image/))#i' -%}
{%- set _controls = {"\t": '', "\n": '', "\r": ''} -%}
{%- if src is not empty and not (src|replace(_controls) matches _refused) and image_origin(src) is not same as('refused') -%}
    {%- set _lazy = (loading in ['lazy', 'eager'] ? loading : (priority ? 'eager' : 'lazy')) is same as('lazy') -%}
    {%- set _default = sizes is not empty ? sizes : {
        constrained: width is empty ? null : '(min-width: ' ~ width ~ 'px) ' ~ width ~ 'px, 100vw',
        'full-width': '100vw',
    }[layout] ?? null -%}
    {%- set _all = [] -%}
    {%- for _variant in [{value: still, media: '(prefers-reduced-motion: reduce)'}, {value: dark, media: '(prefers-color-scheme: dark)'}] -%}
        {%- if _variant.value is not empty -%}
            {%- set _list = _variant.value is iterable and (_variant.value|first).srcset is defined ? _variant.value : [{srcset: _variant.value}] -%}
            {%- set _all = _all|merge(_list|map(source => source|merge({media: _variant.media}))) -%}
        {%- endif -%}
    {%- endfor -%}
    {%- set _all = _all|merge(sources) -%}
    {%- set _medias = [] -%}
    {%- for source in _all -%}
        {%- set _media = (source.media ?? '')|trim -%}
        {%- if _media is not empty and _media not in _medias -%}{%- set _medias = _medias|merge([_media]) -%}{%- endif -%}
    {%- endfor -%}
    {%- set _sources = [] -%}
    {%- set _external = false -%}
    {%- for source in _all -%}
        {%- set _type = source.type ?? null -%}
        {%- set _media = (source.media ?? '')|trim -%}
        {%- set _sizes = source.sizes ?? _default -%}
        {%- set _set = source.srcset ?? null -%}
        {%- set _set = _set is iterable ? _set|map((url, key) => url ~ ' ' ~ key ~ (_sizes is empty ? 'x' : 'w'))|join(', ') : _set -%}
        {%- set _origin = _set is empty or _set|replace(_controls) matches _refused ? 'refused' : image_origin(_set, true) -%}
        {%- if _origin is not same as('refused')
            and (_type is null or _type matches '#^image/[a-z0-9.+-]+$#')
            and (_media is empty or _media matches '#^[^<>{};]+$#') -%}
            {%- set _external = _external or _origin is same as('external') -%}
            {%- set _sources = _sources|merge([{
                order: (_media is empty ? 999 : _medias|keys|filter(index => _medias[index] is same as(_media))|first) * 10 + ({'image/avif': 0, 'image/webp': 1}[_type] ?? (_type is null ? 3 : 2)),
                preload_sizes: _sizes is empty ? null : _sizes,
                attributes: {
                    type: _type,
                    srcset: _set,
                    sizes: _sizes is empty ? null : (_lazy and not (_sizes|lower starts with 'auto') ? 'auto, ' ~ _sizes : _sizes),
                    media: _media is empty ? null : _media,
                    width: (source.width ?? null) matches '#^[1-9][0-9]*$#' ? source.width : null,
                    height: (source.height ?? null) matches '#^[1-9][0-9]*$#' ? source.height : null,
                },
            }]) -%}
        {%- endif -%}
    {%- endfor -%}
    {%- set _sources = _sources|sort((a, b) => a.order - b.order) -%}
    {%- set _attributes = _external and attributes.all.referrerpolicy is not defined ? attributes.all|merge({referrerpolicy: 'no-referrer'}) : attributes.all -%}
    {%- if _sources is not empty and (preload ?? priority) and _sources|filter(source => source.attributes.media is not null) is empty -%}
        {%- set _first = _sources|first -%}
        {%- do preload(src, {
            as: 'image',
            imagesrcset: _first.attributes.srcset,
            imagesizes: _first.preload_sizes,
            type: _first.attributes.type,
            fetchpriority: priority ? 'high' : null,
            crossorigin: _attributes.crossorigin ?? null,
            referrerpolicy: _attributes.referrerpolicy ?? null,
        }|filter(value => value is not null)) -%}
    {%- endif -%}
    <twig:Image:Figure :caption="caption" :credit="credit" :license="license" :licenseHref="licenseHref" :creation="creation" :creationLabel="creationLabel">
        {%- if _sources is not empty -%}<picture>{%- endif -%}
        {%- for source in _sources -%}<source {{ html_attr(source.attributes) }}>{%- endfor -%}
        <twig:Image :src="src" :alt="alt" :width="width" :height="height" :srcset="srcset" :sizes="sizes" :layout="layout" :aspect="aspect" :fit="fit" :position="position" :priority="priority" :preload="_sources is empty ? preload : false" :loading="loading" :decoding="decoding" {{ ..._attributes }} />
        {%- if _sources is not empty -%}</picture>{%- endif -%}
    </twig:Image:Figure>
{%- endif -%}