image.module 18.2 KB
Newer Older
1 2 3 4 5 6 7
<?php

/**
 * @file
 * Exposes global functionality for creating image styles.
 */

8
use Drupal\Core\Entity\EntityInterface;
9
use Drupal\Core\Routing\RouteMatchInterface;
10
use Drupal\file\Entity\File;
11
use Drupal\field\FieldStorageConfigInterface;
12
use Drupal\field\FieldConfigInterface;
13

14 15 16
/**
 * Image style constant for user presets in the database.
 */
17
const IMAGE_STORAGE_NORMAL = 1;
18 19 20 21

/**
 * Image style constant for user presets that override module-defined presets.
 */
22
const IMAGE_STORAGE_OVERRIDE = 2;
23 24 25 26

/**
 * Image style constant for module-defined presets in code.
 */
27
const IMAGE_STORAGE_DEFAULT = 4;
28 29 30 31 32 33 34 35 36 37 38

/**
 * Image style constant to represent an editable preset.
 */
define('IMAGE_STORAGE_EDITABLE', IMAGE_STORAGE_NORMAL | IMAGE_STORAGE_OVERRIDE);

/**
 * Image style constant to represent any module-based preset.
 */
define('IMAGE_STORAGE_MODULE', IMAGE_STORAGE_OVERRIDE | IMAGE_STORAGE_DEFAULT);

39 40 41 42 43
/**
 * The name of the query parameter for image derivative tokens.
 */
define('IMAGE_DERIVATIVE_TOKEN', 'itok');

44
// Load all Field module hooks for Image.
45
require_once __DIR__ . '/image.field.inc';
46

47
/**
48
 * Implements hook_help().
49
 */
50
function image_help($route_name, RouteMatchInterface $route_match) {
51 52
  switch ($route_name) {
    case 'help.page.image':
53
      $output = '';
54
      $output .= '<h3>' . t('About') . '</h3>';
55
      $output .= '<p>' . t('The Image module allows you to manipulate images on your website. It exposes a setting for using the <em>Image toolkit</em>, allows you to configure <em>Image styles</em> that can be used for resizing or adjusting images on display, and provides an <em>Image</em> field for attaching images to content. For more information, see the online handbook entry for <a href="@image">Image module</a>.', array('@image' => 'http://drupal.org/documentation/modules/image')) . '</p>';
56 57 58
      $output .= '<h3>' . t('Uses') . '</h3>';
      $output .= '<dl>';
      $output .= '<dt>' . t('Manipulating images') . '</dt>';
59
      $output .= '<dd>' . t('With the Image module you can scale, crop, resize, rotate and desaturate images without affecting the original image using <a href="@image">image styles</a>. When you change an image style, the module automatically refreshes all created images. Every image style must have a name, which will be used in the URL of the generated images. There are two common approaches to naming image styles (which you use will depend on how the image style is being applied):',array('@image' => \Drupal::url('image.style_list')));
60 61
      $output .= '<ul><li>' . t('Based on where it will be used: eg. <em>profile-picture</em>') . '</li>';
      $output .= '<li>' . t('Describing its appearance: eg. <em>square-85x85</em>') . '</li></ul>';
62
      $output .=  t('After you create an image style, you can add effects: crop, scale, resize, rotate, and desaturate (other contributed modules provide additional effects). For example, by combining effects as crop, scale, and desaturate, you can create square, grayscale thumbnails.') . '<dd>';
63
      $output .= '<dt>' . t('Attaching images to content as fields') . '</dt>';
64
      $output .= '<dd>' . t("Image module also allows you to attach images to content as fields. To add an image field to a <a href='@content-type'>content type</a>, go to the content type's <em>manage fields</em> page, and add a new field of type <em>Image</em>. Attaching images to content this way allows image styles to be applied and maintained, and also allows you more flexibility when theming.", array('@content-type' => \Drupal::url('node.overview_types'))) . '</dd>';
65 66
      $output .= '<dt>' . t('Configuring image fields for accessibility') . '</dt>';
      $output .= '<dd>' . t('For accessibility and search engine optimization, all images that convey meaning on web sites should have alternate text. Drupal also allows entry of title text for images, but it can lead to confusion for screen reader users and its use is not recommended. Image fields can be configured so that alternate and title text fields are enabled or disabled; if enabled, the fields can be set to be required. The recommended setting is to enable and require alternate text and disable title text.') . '</dd>';
67
      $output .= '</dl>';
68
      return $output;
69 70

    case 'image.style_list':
71
      return '<p>' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '</p>';
72 73

    case 'image.effect_add_form':
74
      $effect = \Drupal::service('plugin.manager.image.effect')->getDefinition($route_match->getParameter('image_effect'));
75
      return isset($effect['description']) ? ('<p>' . $effect['description'] . '</p>') : NULL;
76 77

    case 'image.effect_edit_form':
78
      $effect = $route_match->getParameter('image_style')->getEffect($route_match->getParameter('image_effect'));
79 80
      $effect_definition = $effect->getPluginDefinition();
      return isset($effect_definition['description']) ? ('<p>' . $effect_definition['description'] . '</p>') : NULL;
81 82 83
  }
}

84
/**
85
 * Implements hook_theme().
86 87 88
 */
function image_theme() {
  return array(
89
    // Theme functions in image.module.
90
    'image_style' => array(
91 92 93
      // HTML 4 and XHTML 1.0 always require an alt attribute. The HTML 5 draft
      // allows the alt attribute to be omitted in some cases. Therefore,
      // default the alt attribute to an empty string, but allow code calling
94
      // _theme('image_style') to pass explicit NULL for it to be omitted.
95 96
      // Usually, neither omission nor an empty string satisfies accessibility
      // requirements, so it is strongly encouraged for code calling
97
      // _theme('image_style') to pass a meaningful value for the alt variable.
98 99 100 101 102
      // - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
      // - http://www.w3.org/TR/xhtml1/dtds.html
      // - http://dev.w3.org/html5/spec/Overview.html#alt
      // The title attribute is optional in all cases, so it is omitted by
      // default.
103
      'variables' => array(
104
        'style_name' => NULL,
105
        'uri' => NULL,
106 107
        'width' => NULL,
        'height' => NULL,
108
        'alt' => '',
109
        'title' => NULL,
110 111
        'attributes' => array(),
      ),
112
    ),
113 114

    // Theme functions in image.admin.inc.
115
    'image_style_preview' => array(
116
      'variables' => array('style' => NULL),
117
      'file' => 'image.admin.inc',
118 119
    ),
    'image_anchor' => array(
120
      'render element' => 'element',
121
      'file' => 'image.admin.inc',
122 123
    ),
    'image_resize_summary' => array(
124
      'variables' => array('data' => NULL, 'effect' => array()),
125 126
    ),
    'image_scale_summary' => array(
127
      'variables' => array('data' => NULL, 'effect' => array()),
128 129
    ),
    'image_crop_summary' => array(
130
      'variables' => array('data' => NULL, 'effect' => array()),
131 132
    ),
    'image_rotate_summary' => array(
133
      'variables' => array('data' => NULL, 'effect' => array()),
134
    ),
135 136 137

    // Theme functions in image.field.inc.
    'image_widget' => array(
138
      'render element' => 'element',
139
      'file' => 'image.field.inc',
140
    ),
141
    'image_formatter' => array(
142
      'variables' => array('item' => NULL, 'item_attributes' => NULL, 'url' => NULL, 'image_style' => NULL),
143
      'file' => 'image.field.inc',
144
    ),
145 146 147
  );
}

148
/**
149
 * Implements hook_file_download().
150 151 152
 *
 * Control the access to files underneath the styles directory.
 */
153 154 155 156 157 158
function image_file_download($uri) {
  $path = file_uri_target($uri);

  // Private file access for image style derivatives.
  if (strpos($path, 'styles/') === 0) {
    $args = explode('/', $path);
159 160 161

    // Discard "styles", style name, and scheme from the path
    $args = array_slice($args, 3);
162

163
    // Then the remaining parts are the path to the image.
164
    $original_uri = file_uri_scheme($uri) . '://' . implode('/', $args);
165 166

    // Check that the file exists and is an image.
167
    $image = \Drupal::service('image.factory')->get($uri);
168
    if ($image->isValid()) {
169
      // Check the permissions of the original to grant access to this image.
170
      $headers = \Drupal::moduleHandler()->invokeAll('file_download', array($original_uri));
171 172
      // Confirm there's at least one module granting access and none denying access.
      if (!empty($headers) && !in_array(-1, $headers)) {
173
        return array(
174
          // Send headers describing the image's size, and MIME-type...
175 176
          'Content-Type' => $image->getMimeType(),
          'Content-Length' => $image->getFileSize(),
177 178 179
          // By not explicitly setting them here, this uses normal Drupal
          // Expires, Cache-Control and ETag headers to prevent proxy or
          // browser caching of private images.
180 181 182 183 184 185 186 187
        );
      }
    }
    return -1;
  }
}

/**
188
 * Implements hook_file_move().
189
 */
190
function image_file_move(File $file, File $source) {
191
  // Delete any image derivatives at the original image path.
192
  image_path_flush($source->getFileUri());
193 194 195
}

/**
196
 * Implements hook_ENTITY_TYPE_predelete() for file entities.
197
 */
198
function image_file_predelete(File $file) {
199
  // Delete any image derivatives of this image.
200
  image_path_flush($file->getFileUri());
201 202 203
}

/**
204
 * Clears cached versions of a specific file in all styles.
205 206 207 208 209
 *
 * @param $path
 *   The Drupal file path to the original image.
 */
function image_path_flush($path) {
210
  $styles = entity_load_multiple('image_style');
211
  foreach ($styles as $style) {
212
    $style->flush($path);
213 214 215 216
  }
}

/**
217
 * Gets an array of image styles suitable for using as select list options.
218 219
 *
 * @param $include_empty
220
 *   If TRUE a '- None -' option will be inserted in the options array.
221 222 223 224
 * @return
 *   Array of image styles both key and value are set to style name.
 */
function image_style_options($include_empty = TRUE) {
225
  $styles = entity_load_multiple('image_style');
226 227
  $options = array();
  if ($include_empty && !empty($styles)) {
228
    $options[''] = t('- None -');
229
  }
230
  foreach ($styles as $name => $style) {
231
    $options[$name] = $style->label();
232 233
  }

234 235 236 237 238 239 240
  if (empty($options)) {
    $options[''] = t('No defined styles');
  }
  return $options;
}

/**
241 242 243
 * Prepares variables for image style templates.
 *
 * Default template: image-style.html.twig.
244
 *
245
 * @param array $variables
246
 *   An associative array containing:
247 248 249 250 251
 *   - width: The width of the image.
 *   - height: The height of the image.
 *   - style_name: The name of the image style to be applied.
 *   - attributes: Additional attributes to apply to the image.
 *   - uri: URI of the source image before styling.
252 253 254 255 256 257
 *   - alt: The alternative text for text-based browsers. HTML 4 and XHTML 1.0
 *     always require an alt attribute. The HTML 5 draft allows the alt
 *     attribute to be omitted in some cases. Therefore, this variable defaults
 *     to an empty string, but can be set to NULL for the attribute to be
 *     omitted. Usually, neither omission nor an empty string satisfies
 *     accessibility requirements, so it is strongly encouraged for code calling
258
 *     _theme('image_style') to pass a meaningful value for this variable.
259 260 261
 *     - http://www.w3.org/TR/REC-html40/struct/objects.html#h-13.8
 *     - http://www.w3.org/TR/xhtml1/dtds.html
 *     - http://dev.w3.org/html5/spec/Overview.html#alt
262 263 264
 *   - title: The title text is displayed when the image is hovered in some
 *     popular browsers.
 *   - attributes: Associative array of attributes to be placed in the img tag.
265
 */
266
function template_preprocess_image_style(&$variables) {
267 268
  $style = entity_load('image_style', $variables['style_name']);

269 270 271 272 273 274
  // Determine the dimensions of the styled image.
  $dimensions = array(
    'width' => $variables['width'],
    'height' => $variables['height'],
  );

275
  $style->transformDimensions($dimensions);
276

277
  $variables['image'] = array(
278 279 280 281
    '#theme' => 'image',
    '#width' => $dimensions['width'],
    '#height' => $dimensions['height'],
    '#attributes' => $variables['attributes'],
282
    '#uri' => $style->buildUrl($variables['uri']),
283
    '#style_name' => $variables['style_name'],
284 285
  );

286
  if (isset($variables['alt']) || array_key_exists('alt', $variables)) {
287
    $variables['image']['#alt'] = $variables['alt'];
288
  }
289
  if (isset($variables['title']) || array_key_exists('title', $variables)) {
290
    $variables['image']['#title'] = $variables['title'];
291 292
  }

293 294 295
}

/**
296
 * Accepts a keyword (center, top, left, etc) and returns it as a pixel offset.
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
 *
 * @param $value
 * @param $current_pixels
 * @param $new_pixels
 */
function image_filter_keyword($value, $current_pixels, $new_pixels) {
  switch ($value) {
    case 'top':
    case 'left':
      return 0;

    case 'bottom':
    case 'right':
      return $current_pixels - $new_pixels;

    case 'center':
      return $current_pixels / 2 - $new_pixels / 2;
  }
  return $value;
}

318 319 320 321 322
/**
 * Implements hook_entity_presave().
 *
 * Transforms default image of image field from array into single value at save.
 */
323
function image_entity_presave(EntityInterface $entity) {
324
  $field_storage = FALSE;
325
  $entity_type_id = $entity->getEntityTypeId();
326
  if ($entity_type_id == 'field_config') {
327
    $field_storage = $entity->getFieldStorageDefinition();
328
    $default_settings = \Drupal::service('plugin.manager.field.field_type')->getDefaultFieldSettings('image');
329
  }
330 331
  elseif ($entity_type_id == 'field_storage_config') {
    $field_storage = $entity;
332
    $default_settings = \Drupal::service('plugin.manager.field.field_type')->getDefaultStorageSettings('image');
333
  }
334
  // Exit, if not saving an image field storage or image field entity.
335
  if (!$field_storage || $field_storage->type != 'image') {
336 337 338
    return;
  }

339
  if ($field_storage->isSyncing()) {
340 341 342
    return;
  }

343 344 345 346 347
  $uuid = $entity->settings['default_image']['uuid'];
  if ($uuid) {
    $original_uuid = isset($entity->original) ? $entity->original->settings['default_image']['uuid'] : NULL;
    if ($uuid != $original_uuid) {
      $file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid);
348 349 350 351 352 353
      if ($file) {
        $image = \Drupal::service('image.factory')->get($file->getFileUri());
        $entity->settings['default_image']['width'] = $image->getWidth();
        $entity->settings['default_image']['height'] = $image->getHeight();
      }
      else {
354
        $entity->settings['default_image']['uuid'] = NULL;
355
      }
356 357
    }
  }
358 359

  $entity->settings['default_image'] += $default_settings['default_image'];
360
}
361 362

/**
363
 * Implements hook_ENTITY_TYPE_update() for 'field_storage_config'.
364
 */
365 366
function image_field_storage_config_update(FieldStorageConfigInterface $field_storage) {
  if ($field_storage->type != 'image') {
367 368 369 370
    // Only act on image fields.
    return;
  }

371
  $prior_field_storage = $field_storage->original;
372 373

  // The value of a managed_file element can be an array if #extended == TRUE.
374 375
  $uuid_new = $field_storage->settings['default_image']['uuid'];
  $uuid_old = $prior_field_storage->settings['default_image']['uuid'];
376

377
  $file_new = $uuid_new ? \Drupal::entityManager()->loadEntityByUuid('file', $uuid_new) : FALSE;
378

379
  if ($uuid_new != $uuid_old) {
380 381 382 383 384

    // Is there a new file?
    if ($file_new) {
      $file_new->status = FILE_STATUS_PERMANENT;
      $file_new->save();
385
      \Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field_storage->uuid());
386 387 388
    }

    // Is there an old file?
389
    if ($uuid_old && ($file_old = \Drupal::entityManager()->loadEntityByUuid('file', $uuid_old))) {
390
      \Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field_storage->uuid());
391 392 393 394
    }
  }

  // If the upload destination changed, then move the file.
395 396
  if ($file_new && (file_uri_scheme($file_new->getFileUri()) != $field_storage->settings['uri_scheme'])) {
    $directory = $field_storage->settings['uri_scheme'] . '://default_images/';
397 398 399 400 401 402
    file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
    file_move($file_new, $directory . $file_new->filename);
  }
}

/**
403
 * Implements hook_ENTITY_TYPE_update() for 'field_config'.
404
 */
405 406
function image_field_config_update(FieldConfigInterface $field) {
  $field_storage = $field->getFieldStorageDefinition();
407
  if ($field_storage->type != 'image') {
408 409 410 411
    // Only act on image fields.
    return;
  }

412
  $prior_instance = $field->original;
413

414 415
  $uuid_new = $field->settings['default_image']['uuid'];
  $uuid_old = $prior_instance->settings['default_image']['uuid'];
416 417

  // If the old and new files do not match, update the default accordingly.
418 419
  $file_new = $uuid_new ? \Drupal::entityManager()->loadEntityByUuid('file', $uuid_new) : FALSE;
  if ($uuid_new != $uuid_old) {
420 421 422 423
    // Save the new file, if present.
    if ($file_new) {
      $file_new->status = FILE_STATUS_PERMANENT;
      $file_new->save();
424
      \Drupal::service('file.usage')->add($file_new, 'image', 'default_image', $field->uuid());
425 426
    }
    // Delete the old file, if present.
427
    if ($uuid_old && ($file_old = \Drupal::entityManager()->loadEntityByUuid('file', $uuid_old))) {
428
      \Drupal::service('file.usage')->delete($file_old, 'image', 'default_image', $field->uuid());
429 430 431 432
    }
  }

  // If the upload destination changed, then move the file.
433 434
  if ($file_new && (file_uri_scheme($file_new->getFileUri()) != $field_storage->settings['uri_scheme'])) {
    $directory = $field_storage->settings['uri_scheme'] . '://default_images/';
435 436 437 438 439 440
    file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
    file_move($file_new, $directory . $file_new->filename);
  }
}

/**
441
 * Implements hook_ENTITY_TYPE_delete() for 'field_storage_config'.
442
 */
443
function image_field_storage_config_delete(FieldStorageConfigInterface $field) {
444 445 446 447 448 449
  if ($field->type != 'image') {
    // Only act on image fields.
    return;
  }

  // The value of a managed_file element can be an array if #extended == TRUE.
450 451
  $uuid = $field->settings['default_image']['uuid'];
  if ($uuid && ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid))) {
452
    \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
453 454 455 456
  }
}

/**
457
 * Implements hook_ENTITY_TYPE_delete() for 'field_config'.
458
 */
459 460
function image_field_config_delete(FieldConfigInterface $field) {
  $field_storage = $field->getFieldStorageDefinition();
461
  if ($field_storage->type != 'image') {
462 463 464 465
    // Only act on image fields.
    return;
  }

466
  // The value of a managed_file element can be an array if #extended == TRUE.
467
  $uuid = $field->settings['default_image']['uuid'];
468 469

  // Remove the default image when the instance is deleted.
470
  if ($uuid && ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid))) {
471
    \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid());
472 473
  }
}