Image
composantAn 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
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"
/>
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
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é | Type | Par défaut | Description |
|---|---|---|---|
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 -%}