You are here

image_resize_filter.module in Image Resize Filter 8

Same filename and directory in other branches
  1. 6 image_resize_filter.module
  2. 7 image_resize_filter.module

File

image_resize_filter.module
View source
<?php

/**
 * @file
 * Contains image_resize_filter.module.
 */
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use GuzzleHttp\Exception\ClientException;

/**
 * Implements hook_help().
 */
function image_resize_filter_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {

    // Main module help for the image_resize_filter module.
    case 'help.page.image_resize_filter':
      $output = '';
      $output .= '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('Resizes images based on width and height attributes and optionally link to the original image.') . '</p>';
      return $output;
    default:
  }
}

/**
 * Parsing function to locate all images in a piece of text that need replacing.
 *
 * @param array $settings
 *   An array of settings that will be used to identify which images need
 *   updating. Includes the following:
 *
 *   - image_locations: An array of acceptable image locations. May contain any
 *     of the following values: "remote". Remote image will be downloaded and
 *     saved locally. This procedure is intensive as the images need to
 *     be retrieved to have their dimensions checked.
 * @param string $text
 *   The text to be updated with the new img src tags.
 *
 * @return array
 *   Return the images.
 */
function image_resize_filter_get_images(array $settings, $text) {
  $images = array();

  // Find all image tags, ensuring that they have a src.
  $matches = array();
  preg_match_all('/((<a [^>]*>)[ ]*)?(<img[^>]*?src[ ]*=[ ]*"([^"]+)"[^>]*>)/i', $text, $matches);

  // Loop through matches and find if replacements are necessary.
  // $matches[0]: All complete image tags and preceding anchors.
  // $matches[1]: The anchor tag of each match (if any).
  // $matches[2]: The anchor tag and trailing whitespace of each match (if any).
  // $matches[3]: The complete img tag.
  // $matches[4]: The src value of each match.
  foreach ($matches[0] as $key => $match) {
    $has_link = (bool) $matches[1][$key];
    $img_tag = $matches[3][$key];

    // Extract the query string (image style token) if any from the url.
    $src_query = parse_url($matches[4][$key], PHP_URL_QUERY);
    if ($src_query) {
      $src = substr($matches[4][$key], 0, -strlen($src_query) - 1);
    }
    else {
      $src = $matches[4][$key];
    }
    $resize = NULL;
    $image_size = NULL;
    $attributes = array();

    // Find attributes of this image tag.
    $attribute_matches = array();
    preg_match_all('/([\\w\\-]+)[ ]*=[ ]*"([^"]*)"/i', $img_tag, $attribute_matches);
    foreach ($attribute_matches[0] as $key => $match) {
      $attribute = $attribute_matches[1][$key];
      $attribute_value = $attribute_matches[2][$key];
      $attributes[$attribute] = $attribute_value;
    }

    // Height and width need to be matched specifically because they may come as
    // either an HTML attribute or as part of a style attribute. FCKeditor
    // specifically has a habit of using style tags instead of height and width.
    foreach (array(
      'width',
      'height',
    ) as $property) {
      $property_matches = array();
      preg_match_all('/[ \'";]' . $property . '[ ]*([=:])[ ]*"?([0-9]+)(%?)"?/i', $img_tag, $property_matches);

      // If this image uses percentage width or height, do not process it.
      if (in_array('%', $property_matches[3])) {
        $resize = FALSE;
        break;
      }

      // In the odd scenario there is both a style="width: xx" and a width="xx"
      // tag, base our calculations off the style tag, since that's what the
      // browser will display.
      $property_key = 0;
      $property_count = count($property_matches[1]);
      if ($property_count) {
        $property_key = array_search(':', $property_matches[1]);
      }
      $attributes[$property] = !empty($property_matches[2][$property_key]) ? $property_matches[2][$property_key] : '';
    }

    // Determine if this is a local or remote file.
    $base_path = base_path();
    $location = 'unknown';
    $host = \Drupal::request()
      ->getHost();
    if (strpos($src, $base_path) === 0) {
      $location = 'local';
    }
    elseif (preg_match('/http[s]?:\\/\\/' . preg_quote($host . $base_path, '/') . '/', $src)) {
      $location = 'local';
    }
    elseif (strpos($src, 'http') === 0) {
      $location = 'remote';
    }

    // If not resizing images in this location or location is unknown, continue on to the next image.
    if ($location == 'unknown' || !$settings['image_locations'][$location]) {
      continue;
    }

    // Convert the URL to a local path.
    $local_path = NULL;
    if ($location == 'local') {

      // Remove the http:// and base path.
      $local_path = preg_replace('/(http[s]?:\\/\\/' . preg_quote($_SERVER['HTTP_HOST'], '/') . ')?' . preg_quote(base_path(), '/') . '/', '', $src, 1);

      // @todo In D8 do we need to do something to support multilaguage?
      $lang_codes = '';

      // Convert to a public file system URI.

      /** @var \Drupal\Core\StreamWrapper\StreamWrapperManager $stream_wrapper_manager **/
      $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
      $public_wrapper = $stream_wrapper_manager
        ->getViaScheme('public');
      $directory_path = $public_wrapper
        ->getDirectoryPath() . '/';
      if (preg_match('!^' . preg_quote($directory_path, '!') . '!', $local_path)) {
        $local_path = 'public://' . preg_replace('!^' . preg_quote($directory_path, '!') . '!', '', $local_path);
      }
      elseif (preg_match('!^(\\?q\\=)?' . $lang_codes . 'system/files/!', $local_path)) {
        $local_path = 'private://' . preg_replace('!^(\\?q\\=)?' . $lang_codes . 'system/files/!', '', $local_path);
      }
      $local_path = rawurldecode($local_path);
    }

    // @todo Support get the original image from a preset
    if ($location == 'remote') {

      // Basic flood prevention on remote images.
      $resize_threshold = 10;

      // Basic flood prevention of resizing.
      $flood = \Drupal::flood();
      if (!$flood
        ->isAllowed('image_resize_filter_remote', $resize_threshold, 120)) {
        \Drupal::messenger()
          ->addMessage(t('Image resize threshold of @count remote images has been reached. Please use fewer remote images.', array(
          '@count' => $resize_threshold,
        )), 'error', FALSE);
        continue;
      }
      $flood
        ->register('image_resize_filter_remote', 120);
      try {
        $result = \Drupal::httpClient()
          ->get($src);
        if ($result
          ->getStatusCode() == 200) {
          $tmp_file = \Drupal::service('file_system')
            ->tempnam('temporary://', 'image_resize_filter_');
          $handle = fopen($tmp_file, 'w');
          fwrite($handle, $result
            ->getBody());
          fclose($handle);
          $local_path = $tmp_file;
        }
      } catch (ClientException $e) {
        \Drupal::logger('image_resize_filter')
          ->error('File %src was not found on remote server.', [
          '%src' => $src,
        ]);
      }
    }

    // Get the image size.
    if (is_file($local_path)) {
      $image_size = @getimagesize($local_path);
    }

    // All this work and the image isn't even there. Bummer. Next image please.
    if (empty($image_size)) {
      image_resize_filter_delete_temp_file($location, $local_path);
      continue;
    }
    $actual_width = (int) $image_size[0];
    $actual_height = (int) $image_size[1];

    // If either height or width is missing, calculate the other.
    if (empty($attributes['width'])) {
      $attributes['width'] = $actual_width;
    }
    if (empty($attributes['height'])) {
      $attributes['height'] = $actual_height;
    }
    if (empty($attributes['height']) && is_numeric($attributes['width'])) {
      $ratio = $actual_height / $actual_width;
      $attributes['height'] = (int) round($ratio * $attributes['width']);
    }
    elseif (empty($attributes['width']) && is_numeric($attributes['height'])) {
      $ratio = $actual_width / $actual_height;
      $attributes['width'] = (int) round($ratio * $attributes['height']);
    }

    // Determine if this image requires a resize.
    if (!isset($resize)) {
      $resize = $actual_width != $attributes['width'] || $actual_height != $attributes['height'];
    }

    // Skip processing if the image is a remote tracking image.
    if ($location == 'remote' && $actual_width == 1 && $actual_height == 1) {
      image_resize_filter_delete_temp_file($location, $local_path);
      continue;
    }

    // Check the image extension by name.
    $extension_matches = array();
    preg_match('/\\.([a-zA-Z0-9]+)$/', $src, $extension_matches);
    if (!empty($extension_matches)) {
      $extension = strtolower($extension_matches[1]);
    }
    elseif (isset($image_size['mime'])) {
      switch ($image_size['mime']) {
        case 'image/png':
          $extension = 'png';
          break;
        case 'image/gif':
          $extension = 'gif';
          break;
        case 'image/jpeg':
        case 'image/pjpeg':
          $extension = 'jpg';
          break;
      }
    }

    // If we're not certain we can resize this image, skip it.
    if (!isset($extension) || !in_array(strtolower($extension), array(
      'png',
      'jpg',
      'jpeg',
      'gif',
    ))) {
      image_resize_filter_delete_temp_file($location, $local_path);
      continue;
    }

    // If getting this far, the image exists and is not the right size, needs
    // to be saved locally from a remote server, or needs attributes added.
    // Add all information to a list of images that need resizing.
    $images[] = array(
      'expected_size' => array(
        'width' => $attributes['width'],
        'height' => $attributes['height'],
      ),
      'actual_size' => array(
        'width' => $image_size[0],
        'height' => $image_size[1],
      ),
      'attributes' => $attributes,
      'resize' => $resize,
      'img_tag' => $img_tag,
      'has_link' => $has_link,
      'original' => $src,
      'original_query' => $src_query,
      'location' => $location,
      'local_path' => $local_path,
      'mime' => $image_size['mime'],
      'extension' => $extension,
    );
  }
  return $images;
}

/**
 * A short-cut function to delete temporary remote images.
 */
function image_resize_filter_delete_temp_file($source, $uri) {
  if ($source == 'remote' && is_file($uri)) {
    @unlink($uri);
  }
}

/**
 * Utility function to return path information.
 */
function image_resize_filter_pathinfo($uri) {
  $info = pathinfo($uri);
  $info['extension'] = substr($uri, strrpos($uri, '.') + 1);
  $info['basename'] = basename($uri);
  $info['filename'] = basename($uri, '.' . $info['extension']);

  // Once Drupal 8.7.x is unsupported remove this IF statement.
  if (floatval(\Drupal::VERSION) >= 8.800000000000001) {
    $info['scheme'] = \Drupal::service('stream_wrapper_manager')
      ->getScheme($uri);
  }
  else {
    $info['scheme'] = \Drupal::service('file_system')
      ->uriScheme($uri);
  }
  $stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
  $stream_wrappers = $stream_wrapper_manager
    ->getWrappers();
  if (empty($info['scheme'])) {
    foreach ($stream_wrappers as $scheme => $stream_wrapper) {
      $scheme_base_path = $stream_wrapper_manager
        ->getViaScheme($scheme)
        ->getDirectoryPath();
      $matches = array();
      if (preg_match('/^' . preg_quote($scheme_base_path, '/') . '\\/?(.*)/', $info['dirname'], $matches)) {
        $info['scheme'] = $scheme;
        $info['dirname'] = $scheme . '://' . $matches[1];
        break;
      }
    }
  }
  return $info;
}

/**
 * Generate a themed image tag based on an image array.
 *
 * @param $image
 *   An array containing image information and properties.
 * @param $settings
 *   Settings for the input filter.
 */
function image_resize_filter_image_tag($image = NULL, $settings = NULL) {
  $src = file_create_url($image['destination']);

  // Strip the http:// from the path if the original did not include it.
  if (!preg_match('/^http[s]?:\\/\\/' . preg_quote($_SERVER['HTTP_HOST']) . '/', $image['original'])) {
    $src = preg_replace('/^http[s]?:\\/\\/' . preg_quote($_SERVER['HTTP_HOST']) . '/', '', $src);
  }

  // Restore any URL query.
  if (isset($image['original_query'])) {
    $src .= '?' . $image['original_query'];
    $image['original'] .= '?' . $image['original_query'];
  }
  $image['attributes']['src'] = $src;
  return '<img' . new Attribute($image['attributes']) . ' />';
}

Functions

Namesort descending Description
image_resize_filter_delete_temp_file A short-cut function to delete temporary remote images.
image_resize_filter_get_images Parsing function to locate all images in a piece of text that need replacing.
image_resize_filter_help Implements hook_help().
image_resize_filter_image_tag Generate a themed image tag based on an image array.
image_resize_filter_pathinfo Utility function to return path information.