responsive_image.module

Same filename 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\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.