responsive_image.module

Same filename and directory in other branches
  1. 9 core/modules/responsive_image/responsive_image.module
  2. 8.9.x core/modules/responsive_image/responsive_image.module
  3. 10 core/modules/responsive_image/responsive_image.module

File

core/modules/responsive_image/responsive_image.module

View source
<?php


/**
 * @file
 */

use Drupal\Core\File\MimeType\MimeTypeMapInterface;
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) : void {
  // 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 array $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) : void {
  // 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|null
 *   The MIME type of the image after the image style is applied, or NULL if not
 *   found.
 */
function responsive_image_get_mime_type($image_style_name, $extension) {
  $extension = match ($image_style_name) {  ResponsiveImageStyleInterface::EMPTY_IMAGE => 'gif',
    ResponsiveImageStyleInterface::ORIGINAL_IMAGE => $extension,
    default => ImageStyle::load($image_style_name)->getDerivativeExtension($extension),
  
  };
  return \Drupal::service(MimeTypeMapInterface::class)->getMimeTypeForExtension($extension);
}

/**
 * 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.