annotate core/modules/responsive_image/responsive_image.module @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 /**
Chris@0 4 * @file
Chris@0 5 * Responsive image display formatter for image fields.
Chris@0 6 */
Chris@0 7
Chris@18 8 use Drupal\Core\Url;
Chris@0 9 use Drupal\Core\Template\Attribute;
Chris@0 10 use Drupal\Core\Logger\RfcLogLevel;
Chris@0 11 use Drupal\Core\Routing\RouteMatchInterface;
Chris@0 12 use Drupal\image\Entity\ImageStyle;
Chris@0 13 use Drupal\responsive_image\Entity\ResponsiveImageStyle;
Chris@18 14 use Drupal\responsive_image\ResponsiveImageStyleInterface;
Chris@0 15 use Drupal\Core\Image\ImageInterface;
Chris@0 16 use Drupal\breakpoint\BreakpointInterface;
Chris@0 17
Chris@0 18 /**
Chris@0 19 * The machine name for the empty image breakpoint image style option.
Chris@0 20 *
Chris@0 21 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Use
Chris@0 22 * Drupal\responsive_image\ResponsiveImageStyleInterface::EMPTY_IMAGE
Chris@0 23 * instead.
Chris@0 24 *
Chris@0 25 * @see https://www.drupal.org/node/2831620
Chris@0 26 */
Chris@0 27 const RESPONSIVE_IMAGE_EMPTY_IMAGE = '_empty image_';
Chris@0 28
Chris@0 29 /**
Chris@0 30 * The machine name for the original image breakpoint image style option.
Chris@0 31 *
Chris@0 32 * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Use
Chris@0 33 * \Drupal\responsive_image\ResponsiveImageStyleInterface::ORIGINAL_IMAGE
Chris@0 34 * instead.
Chris@0 35 *
Chris@0 36 * @see https://www.drupal.org/node/2831620
Chris@0 37 */
Chris@0 38 const RESPONSIVE_IMAGE_ORIGINAL_IMAGE = '_original image_';
Chris@0 39
Chris@0 40 /**
Chris@0 41 * Implements hook_help().
Chris@0 42 */
Chris@0 43 function responsive_image_help($route_name, RouteMatchInterface $route_match) {
Chris@0 44 switch ($route_name) {
Chris@0 45 case 'help.page.responsive_image':
Chris@0 46 $output = '';
Chris@0 47 $output .= '<h3>' . t('About') . '</h3>';
Chris@0 48 $output .= '<p>' . t('The Responsive Image module provides an image formatter that allows browsers to select which image file to display based on media queries or which image file types the browser supports, using the HTML 5 picture and source elements and/or the sizes, srcset and type attributes. For more information, see the <a href=":responsive_image">online documentation for the Responsive Image module</a>.', [':responsive_image' => 'https://www.drupal.org/documentation/modules/responsive_image']) . '</p>';
Chris@0 49 $output .= '<h3>' . t('Uses') . '</h3>';
Chris@0 50 $output .= '<dl>';
Chris@0 51 $output .= '<dt>' . t('Defining responsive image styles') . '</dt>';
Chris@18 52 $output .= '<dd>' . t('By creating responsive image styles you define which options the browser has in selecting which image file to display. In most cases this means providing different image sizes based on the viewport size. On the <a href=":responsive_image_style">Responsive image styles</a> page, click <em>Add responsive image style</em> to create a new style. First choose a label, a fallback image style and a breakpoint group and click Save.', [':responsive_image_style' => Url::fromRoute('entity.responsive_image_style.collection')->toString()]) . '</dd>';
Chris@0 53 $output .= '<dl>';
Chris@0 54 $output .= '<dt>' . t('Fallback image style') . '</dt>';
Chris@0 55 $output .= '<dd>' . t('The fallback image style is typically the smallest size image you expect to appear in this space. Because the responsive images module uses the Picturefill library so that responsive images can work in older browsers, the fallback image should only appear on a site if an error occurs.') . '</dd>';
Chris@0 56 $output .= '<dt>' . t('Breakpoint groups: viewport sizing vs art direction') . '</dt>';
Chris@18 57 $output .= '<dd>' . t('The breakpoint group typically only needs a single breakpoint with an empty media query in order to do <em>viewport sizing.</em> Multiple breakpoints are used for changing the crop or aspect ratio of images at different viewport sizes, which is often referred to as <em>art direction.</em> Once you select a breakpoint group, you can choose which breakpoints to use for the responsive image style. By default, the option <em>do not use this breakpoint</em> is selected for each breakpoint. See the <a href=":breakpoint_help">help page of the Breakpoint module</a> for more information.', [':breakpoint_help' => Url::fromRoute('help.page', ['name' => 'breakpoint'])->toString()]) . '</dd>';
Chris@0 58 $output .= '<dt>' . t('Breakpoint settings: sizes vs image styles') . '</dt>';
Chris@0 59 $output .= '<dd>' . t('While you have the option to provide only one image style per breakpoint, the sizes option allows you to provide more options to browsers as to which image file it can display, even when using multiple breakpoints for art direction. Breakpoints are defined in the configuration files of the theme.') . '</dd>';
Chris@0 60 $output .= '<dt>' . t('Sizes field') . '</dt>';
Chris@0 61 $output .= '<dd>' . t('Once the sizes option is selected, you can let the browser know the size of this image in relation to the site layout, using the <em>Sizes</em> field. For a hero image that always fills the entire screen, you could simply enter 100vw, which means 100% of the viewport width. For an image that fills 90% of the screen for small viewports, but only fills 40% of the screen when the viewport is larger than 40em (typically 640px), you could enter "(min-width: 40em) 40vw, 90vw" in the Sizes field. The last item in the comma-separated list is the smallest viewport size: other items in the comma-separated list should have a media condition paired with an image width. <em>Media conditions</em> are similar to a media query, often a min-width paired with a viewport width using em or px units: e.g. (min-width: 640px) or (min-width: 40em). This is paired with the <em>image width</em> at that viewport size using px, em or vw units. The vw unit is viewport width and is used instead of a percentage because the percentage always refers to the width of the entire viewport.') . '</dd>';
Chris@0 62 $output .= '<dt>' . t('Image styles for sizes') . '</dt>';
Chris@18 63 $output .= '<dd>' . t('Below the Sizes field you can choose multiple image styles so the browser can choose the best image file size to fill the space defined in the Sizes field. Typically you will want to use image styles that resize your image to have options that range from the smallest px width possible for the space the image will appear in to the largest px width possible, with a variety of widths in between. You may want to provide image styles with widths that are 1.5x to 2x the space available in the layout to account for high resolution screens. Image styles can be defined on the <a href=":image_styles">Image styles page</a> that is provided by the <a href=":image_help">Image module</a>.', [':image_styles' => Url::fromRoute('entity.image_style.collection')->toString(), ':image_help' => Url::fromRoute('help.page', ['name' => 'image'])->toString()]) . '</dd>';
Chris@0 64 $output .= '</dl></dd>';
Chris@0 65 $output .= '<dt>' . t('Using responsive image styles in Image fields') . '</dt>';
Chris@18 66 $output .= '<dd>' . t('After defining responsive image styles, you can use them in the display settings for your Image fields, so that the site displays responsive images using the HTML5 picture tag. Open the Manage display page for the entity type (content type, taxonomy vocabulary, etc.) that the Image field is attached to. Choose the format <em>Responsive image</em>, click the Edit icon, and select one of the responsive image styles that you have created. For general information on how to manage fields and their display see the <a href=":field_ui">Field UI module help page</a>. For background information about entities and fields see the <a href=":field_help">Field module help page</a>.', [':field_ui' => (\Drupal::moduleHandler()->moduleExists('field_ui')) ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#', ':field_help' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '</dd>';
Chris@0 67 $output .= '</dl>';
Chris@0 68 return $output;
Chris@0 69
Chris@0 70 case 'entity.responsive_image_style.collection':
Chris@0 71 return '<p>' . t('A responsive image style associates an image style with each breakpoint defined by your theme.') . '</p>';
Chris@0 72 }
Chris@0 73 }
Chris@0 74
Chris@0 75 /**
Chris@0 76 * Implements hook_theme().
Chris@0 77 */
Chris@0 78 function responsive_image_theme() {
Chris@0 79 return [
Chris@0 80 'responsive_image' => [
Chris@0 81 'variables' => [
Chris@0 82 'uri' => NULL,
Chris@0 83 'attributes' => [],
Chris@0 84 'responsive_image_style_id' => [],
Chris@0 85 'height' => NULL,
Chris@0 86 'width' => NULL,
Chris@0 87 ],
Chris@0 88 ],
Chris@0 89 'responsive_image_formatter' => [
Chris@0 90 'variables' => [
Chris@0 91 'item' => NULL,
Chris@0 92 'item_attributes' => NULL,
Chris@0 93 'url' => NULL,
Chris@0 94 'responsive_image_style_id' => NULL,
Chris@0 95 ],
Chris@0 96 ],
Chris@0 97 ];
Chris@0 98 }
Chris@0 99
Chris@0 100 /**
Chris@0 101 * Prepares variables for responsive image formatter templates.
Chris@0 102 *
Chris@0 103 * Default template: responsive-image-formatter.html.twig.
Chris@0 104 *
Chris@0 105 * @param array $variables
Chris@0 106 * An associative array containing:
Chris@0 107 * - item: An ImageItem object.
Chris@0 108 * - item_attributes: An optional associative array of HTML attributes to be
Chris@0 109 * placed in the img tag.
Chris@0 110 * - responsive_image_style_id: A responsive image style.
Chris@0 111 * - url: An optional \Drupal\Core\Url object.
Chris@0 112 */
Chris@0 113 function template_preprocess_responsive_image_formatter(&$variables) {
Chris@0 114 // Provide fallback to standard image if valid responsive image style is not
Chris@0 115 // provided in the responsive image formatter.
Chris@0 116 $responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']);
Chris@0 117 if ($responsive_image_style) {
Chris@0 118 $variables['responsive_image'] = [
Chris@0 119 '#type' => 'responsive_image',
Chris@0 120 '#responsive_image_style_id' => $variables['responsive_image_style_id'],
Chris@0 121 ];
Chris@0 122 }
Chris@0 123 else {
Chris@0 124 $variables['responsive_image'] = [
Chris@0 125 '#theme' => 'image',
Chris@0 126 ];
Chris@0 127 }
Chris@0 128 $item = $variables['item'];
Chris@0 129 $attributes = [];
Chris@0 130 // Do not output an empty 'title' attribute.
Chris@17 131 if (mb_strlen($item->title) != 0) {
Chris@0 132 $attributes['title'] = $item->title;
Chris@0 133 }
Chris@0 134 $attributes['alt'] = $item->alt;
Chris@0 135 // Need to check that item_attributes has a value since it can be NULL.
Chris@0 136 if ($variables['item_attributes']) {
Chris@0 137 $attributes += $variables['item_attributes'];
Chris@0 138 }
Chris@0 139 if (($entity = $item->entity) && empty($item->uri)) {
Chris@0 140 $variables['responsive_image']['#uri'] = $entity->getFileUri();
Chris@0 141 }
Chris@0 142 else {
Chris@0 143 $variables['responsive_image']['#uri'] = $item->uri;
Chris@0 144 }
Chris@0 145
Chris@0 146 foreach (['width', 'height'] as $key) {
Chris@0 147 $variables['responsive_image']["#$key"] = $item->$key;
Chris@0 148 }
Chris@0 149 $variables['responsive_image']['#attributes'] = $attributes;
Chris@0 150 }
Chris@0 151
Chris@0 152 /**
Chris@0 153 * Prepares variables for a responsive image.
Chris@0 154 *
Chris@0 155 * Default template: responsive-image.html.twig.
Chris@0 156 *
Chris@0 157 * @param $variables
Chris@0 158 * An associative array containing:
Chris@0 159 * - uri: The URI of the image.
Chris@0 160 * - width: The width of the image (if known).
Chris@0 161 * - height: The height of the image (if known).
Chris@0 162 * - attributes: Associative array of attributes to be placed in the img tag.
Chris@0 163 * - responsive_image_style_id: The ID of the responsive image style.
Chris@0 164 */
Chris@0 165 function template_preprocess_responsive_image(&$variables) {
Chris@0 166 // Make sure that width and height are proper values
Chris@0 167 // If they exists we'll output them
Chris@0 168 // @see http://www.w3.org/community/respimg/2012/06/18/florians-compromise/
Chris@0 169 if (isset($variables['width']) && empty($variables['width'])) {
Chris@0 170 unset($variables['width']);
Chris@0 171 unset($variables['height']);
Chris@0 172 }
Chris@0 173 elseif (isset($variables['height']) && empty($variables['height'])) {
Chris@0 174 unset($variables['width']);
Chris@0 175 unset($variables['height']);
Chris@0 176 }
Chris@0 177
Chris@0 178 $responsive_image_style = ResponsiveImageStyle::load($variables['responsive_image_style_id']);
Chris@0 179 // If a responsive image style is not selected, log the error and stop
Chris@0 180 // execution.
Chris@0 181 if (!$responsive_image_style) {
Chris@0 182 $variables['img_element'] = [];
Chris@0 183 \Drupal::logger('responsive_image')->log(RfcLogLevel::ERROR, 'Failed to load responsive image style: “@style“ while displaying responsive image.', ['@style' => $variables['responsive_image_style_id']]);
Chris@0 184 return;
Chris@0 185 }
Chris@0 186 // Retrieve all breakpoints and multipliers and reverse order of breakpoints.
Chris@0 187 // By default, breakpoints are ordered from smallest weight to largest:
Chris@0 188 // the smallest weight is expected to have the smallest breakpoint width,
Chris@0 189 // while the largest weight is expected to have the largest breakpoint
Chris@0 190 // width. For responsive images, we need largest breakpoint widths first, so
Chris@0 191 // we need to reverse the order of these breakpoints.
Chris@0 192 $breakpoints = array_reverse(\Drupal::service('breakpoint.manager')->getBreakpointsByGroup($responsive_image_style->getBreakpointGroup()));
Chris@0 193 foreach ($responsive_image_style->getKeyedImageStyleMappings() as $breakpoint_id => $multipliers) {
Chris@0 194 if (isset($breakpoints[$breakpoint_id])) {
Chris@0 195 $variables['sources'][] = _responsive_image_build_source_attributes($variables, $breakpoints[$breakpoint_id], $multipliers);
Chris@0 196 }
Chris@0 197 }
Chris@0 198
Chris@0 199 if (isset($variables['sources']) && count($variables['sources']) === 1 && !isset($variables['sources'][0]['media'])) {
Chris@0 200 // There is only one source tag with an empty media attribute. This means
Chris@0 201 // we can output an image tag with the srcset attribute instead of a
Chris@0 202 // picture tag.
Chris@0 203 $variables['output_image_tag'] = TRUE;
Chris@0 204 foreach ($variables['sources'][0] as $attribute => $value) {
Chris@0 205 if ($attribute != 'type') {
Chris@0 206 $variables['attributes'][$attribute] = $value;
Chris@0 207 }
Chris@0 208 }
Chris@0 209 $variables['img_element'] = [
Chris@0 210 '#theme' => 'image',
Chris@0 211 '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
Chris@0 212 ];
Chris@0 213 }
Chris@0 214 else {
Chris@0 215 $variables['output_image_tag'] = FALSE;
Chris@0 216 // Prepare the fallback image. We use the src attribute, which might cause
Chris@0 217 // double downloads in browsers that don't support the picture tag (might,
Chris@0 218 // because when picturefill kicks in, it cancels the download and triggers
Chris@0 219 // the download for the correct image).
Chris@0 220 $variables['img_element'] = [
Chris@0 221 '#theme' => 'image',
Chris@0 222 '#uri' => _responsive_image_image_style_url($responsive_image_style->getFallbackImageStyle(), $variables['uri']),
Chris@0 223 ];
Chris@0 224 }
Chris@0 225
Chris@0 226 if (isset($variables['attributes'])) {
Chris@0 227 if (isset($variables['attributes']['alt'])) {
Chris@0 228 $variables['img_element']['#alt'] = $variables['attributes']['alt'];
Chris@0 229 unset($variables['attributes']['alt']);
Chris@0 230 }
Chris@0 231 if (isset($variables['attributes']['title'])) {
Chris@0 232 $variables['img_element']['#title'] = $variables['attributes']['title'];
Chris@0 233 unset($variables['attributes']['title']);
Chris@0 234 }
Chris@0 235 $variables['img_element']['#attributes'] = $variables['attributes'];
Chris@0 236 }
Chris@0 237 }
Chris@0 238
Chris@0 239 /**
Chris@0 240 * Helper function for template_preprocess_responsive_image().
Chris@0 241 *
Chris@0 242 * @param \Drupal\Core\Image\ImageInterface $image
Chris@0 243 * The image to build the <source> tags for.
Chris@0 244 * @param array $variables
Chris@0 245 * An array with the following keys:
Chris@0 246 * - responsive_image_style_id: The \Drupal\responsive_image\Entity\ResponsiveImageStyle
Chris@0 247 * ID.
Chris@0 248 * - width: The width of the image (if known).
Chris@0 249 * - height: The height of the image (if known).
Chris@0 250 * - uri: The URI of the image file.
Chris@0 251 * @param \Drupal\breakpoint\BreakpointInterface $breakpoint
Chris@0 252 * The breakpoint for this source tag.
Chris@0 253 * @param array $multipliers
Chris@0 254 * An array with multipliers as keys and image style mappings as values.
Chris@0 255 *
Chris@0 256 * @return \Drupal\Core\Template\Attribute[]
Chris@0 257 * An array of attributes for the source tag.
Chris@0 258 *
Chris@0 259 * @deprecated in Drupal 8.3.x and will be removed before 9.0.0.
Chris@0 260 */
Chris@0 261 function responsive_image_build_source_attributes(ImageInterface $image, array $variables, BreakpointInterface $breakpoint, array $multipliers) {
Chris@0 262 return _responsive_image_build_source_attributes($variables, $breakpoint, $multipliers);
Chris@0 263 }
Chris@0 264
Chris@0 265 /**
Chris@0 266 * Helper function for template_preprocess_responsive_image().
Chris@0 267 *
Chris@0 268 * Builds an array of attributes for <source> tags to be used in a <picture>
Chris@0 269 * tag. In other words, this function provides the attributes for each <source>
Chris@0 270 * tag in a <picture> tag.
Chris@0 271 *
Chris@0 272 * In a responsive image style, each breakpoint has an image style mapping for
Chris@0 273 * each of its multipliers. An image style mapping can be either of two types:
Chris@0 274 * 'sizes' (meaning it will output a <source> tag with the 'sizes' attribute) or
Chris@0 275 * 'image_style' (meaning it will output a <source> tag based on the selected
Chris@0 276 * image style for this breakpoint and multiplier). A responsive image style
Chris@0 277 * can contain image style mappings of mixed types (both 'image_style' and
Chris@0 278 * 'sizes'). For example:
Chris@0 279 * @code
Chris@0 280 * $responsive_img_style = ResponsiveImageStyle::create(array(
Chris@0 281 * 'id' => 'style_one',
Chris@0 282 * 'label' => 'Style One',
Chris@0 283 * 'breakpoint_group' => 'responsive_image_test_module',
Chris@0 284 * ));
Chris@0 285 * $responsive_img_style->addImageStyleMapping('responsive_image_test_module.mobile', '1x', array(
Chris@0 286 * 'image_mapping_type' => 'image_style',
Chris@0 287 * 'image_mapping' => 'thumbnail',
Chris@0 288 * ))
Chris@0 289 * ->addImageStyleMapping('responsive_image_test_module.narrow', '1x', array(
Chris@0 290 * 'image_mapping_type' => 'sizes',
Chris@0 291 * 'image_mapping' => array(
Chris@0 292 * 'sizes' => '(min-width: 700px) 700px, 100vw',
Chris@0 293 * 'sizes_image_styles' => array(
Chris@0 294 * 'large' => 'large',
Chris@0 295 * 'medium' => 'medium',
Chris@0 296 * ),
Chris@0 297 * ),
Chris@0 298 * ))
Chris@0 299 * ->save();
Chris@0 300 * @endcode
Chris@0 301 * The above responsive image style will result in a <picture> tag like this:
Chris@0 302 * @code
Chris@0 303 * <picture>
Chris@0 304 * <source media="(min-width: 0px)" srcset="sites/default/files/styles/thumbnail/image.jpeg" />
Chris@0 305 * <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" />
Chris@0 306 * <img src="fallback.jpeg" />
Chris@0 307 * </picture>
Chris@0 308 * @endcode
Chris@0 309 *
Chris@0 310 * When all the images in the 'srcset' attribute of a <source> tag have the same
Chris@0 311 * MIME type, the source tag will get a 'mime-type' attribute as well. This way
Chris@0 312 * we can gain some front-end performance because browsers can select which
Chris@0 313 * image (<source> tag) to load based on the MIME types they support (which, for
Chris@0 314 * instance, can be beneficial for browsers supporting WebP).
Chris@0 315 * For example:
Chris@0 316 * A <source> tag can contain multiple images:
Chris@0 317 * @code
Chris@0 318 * <source [...] srcset="image1.jpeg 1x, image2.jpeg 2x, image3.jpeg 3x" />
Chris@0 319 * @endcode
Chris@0 320 * In the above example we can add the 'mime-type' attribute ('image/jpeg')
Chris@0 321 * since all images in the 'srcset' attribute of the <source> tag have the same
Chris@0 322 * MIME type.
Chris@0 323 * If a <source> tag were to look like this:
Chris@0 324 * @code
Chris@0 325 * <source [...] srcset="image1.jpeg 1x, image2.webp 2x, image3.jpeg 3x" />
Chris@0 326 * @endcode
Chris@0 327 * We can't add the 'mime-type' attribute ('image/jpeg' vs 'image/webp'). So in
Chris@0 328 * order to add the 'mime-type' attribute to the <source> tag all images in the
Chris@0 329 * 'srcset' attribute of the <source> tag need to be of the same MIME type. This
Chris@0 330 * way, a <picture> tag could look like this:
Chris@0 331 * @code
Chris@0 332 * <picture>
Chris@0 333 * <source [...] mime-type="image/webp" srcset="image1.webp 1x, image2.webp 2x, image3.webp 3x"/>
Chris@0 334 * <source [...] mime-type="image/jpeg" srcset="image1.jpeg 1x, image2.jpeg 2x, image3.jpeg 3x"/>
Chris@0 335 * <img src="fallback.jpeg" />
Chris@0 336 * </picture>
Chris@0 337 * @endcode
Chris@0 338 * This way a browser can decide which <source> tag is preferred based on the
Chris@0 339 * MIME type. In other words, the MIME types of all images in one <source> tag
Chris@0 340 * need to be the same in order to set the 'mime-type' attribute but not all
Chris@0 341 * MIME types within the <picture> tag need to be the same.
Chris@0 342 *
Chris@0 343 * For image style mappings of the type 'sizes', a width descriptor is added to
Chris@0 344 * each source. For example:
Chris@0 345 * @code
Chris@0 346 * <source media="(min-width: 0px)" srcset="image1.jpeg 100w" />
Chris@0 347 * @endcode
Chris@0 348 * The width descriptor here is "100w". This way the browser knows this image is
Chris@0 349 * 100px wide without having to load it. According to the spec, a multiplier can
Chris@0 350 * not be present if a width descriptor is.
Chris@0 351 * For example:
Chris@0 352 * Valid:
Chris@0 353 * @code
Chris@0 354 * <source media="(min-width:0px)" srcset="img1.jpeg 50w, img2.jpeg=100w" />
Chris@0 355 * @endcode
Chris@0 356 * Invalid:
Chris@0 357 * @code
Chris@0 358 * <source media="(min-width:0px)" srcset="img1.jpeg 50w 1x, img2.jpeg=100w 1x" />
Chris@0 359 * @endcode
Chris@0 360 *
Chris@0 361 * Note: Since the specs do not allow width descriptors and multipliers combined
Chris@0 362 * inside one 'srcset' attribute, we either have to use something like
Chris@0 363 * @code
Chris@0 364 * <source [...] srcset="image1.jpeg 1x, image2.webp 2x, image3.jpeg 3x" />
Chris@0 365 * @endcode
Chris@0 366 * to support multipliers or
Chris@0 367 * @code
Chris@0 368 * <source [...] sizes"(min-width: 40em) 80vw, 100vw" srcset="image1.jpeg 300w, image2.webp 600w, image3.jpeg 1200w" />
Chris@0 369 * @endcode
Chris@0 370 * to support the 'sizes' attribute.
Chris@0 371 *
Chris@0 372 * In theory people could add an image style mapping for the same breakpoint
Chris@0 373 * (but different multiplier) so the array contains an entry for breakpointA.1x
Chris@0 374 * and breakpointA.2x. If we would output those we will end up with something
Chris@0 375 * like
Chris@0 376 * @code
Chris@0 377 * <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" />
Chris@0 378 * @endcode
Chris@0 379 * which is illegal. So the solution is to merge both arrays into one and
Chris@0 380 * disregard the multiplier. Which, in this case, would output
Chris@0 381 * @code
Chris@0 382 * <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" />
Chris@0 383 * @endcode
Chris@0 384 * See http://www.w3.org/html/wg/drafts/html/master/embedded-content.html#image-candidate-string
Chris@0 385 * for further information.
Chris@0 386 *
Chris@0 387 * @param array $variables
Chris@0 388 * An array with the following keys:
Chris@0 389 * - responsive_image_style_id: The \Drupal\responsive_image\Entity\ResponsiveImageStyle
Chris@0 390 * ID.
Chris@0 391 * - width: The width of the image (if known).
Chris@0 392 * - height: The height of the image (if known).
Chris@0 393 * - uri: The URI of the image file.
Chris@0 394 * @param \Drupal\breakpoint\BreakpointInterface $breakpoint
Chris@0 395 * The breakpoint for this source tag.
Chris@0 396 * @param array $multipliers
Chris@0 397 * An array with multipliers as keys and image style mappings as values.
Chris@0 398 *
Chris@0 399 * @return \Drupal\Core\Template\Attribute[]
Chris@0 400 * An array of attributes for the source tag.
Chris@0 401 */
Chris@0 402 function _responsive_image_build_source_attributes(array $variables, BreakpointInterface $breakpoint, array $multipliers) {
Chris@0 403 if ((empty($variables['width']) || empty($variables['height']))) {
Chris@0 404 $image = \Drupal::service('image.factory')->get($variables['uri']);
Chris@0 405 $width = $image->getWidth();
Chris@0 406 $height = $image->getHeight();
Chris@0 407 }
Chris@0 408 else {
Chris@0 409 $width = $variables['width'];
Chris@0 410 $height = $variables['height'];
Chris@0 411 }
Chris@0 412 $extension = pathinfo($variables['uri'], PATHINFO_EXTENSION);
Chris@0 413 $sizes = [];
Chris@0 414 $srcset = [];
Chris@0 415 $derivative_mime_types = [];
Chris@0 416 foreach ($multipliers as $multiplier => $image_style_mapping) {
Chris@0 417 switch ($image_style_mapping['image_mapping_type']) {
Chris@0 418 // Create a <source> tag with the 'sizes' attribute.
Chris@0 419 case 'sizes':
Chris@0 420 // Loop through the image styles for this breakpoint and multiplier.
Chris@0 421 foreach ($image_style_mapping['image_mapping']['sizes_image_styles'] as $image_style_name) {
Chris@0 422 // Get the dimensions.
Chris@0 423 $dimensions = responsive_image_get_image_dimensions($image_style_name, ['width' => $width, 'height' => $height], $variables['uri']);
Chris@0 424 // Get MIME type.
Chris@0 425 $derivative_mime_type = responsive_image_get_mime_type($image_style_name, $extension);
Chris@0 426 $derivative_mime_types[] = $derivative_mime_type;
Chris@0 427
Chris@0 428 // Add the image source with its width descriptor. When a width
Chris@0 429 // descriptor is used in a srcset, we can't add a multiplier to
Chris@0 430 // it. Because of this, the image styles for all multipliers of
Chris@0 431 // this breakpoint should be merged into one srcset and the sizes
Chris@0 432 // attribute should be merged as well.
Chris@0 433 if (is_null($dimensions['width'])) {
Chris@0 434 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.");
Chris@0 435 }
Chris@0 436 // Use the image width as key so we can sort the array later on.
Chris@0 437 // Images within a srcset should be sorted from small to large, since
Chris@0 438 // the first matching source will be used.
Chris@0 439 $srcset[intval($dimensions['width'])] = _responsive_image_image_style_url($image_style_name, $variables['uri']) . ' ' . $dimensions['width'] . 'w';
Chris@0 440 $sizes = array_merge(explode(',', $image_style_mapping['image_mapping']['sizes']), $sizes);
Chris@0 441 }
Chris@0 442 break;
Chris@0 443
Chris@0 444 case 'image_style':
Chris@0 445 // Get MIME type.
Chris@0 446 $derivative_mime_type = responsive_image_get_mime_type($image_style_mapping['image_mapping'], $extension);
Chris@0 447 $derivative_mime_types[] = $derivative_mime_type;
Chris@0 448 // Add the image source with its multiplier. Use the multiplier as key
Chris@0 449 // so we can sort the array later on. Multipliers within a srcset should
Chris@0 450 // be sorted from small to large, since the first matching source will
Chris@0 451 // be used. We multiply it by 100 so multipliers with up to two decimals
Chris@0 452 // can be used.
Chris@17 453 $srcset[intval(mb_substr($multiplier, 0, -1) * 100)] = _responsive_image_image_style_url($image_style_mapping['image_mapping'], $variables['uri']) . ' ' . $multiplier;
Chris@0 454 break;
Chris@0 455 }
Chris@0 456 }
Chris@0 457 // Sort the srcset from small to large image width or multiplier.
Chris@0 458 ksort($srcset);
Chris@0 459 $source_attributes = new Attribute([
Chris@0 460 'srcset' => implode(', ', array_unique($srcset)),
Chris@0 461 ]);
Chris@0 462 $media_query = trim($breakpoint->getMediaQuery());
Chris@0 463 if (!empty($media_query)) {
Chris@0 464 $source_attributes->setAttribute('media', $media_query);
Chris@0 465 }
Chris@0 466 if (count(array_unique($derivative_mime_types)) == 1) {
Chris@0 467 $source_attributes->setAttribute('type', $derivative_mime_types[0]);
Chris@0 468 }
Chris@0 469 if (!empty($sizes)) {
Chris@0 470 $source_attributes->setAttribute('sizes', implode(',', array_unique($sizes)));
Chris@0 471 }
Chris@0 472 return $source_attributes;
Chris@0 473 }
Chris@0 474
Chris@0 475 /**
Chris@0 476 * Determines the dimensions of an image.
Chris@0 477 *
Chris@0 478 * @param string $image_style_name
Chris@0 479 * The name of the style to be used to alter the original image.
Chris@0 480 * @param array $dimensions
Chris@0 481 * An associative array containing:
Chris@0 482 * - width: The width of the source image (if known).
Chris@0 483 * - height: The height of the source image (if known).
Chris@0 484 * @param string $uri
Chris@0 485 * The URI of the image file.
Chris@0 486 *
Chris@0 487 * @return array
Chris@0 488 * Dimensions to be modified - an array with components width and height, in
Chris@0 489 * pixels.
Chris@0 490 */
Chris@0 491 function responsive_image_get_image_dimensions($image_style_name, array $dimensions, $uri) {
Chris@0 492 // Determine the dimensions of the styled image.
Chris@18 493 if ($image_style_name == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
Chris@0 494 $dimensions = [
Chris@0 495 'width' => 1,
Chris@0 496 'height' => 1,
Chris@0 497 ];
Chris@0 498 }
Chris@0 499 elseif ($entity = ImageStyle::load($image_style_name)) {
Chris@0 500 $entity->transformDimensions($dimensions, $uri);
Chris@0 501 }
Chris@0 502
Chris@0 503 return $dimensions;
Chris@0 504 }
Chris@0 505
Chris@0 506 /**
Chris@0 507 * Determines the MIME type of an image.
Chris@0 508 *
Chris@0 509 * @param string $image_style_name
Chris@0 510 * The image style that will be applied to the image.
Chris@0 511 * @param string $extension
Chris@0 512 * The original extension of the image (without the leading dot).
Chris@0 513 *
Chris@0 514 * @return string
Chris@0 515 * The MIME type of the image after the image style is applied.
Chris@0 516 */
Chris@0 517 function responsive_image_get_mime_type($image_style_name, $extension) {
Chris@18 518 if ($image_style_name == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
Chris@0 519 return 'image/gif';
Chris@0 520 }
Chris@0 521 // The MIME type guesser needs a full path, not just an extension, but the
Chris@0 522 // file doesn't have to exist.
Chris@18 523 if ($image_style_name === ResponsiveImageStyleInterface::ORIGINAL_IMAGE) {
Chris@0 524 $fake_path = 'responsive_image.' . $extension;
Chris@0 525 }
Chris@0 526 else {
Chris@0 527 $fake_path = 'responsive_image.' . ImageStyle::load($image_style_name)->getDerivativeExtension($extension);
Chris@0 528 }
Chris@0 529 return Drupal::service('file.mime_type.guesser.extension')->guess($fake_path);
Chris@0 530 }
Chris@0 531
Chris@0 532 /**
Chris@0 533 * Wrapper around image_style_url() so we can return an empty image.
Chris@0 534 */
Chris@0 535 function _responsive_image_image_style_url($style_name, $path) {
Chris@18 536 if ($style_name == ResponsiveImageStyleInterface::EMPTY_IMAGE) {
Chris@0 537 // The smallest data URI for a 1px square transparent GIF image.
Chris@0 538 // http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever
Chris@0 539 return 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
Chris@0 540 }
Chris@0 541 $entity = ImageStyle::load($style_name);
Chris@0 542 if ($entity instanceof ImageStyle) {
Chris@0 543 return file_url_transform_relative($entity->buildUrl($path));
Chris@0 544 }
Chris@0 545 return file_url_transform_relative(file_create_url($path));
Chris@0 546 }
Chris@0 547
Chris@0 548 /**
Chris@0 549 * Implements hook_library_info_alter().
Chris@0 550 *
Chris@0 551 * Load responsive_image.js whenever ajax is added.
Chris@0 552 */
Chris@0 553 function responsive_image_library_info_alter(array &$libraries, $module) {
Chris@0 554 if ($module === 'core' && isset($libraries['drupal.ajax'])) {
Chris@0 555 $libraries['drupal.ajax']['dependencies'][] = 'responsive_image/ajax';
Chris@0 556 }
Chris@0 557 }