responsive_image.module 12.9 KB
Newer Older
1 2 3 4
<?php

/**
 * @file
5
 * Responsive image display formatter for image fields.
6 7
 */

8
use Drupal\breakpoint\Entity\Breakpoint;
9
use Drupal\Component\Utility\SafeMarkup;
10
use Drupal\Core\Routing\RouteMatchInterface;
11 12
use \Drupal\Core\Template\Attribute;

13 14 15 16 17
/**
 * The machine name for the empty image breakpoint image style option.
 */
const RESPONSIVE_IMAGE_EMPTY_IMAGE = '_empty image_';

18 19 20
/**
 * Implements hook_help().
 */
21
function responsive_image_help($route_name, RouteMatchInterface $route_match) {
22
  $output = '';
23 24
  switch ($route_name) {
    case 'help.page.responsive_image':
25
      $output .= '<h3>' . t('About') . '</h3>';
26
      $output .= '<p>' . t('The Responsive Image module provides an image formatter and breakpoint mappings to output responsive images using the HTML5 picture tag. For more information, see the <a href="!responsive_image">online documentation for the Responsive Image module</a>.', array( '!responsive_image' => 'https://drupal.org/documentation/modules/responsive_image')) . '</p>';
27 28
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
29 30 31 32
      $output .= '<dt>' . t('Defining responsive image mappings') . '</dt>';
      $output .= '<dd>' . t('By creating responsive image mappings you define the image styles that are being used to output images at certain breakpoints. On the <a href="!responsive_image_mapping">Responsive image mappings</a> page, click <em>Add responsive image mapping</em> to create a new mapping. First chose a label and a breakpoint group and click Save. After that you can choose the image styles that will be used for each breakpoint. 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>. Breakpoints are defined in the configuration files of the theme. See the <a href="!breakpoint_help">help page of the Breakpoint module</a> for more information.', array('!responsive_image_mapping' => \Drupal::url('responsive_image.mapping_page'), '!image_styles' => \Drupal::url('image.style_list'),'!image_help' => \Drupal::url('help.page', array('name' => 'image')), '!breakpoint_help' => \Drupal::url('help.page', array('name' => 'breakpoint')))) . '</dd>';
      $output .= '<dt>' . t('Using responsive image mappings in Image fields') . '</dt>';
      $output .= '<dd>' . t('After defining responsive image mappings, 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 mappings that you have created. For general information on how to manage fields and their display see the <a href="!field_ui">help page of the Field UI module</a>. For information about entities see the <a href="!entity_help">help page of the Entity module</a>.', array('!field_ui' => \Drupal::url('help.page', array('name' => 'field_ui')),'!entity_help' => \Drupal::url('help.page', array('name' => 'entity')))) . '</dd>';
33
      $output .= '</dl>';
34
      break;
35 36

    case 'responsive_image.mapping_page':
37
      $output .= '<p>' . t('A responsive image mapping associates an image style with each breakpoint defined by your theme.') . '</p>';
38 39 40 41 42 43 44 45 46
      break;

  }
  return $output;
}

/**
 * Implements hook_permission().
 */
47
function responsive_image_permission() {
48
  return array(
49
    'administer responsive images' => array(
50
      'title' => t('Administer responsive images'),
51 52 53 54
    ),
  );
}

55 56 57 58 59 60 61 62 63 64 65 66 67 68
/**
 * Implements hook_menu().
 */
function responsive_image_menu() {
  $items = array();

  $items['admin/config/media/responsive-image-mapping'] = array(
    'title' => 'Responsive image mappings',
    'description' => 'Manage responsive image mappings',
    'weight' => 10,
    'route_name' => 'responsive_image.mapping_page',
  );
  $items['admin/config/media/responsive-image-mapping/%responsive_image_mapping'] = array(
    'title' => 'Edit responsive image mapping',
69
    'route_name' => 'entity.responsive_image_mapping.edit_form',
70 71 72
  );
  $items['admin/config/media/responsive-image-mapping/%responsive_image_mapping/duplicate'] = array(
    'title' => 'Duplicate responsive image mapping',
73
    'route_name' => 'entity.responsive_image_mapping.duplicate_form',
74 75 76 77 78
  );

  return $items;
}

79 80 81
/**
 * Implements hook_theme().
 */
82
function responsive_image_theme() {
83
  return array(
84
    'responsive_image' => array(
85 86
      'variables' => array(
        'style_name' => NULL,
87
        'uri' => NULL,
88 89 90 91 92 93 94 95
        'width' => NULL,
        'height' => NULL,
        'alt' => '',
        'title' => NULL,
        'attributes' => array(),
        'breakpoints' => array(),
      ),
    ),
96
    'responsive_image_formatter' => array(
97 98 99 100 101 102 103
      'variables' => array(
        'item' => NULL,
        'path' => NULL,
        'image_style' => NULL,
        'breakpoints' => array(),
      ),
    ),
104
    'responsive_image_source' => array(
105 106 107
      'variables' => array(
        'src' => NULL,
        'srcset' => NULL,
108
        'dimensions' => NULL,
109 110 111 112 113 114 115
        'media' => NULL,
      ),
    ),
  );
}

/**
116
 * Returns HTML for a responsive image field formatter.
117 118 119
 *
 * @param array $variables
 *   An associative array containing:
120
 *   - item: An ImageItem object.
121 122 123 124 125 126
 *   - image_style: An optional image style.
 *   - path: An optional array containing the link 'path' and link 'options'.
 *   - breakpoints: An array containing breakpoints.
 *
 * @ingroup themeable
 */
127
function theme_responsive_image_formatter($variables) {
128
  $item = $variables['item'];
129
  if (!isset($variables['breakpoints']) || empty($variables['breakpoints'])) {
130 131
    $image_formatter = array(
      '#theme' => 'image_formatter',
132
      '#item' => $item,
133 134 135 136
      '#image_style' => $variables['image_style'],
      '#path' => $variables['path'],
    );
    return drupal_render($image_formatter);
137 138
  }

139 140
  $responsive_image = array(
    '#theme' => 'responsive_image',
141 142
    '#width' => $item->width,
    '#height' => $item->height,
143 144 145
    '#style_name' => $variables['image_style'],
    '#breakpoints' => $variables['breakpoints'],
  );
146
  if (isset($item->uri)) {
147
    $responsive_image['#uri'] = $item->uri;
148
  }
149
  elseif ($entity = $item->entity) {
150 151
    $responsive_image['#uri'] = $entity->getFileUri();
    $responsive_image['#entity'] = $entity;
152
  }
153
  $responsive_image['#alt'] = $item->alt;
154
  if (drupal_strlen($item->title) != 0) {
155
    $responsive_image['#title'] = $item->title;
156
  }
157
  // @todo Add support for route names.
158 159 160 161
  if (isset($variables['path']['path'])) {
    $path = $variables['path']['path'];
    $options = isset($variables['path']['options']) ? $variables['path']['options'] : array();
    $options['html'] = TRUE;
162
    return l($responsive_image, $path, $options);
163
  }
164

165
  return drupal_render($responsive_image);
166 167 168
}

/**
169
 * Returns HTML for a responsive image.
170 171 172 173 174 175 176 177
 *
 * @param $variables
 *   An associative array containing:
 *   - uri: Either the path of the image file (relative to base_path()) or a
 *     full URL.
 *   - width: The width of the image (if known).
 *   - height: The height of the image (if known).
 *   - alt: The alternative text for text-based browsers.
178 179
 *   - title: The title text is displayed when the image is hovered in some
 *     popular browsers.
180
 *   - style_name: The name of the style to be used as a fallback image.
181 182 183 184
 *   - breakpoints: An array containing breakpoints.
 *
 * @ingroup themeable
 */
185
function theme_responsive_image($variables) {
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201
  // Make sure that width and height are proper values
  // If they exists we'll output them
  // @see http://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']);
  }

  $sources = array();

  // Fallback image, output as source with media query.
  $sources[] = array(
202
    'src' => _responsive_image_image_style_url($variables['style_name'], $variables['uri']),
203
    'dimensions' => responsive_image_get_image_dimensions($variables),
204 205 206 207
  );

  // All breakpoints and multipliers.
  foreach ($variables['breakpoints'] as $breakpoint_name => $multipliers) {
208
    $breakpoint = Breakpoint::load($breakpoint_name);
209 210 211 212 213 214 215 216 217 218 219 220
    if ($breakpoint) {
      $new_sources = array();
      foreach ($multipliers as $multiplier => $image_style) {
        $new_source = $variables;
        $new_source['style_name'] = $image_style;
        $new_source['#multiplier'] = $multiplier;
        $new_sources[] = $new_source;
      }

      // Only one image, use src.
      if (count($new_sources) == 1) {
        $sources[] = array(
221
          'src' => _responsive_image_image_style_url($new_sources[0]['style_name'], $new_sources[0]['uri']),
222
          'dimensions' => responsive_image_get_image_dimensions($new_sources[0]),
223 224 225 226
          'media' => $breakpoint->mediaQuery,
        );
      }
      else {
227
        // Multiple images, use srcset.
228 229
        $srcset = array();
        foreach ($new_sources as $new_source) {
230
          $srcset[] = _responsive_image_image_style_url($new_source['style_name'], $new_source['uri']) . ' ' . $new_source['#multiplier'];
231 232 233
        }
        $sources[] = array(
          'srcset' => implode(', ', $srcset),
234
          'dimensions' => responsive_image_get_image_dimensions($new_sources[0]),
235 236 237 238 239 240 241
          'media' => $breakpoint->mediaQuery,
        );
      }
    }
  }

  if (!empty($sources)) {
242
    $output = array();
243
    $output[] = '<picture>';
244

245
    // Add source tags to the output.
246
    foreach ($sources as $source) {
247 248
      $responsive_image_source = array(
        '#theme' => 'responsive_image_source',
249 250 251 252
        '#src' => $source['src'],
        '#dimensions' => $source['dimensions'],
      );
      if (isset($source['media'])) {
253
        $responsive_image_source['#media'] = $source['media'];
254 255
      }
      if (isset($source['srcset'])) {
256
        $responsive_image_source['#srcset'] = $source['srcset'];
257
      }
258
      $output[] = drupal_render($responsive_image_source);
259 260 261
    }

    $output[] = '</picture>';
262
    return SafeMarkup::set(implode("\n", $output));
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
  }
}

/**
 * Returns HTML for a source tag.
 *
 * @param type $variables
 *   An associative array containing:
 *   - media: The media query to use.
 *   - srcset: The srcset containing the the path of the image file or a full
 *     URL and optionally multipliers.
 *   - src: Either the path of the image file (relative to base_path()) or a
 *     full URL.
 *   - dimensions: The width and height of the image (if known).
 *
 * @ingroup themeable
 */
280
function theme_responsive_image_source($variables) {
281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
  $output = array();
  if (isset($variables['media']) && !empty($variables['media'])) {
    if (!isset($variables['srcset'])) {
      $output[] = '<!-- <source media="' . $variables['media'] . '" src="' . $variables['src'] . '" ' . new Attribute($variables['dimensions']) . ' /> -->';
      $output[] = '<source media="' . $variables['media'] . '" src="' . $variables['src'] . '" ' . new Attribute($variables['dimensions']) . '/>';
    }
    elseif (!isset($variables['src'])) {
      $output[] = '<!-- <source media="' . $variables['media'] . '" srcset="' . $variables['srcset'] . '" ' . new Attribute($variables['dimensions']) . ' /> -->';
      $output[] = '<source media="' . $variables['media'] . '" srcset="' . $variables['srcset'] . '" ' . new Attribute($variables['dimensions']) . ' />';
    }
  }
  else {
    $output[] = '<!-- <source src="' . $variables['src'] . '" ' . new Attribute($variables['dimensions']) . ' /> -->';
    $output[] = '<source src="' . $variables['src'] . '" ' . new Attribute($variables['dimensions']) . '/>';
  }
  return implode("\n", $output);
}

/**
 * Determines the dimensions of an image.
 *
 * @param $variables
 *   An associative array containing:
 *   - style_name: The name of the style to be used to alter the original image.
 *   - width: The width of the source image (if known).
 *   - height: The height of the source image (if known).
 *
 * @return array
 *   Dimensions to be modified - an array with components width and height, in
 *   pixels.
 */
312
function responsive_image_get_image_dimensions($variables) {
313 314 315 316 317 318
  // Determine the dimensions of the styled image.
  $dimensions = array(
    'width' => $variables['width'],
    'height' => $variables['height'],
  );

319 320 321 322 323 324 325 326 327
  if ($variables['style_name'] == RESPONSIVE_IMAGE_EMPTY_IMAGE) {
    $dimensions = array(
      'width' => 1,
      'height' => 1,
    );
  }
  else {
    entity_load('image_style', $variables['style_name'])->transformDimensions($dimensions);
  }
328 329 330

  return $dimensions;
}
331 332 333 334 335 336 337 338 339 340 341 342

/**
 * Wrapper around image_style_url() so we can return an empty image.
 */
function _responsive_image_image_style_url($style_name, $path) {
  if ($style_name == RESPONSIVE_IMAGE_EMPTY_IMAGE) {
    // The smallest data URI for a 1px square transparent GIF image.
    return 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
  }
  return entity_load('image_style', $style_name)->buildUrl($path);
}