responsive_image.module
Same filename in other branches
File
-
core/
modules/ responsive_image/ responsive_image.module
View source
<?php
/**
* @file
*/
use Drupal\Core\Template\Attribute;
use Drupal\Core\Logger\RfcLogLevel;
use Drupal\image\Entity\ImageStyle;
use Drupal\responsive_image\Entity\ResponsiveImageStyle;
use Drupal\responsive_image\ResponsiveImageStyleInterface;
use Drupal\breakpoint\BreakpointInterface;
/**
* Prepares variables for responsive image formatter templates.
*
* Default template: responsive-image-formatter.html.twig.
*
* @param array $variables
* An associative array containing:
* - item: An ImageItem object.
* - item_attributes: An optional associative array of HTML attributes to be
* placed in the img tag.
* - responsive_image_style_id: A responsive image style.
* - url: An optional \Drupal\Core\Url object.
*/
function template_preprocess_responsive_image_formatter(&$variables) {
// Provide fallback to standard image if valid responsive image style is not
// provided in the responsive image formatter.
$responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']);
if ($responsive_image_style) {
$variables['responsive_image'] = [
'#type' => 'responsive_image',
'#responsive_image_style_id' => $variables['responsive_image_style_id'],
];
}
else {
$variables['responsive_image'] = [
'#theme' => 'image',
];
}
$item = $variables['item'];
$attributes = [];
// Do not output an empty 'title' attribute.
if (!is_null($item->title) && mb_strlen($item->title) != 0) {
$attributes['title'] = $item->title;
}
$attributes['alt'] = $item->alt;
// Need to check that item_attributes has a value since it can be NULL.
if ($variables['item_attributes']) {
$attributes += $variables['item_attributes'];
}
if (($entity = $item->entity) && empty($item->uri)) {
$variables['responsive_image']['#uri'] = $entity->getFileUri();
}
else {
$variables['responsive_image']['#uri'] = $item->uri;
}
foreach ([
'width',
'height',
] as $key) {
$variables['responsive_image']["#{$key}"] = $item->{$key};
}
$variables['responsive_image']['#attributes'] = $attributes;
}
/**
* Prepares variables for a responsive image.
*
* Default template: responsive-image.html.twig.
*
* @param $variables
* An associative array containing:
* - uri: The URI of the image.
* - width: The width of the image (if known).
* - height: The height of the image (if known).
* - attributes: Associative array of attributes to be placed in the img tag.
* - responsive_image_style_id: The ID of the responsive image style.
*/
function template_preprocess_responsive_image(&$variables) {
// Make sure that width and height are proper values
// If they exists we'll output them
// @see https://www.w3.org/community/respimg/2012/06/18/florians-compromise/
if (isset($variables['width']) && empty($variables['width'])) {
unset($variables['width']);
unset($variables['height']);
}
elseif (isset($variables['height']) && empty($variables['height'])) {
unset($variables['width']);
unset($variables['height']);
}
$responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']);
// If a responsive image style is not selected, log the error and stop
// execution.
if (!$responsive_image_style) {
$variables['img_element'] = [];
\Drupal::logger('responsive_image')->log(RfcLogLevel::ERROR, 'Failed to load responsive image style: “@style“ while displaying responsive image.', [
'@style' => $variables['responsive_image_style_id'],
]);
return;
}
// Retrieve all breakpoints and multipliers and reverse order of breakpoints.
// By default, breakpoints are ordered from smallest weight to largest:
// the smallest weight is expected to have the smallest breakpoint width,
// while the largest weight is expected to have the largest breakpoint
// width. For responsive images, we need largest breakpoint widths first, so
// we need to reverse the order of these breakpoints.
$breakpoints = array_reverse(\Drupal::service('breakpoint.manager')->getBreakpointsByGroup($responsive_image_style->getBreakpointGroup()));
foreach ($responsive_image_style->getKeyedImageStyleMappings() as $breakpoint_id => $multipliers) {
if (isset($breakpoints[$breakpoint_id])) {
$variables['sources'][] = _responsive_image_build_source_attributes($variables, $breakpoints[$breakpoint_id], $multipliers);
}
}
if (isset($variables['sources']) && count($variables['sources']) === 1 && !isset($variables['sources'][0]['media'])) {
// There is only one source tag with an empty media attribute. This means
// we can output an image tag with the srcset attribute instead of a
// picture tag.
$variables['output_image_tag'] = TRUE;
foreach ($variables['sources'][0] as $attribute => $value) {
if ($attribute != 'type') {
$variables['attributes'][$attribute] = $value;
}
}
$variables['img_element'] = [
'#theme' => 'image',
'#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
'#attributes' => [],
];
}
else {
$variables['output_image_tag'] = FALSE;
// Prepare the fallback image. We use the src attribute, which might cause
// double downloads in browsers that don't support the picture tag.
$variables['img_element'] = [
'#theme' => 'image',
'#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
'#attributes' => [],
];
}
// Get width and height from fallback responsive image style and transfer them
// to img tag so browser can do aspect ratio calculation and prevent
// recalculation of layout on image load.
$dimensions = responsive_image_get_image_dimensions($responsive_image_style->getFallbackImageStyle(), [
'width' => $variables['width'],
'height' => $variables['height'],
], $variables['uri']);
$variables['img_element']['#width'] = $dimensions['width'];
$variables['img_element']['#height'] = $dimensions['height'];
if (isset($variables['attributes'])) {
if (isset($variables['attributes']['alt'])) {
$variables['img_element']['#alt'] = $variables['attributes']['alt'];
unset($variables['attributes']['alt']);
}
if (isset($variables['attributes']['title'])) {
$variables['img_element']['#title'] = $variables['attributes']['title'];
unset($variables['attributes']['title']);
}
if (isset($variables['img_element']['#width'])) {
$variables['attributes']['width'] = $variables['img_element']['#width'];
}
if (isset($variables['img_element']['#height'])) {
$variables['attributes']['height'] = $variables['img_element']['#height'];
}
$variables['img_element']['#attributes'] = $variables['attributes'];
}
}
/**
* Helper function for template_preprocess_responsive_image().
*
* Builds an array of attributes for <source> tags to be used in a <picture>
* tag. In other words, this function provides the attributes for each <source>
* tag in a <picture> tag.
*
* In a responsive image style, each breakpoint has an image style mapping for
* each of its multipliers. An image style mapping can be either of two types:
* 'sizes' (meaning it will output a <source> tag with the 'sizes' attribute) or
* 'image_style' (meaning it will output a <source> tag based on the selected
* image style for this breakpoint and multiplier). A responsive image style
* can contain image style mappings of mixed types (both 'image_style' and
* 'sizes'). For example:
* @code
* $responsive_img_style = ResponsiveImageStyle::create([
* 'id' => 'style_one',
* 'label' => 'Style One',
* 'breakpoint_group' => 'responsive_image_test_module',
* ]);
* $responsive_img_style->addImageStyleMapping('responsive_image_test_module.mobile', '1x', [
* 'image_mapping_type' => 'image_style',
* 'image_mapping' => 'thumbnail',
* ])
* ->addImageStyleMapping('responsive_image_test_module.narrow', '1x', [
* 'image_mapping_type' => 'sizes',
* 'image_mapping' => [
* 'sizes' => '(min-width: 700px) 700px, 100vw',
* 'sizes_image_styles' => [
* 'large' => 'large',
* 'medium' => 'medium',
* ],
* ],
* ])
* ->save();
* @endcode
* The above responsive image style will result in a <picture> tag like this:
* @code
* <picture>
* <source media="(min-width: 0px)" srcset="sites/default/files/styles/thumbnail/image.jpeg" />
* <source media="(min-width: 560px)" sizes="(min-width: 700px) 700px, 100vw" srcset="sites/default/files/styles/large/image.jpeg 480w, sites/default/files/styles/medium/image.jpeg 220w" />
* <img src="fallback.jpeg" />
* </picture>
* @endcode
*
* When all the images in the 'srcset' attribute of a <source> tag have the same
* MIME type, the source tag will get a 'mime-type' attribute as well. This way
* we can gain some front-end performance because browsers can select which
* image (<source> tag) to load based on the MIME types they support (which, for
* instance, can be beneficial for browsers supporting WebP).
* For example:
* A <source> tag can contain multiple images:
* @code
* <source [...] srcset="image1.jpeg 1x, image2.jpeg 2x, image3.jpeg 3x" />
* @endcode
* In the above example we can add the 'mime-type' attribute ('image/jpeg')
* since all images in the 'srcset' attribute of the <source> tag have the same
* MIME type.
* If a <source> tag were to look like this:
* @code
* <source [...] srcset="image1.jpeg 1x, image2.webp 2x, image3.jpeg 3x" />
* @endcode
* We can't add the 'mime-type' attribute ('image/jpeg' vs 'image/webp'). So in
* order to add the 'mime-type' attribute to the <source> tag all images in the
* 'srcset' attribute of the <source> tag need to be of the same MIME type. This
* way, a <picture> tag could look like this:
* @code
* <picture>
* <source [...] mime-type="image/webp" srcset="image1.webp 1x, image2.webp 2x, image3.webp 3x"/>
* <source [...] mime-type="image/jpeg" srcset="image1.jpeg 1x, image2.jpeg 2x, image3.jpeg 3x"/>
* <img src="fallback.jpeg" />
* </picture>
* @endcode
* This way a browser can decide which <source> tag is preferred based on the
* MIME type. In other words, the MIME types of all images in one <source> tag
* need to be the same in order to set the 'mime-type' attribute but not all
* MIME types within the <picture> tag need to be the same.
*
* For image style mappings of the type 'sizes', a width descriptor is added to
* each source. For example:
* @code
* <source media="(min-width: 0px)" srcset="image1.jpeg 100w" />
* @endcode
* The width descriptor here is "100w". This way the browser knows this image is
* 100px wide without having to load it. According to the spec, a multiplier can
* not be present if a width descriptor is.
* For example:
* Valid:
* @code
* <source media="(min-width:0px)" srcset="img1.jpeg 50w, img2.jpeg=100w" />
* @endcode
* Invalid:
* @code
* <source media="(min-width:0px)" srcset="img1.jpeg 50w 1x, img2.jpeg=100w 1x" />
* @endcode
*
* Note: Since the specs do not allow width descriptors and multipliers combined
* inside one 'srcset' attribute, we either have to use something like
* @code
* <source [...] srcset="image1.jpeg 1x, image2.webp 2x, image3.jpeg 3x" />
* @endcode
* to support multipliers or
* @code
* <source [...] sizes"(min-width: 40em) 80vw, 100vw" srcset="image1.jpeg 300w, image2.webp 600w, image3.jpeg 1200w" />
* @endcode
* to support the 'sizes' attribute.
*
* In theory people could add an image style mapping for the same breakpoint
* (but different multiplier) so the array contains an entry for breakpointA.1x
* and breakpointA.2x. If we would output those we will end up with something
* like
* @code
* <source [...] sizes="(min-width: 40em) 80vw, 100vw" srcset="a1.jpeg 300w 1x, a2.jpeg 600w 1x, a3.jpeg 1200w 1x, b1.jpeg 250w 2x, b2.jpeg 680w 2x, b3.jpeg 1240w 2x" />
* @endcode
* which is illegal. So the solution is to merge both arrays into one and
* disregard the multiplier. Which, in this case, would output
* @code
* <source [...] sizes="(min-width: 40em) 80vw, 100vw" srcset="b1.jpeg 250w, a1.jpeg 300w, a2.jpeg 600w, b2.jpeg 680w, a3.jpeg 1200w, b3.jpeg 1240w" />
* @endcode
* See https://www.w3.org/html/wg/drafts/html/master/embedded-content.html#image-candidate-string
* for further information.
*
* @param array $variables
* An array with the following keys:
* - responsive_image_style_id: The \Drupal\responsive_image\Entity\ResponsiveImageStyle
* ID.
* - width: The width of the image (if known).
* - height: The height of the image (if known).
* - uri: The URI of the image file.
* @param \Drupal\breakpoint\BreakpointInterface $breakpoint
* The breakpoint for this source tag.
* @param array $multipliers
* An array with multipliers as keys and image style mappings as values.
*
* @return \Drupal\Core\Template\Attribute
* An object of attributes for the source tag.
*/
function _responsive_image_build_source_attributes(array $variables, BreakpointInterface $breakpoint, array $multipliers) {
if (empty($variables['width']) || empty($variables['height'])) {
$image = \Drupal::service('image.factory')->get($variables['uri']);
$width = $image->getWidth();
$height = $image->getHeight();
}
else {
$width = $variables['width'];
$height = $variables['height'];
}
$extension = pathinfo($variables['uri'], PATHINFO_EXTENSION);
$sizes = [];
$srcset = [];
$derivative_mime_types = [];
// Traverse the multipliers in reverse so the largest image is processed last.
// The last image's dimensions are used for img.srcset height and width.
foreach (array_reverse($multipliers) as $multiplier => $image_style_mapping) {
switch ($image_style_mapping['image_mapping_type']) {
// Create a <source> tag with the 'sizes' attribute.
case 'sizes':
// Loop through the image styles for this breakpoint and multiplier.
foreach ($image_style_mapping['image_mapping']['sizes_image_styles'] as $image_style_name) {
// Get the dimensions.
$dimensions = responsive_image_get_image_dimensions($image_style_name, [
'width' => $width,
'height' => $height,
], $variables['uri']);
// Get MIME type.
$derivative_mime_type = responsive_image_get_mime_type($image_style_name, $extension);
$derivative_mime_types[] = $derivative_mime_type;
// Add the image source with its width descriptor. When a width
// descriptor is used in a srcset, we can't add a multiplier to
// it. Because of this, the image styles for all multipliers of
// this breakpoint should be merged into one srcset and the sizes
// attribute should be merged as well.
if (is_null($dimensions['width'])) {
throw new \LogicException("Could not determine image width for '{$variables['uri']}' using image style with ID: {$image_style_name}. This image style can not be used for a responsive image style mapping using the 'sizes' attribute.");
}
// Use the image width as key so we can sort the array later on.
// Images within a srcset should be sorted from small to large, since
// the first matching source will be used.
$srcset[intval($dimensions['width'])] = _responsive_image_image_style_url($image_style_name, $variables['uri']) . ' ' . $dimensions['width'] . 'w';
$sizes = array_merge(explode(',', $image_style_mapping['image_mapping']['sizes']), $sizes);
}
break;
case 'image_style':
// Get MIME type.
$derivative_mime_type = responsive_image_get_mime_type($image_style_mapping['image_mapping'], $extension);
$derivative_mime_types[] = $derivative_mime_type;
// Add the image source with its multiplier. Use the multiplier as key
// so we can sort the array later on. Multipliers within a srcset should
// be sorted from small to large, since the first matching source will
// be used. We multiply it by 100 so multipliers with up to two decimals
// can be used.
$srcset[intval(mb_substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $variables['uri']) . ' ' . $multiplier;
$dimensions = responsive_image_get_image_dimensions($image_style_mapping['image_mapping'], [
'width' => $width,
'height' => $height,
], $variables['uri']);
break;
}
}
// Sort the srcset from small to large image width or multiplier.
ksort($srcset);
$source_attributes = new Attribute([
'srcset' => implode(', ', array_unique($srcset)),
]);
$media_query = trim($breakpoint->getMediaQuery());
if (!empty($media_query)) {
$source_attributes->setAttribute('media', $media_query);
}
if (count(array_unique($derivative_mime_types)) == 1) {
$source_attributes->setAttribute('type', $derivative_mime_types[0]);
}
if (!empty($sizes)) {
$source_attributes->setAttribute('sizes', implode(',', array_unique($sizes)));
}
// The images used in a particular srcset attribute should all have the same
// aspect ratio. The sizes attribute paired with the srcset attribute provides
// information on how much space these images take up within the viewport at
// different breakpoints, but the aspect ratios should remain the same across
// those breakpoints. Multiple source elements can be used for art direction,
// where aspect ratios should change at particular breakpoints. Each source
// element can still have srcset and sizes attributes to handle variations for
// that particular aspect ratio. Because the same aspect ratio is assumed for
// all images in a srcset, dimensions are always added to the source
// attribute. Within srcset, images are sorted from largest to smallest in
// terms of the real dimension of the image.
if (!empty($dimensions['width']) && !empty($dimensions['height'])) {
$source_attributes->setAttribute('width', $dimensions['width']);
$source_attributes->setAttribute('height', $dimensions['height']);
}
return $source_attributes;
}
/**
* Determines the dimensions of an image.
*
* @param string $image_style_name
* The name of the style to be used to alter the original image.
* @param array $dimensions
* An associative array containing:
* - width: The width of the source image (if known).
* - height: The height of the source image (if known).
* @param string $uri
* The URI of the image file.
*
* @return array
* Dimensions to be modified - an array with components width and height, in
* pixels.
*/
function responsive_image_get_image_dimensions($image_style_name, array $dimensions, $uri) {
// Determine the dimensions of the styled image.
if ($image_style_name == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
$dimensions = [
'width' => 1,
'height' => 1,
];
}
elseif ($entity = ImageStyle::load($image_style_name)) {
$entity->transformDimensions($dimensions, $uri);
}
return $dimensions;
}
/**
* Determines the MIME type of an image.
*
* @param string $image_style_name
* The image style that will be applied to the image.
* @param string $extension
* The original extension of the image (without the leading dot).
*
* @return string
* The MIME type of the image after the image style is applied.
*/
function responsive_image_get_mime_type($image_style_name, $extension) {
if ($image_style_name == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
return 'image/gif';
}
// The MIME type guesser needs a full path, not just an extension, but the
// file doesn't have to exist.
if ($image_style_name === ResponsiveImageStyleInterface::ORIGINAL_IMAGE) {
$fake_path = 'responsive_image.' . $extension;
}
else {
$fake_path = 'responsive_image.' . ImageStyle::load($image_style_name)->getDerivativeExtension($extension);
}
return \Drupal::service('file.mime_type.guesser.extension')->guessMimeType($fake_path);
}
/**
* Wrapper around image_style_url() so we can return an empty image.
*/
function _responsive_image_image_style_url($style_name, $path) {
if ($style_name == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
// The smallest data URI for a 1px square transparent GIF image.
// http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
}
/** @var \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator */
$file_url_generator = \Drupal::service('file_url_generator');
$entity = ImageStyle::load($style_name);
if ($entity instanceof ImageStyle) {
return $file_url_generator->transformRelative($entity->buildUrl($path));
}
return $file_url_generator->generateString($path);
}
Functions
Title | Deprecated | Summary |
---|---|---|
responsive_image_get_image_dimensions | Determines the dimensions of an image. | |
responsive_image_get_mime_type | Determines the MIME type of an image. | |
template_preprocess_responsive_image | Prepares variables for a responsive image. | |
template_preprocess_responsive_image_formatter | Prepares variables for responsive image formatter templates. | |
_responsive_image_build_source_attributes | Helper function for template_preprocess_responsive_image(). | |
_responsive_image_image_style_url | Wrapper around image_style_url() so we can return an empty image. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.